diff --git a/.env.example b/.env.example deleted file mode 100644 index 7031e00..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# Optional: only needed if AI detection paths are enabled in your build/UI. -OPENAI_API_KEY= diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index bb310db..089b391 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -24,6 +24,4 @@ Project maintainers may remove, edit, or reject comments, commits, issues, and p ## Reporting -Report concerns privately to: - -- `mratcliff1995@gmail.com` +Use GitHub's built-in content reporting tools for abusive content. For project-specific concerns, open a brief issue without including private or sensitive details so a maintainer can arrange an appropriate follow-up. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 87f9c85..1491401 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,11 +7,11 @@ Thanks for improving Photo Cutter. 1. Install .NET SDK 8.x. 2. Restore packages: ```powershell - dotnet restore .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj + dotnet restore .\\solution\\ImageUiSlicer.sln ``` 3. Run locally: ```powershell - dotnet run --project .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj + dotnet run --project .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release ``` ## Pull request guidelines @@ -22,7 +22,8 @@ Thanks for improving Photo Cutter. - Update docs for any user-facing workflow changes. - Ensure build passes before pushing: ```powershell - dotnet build .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release + dotnet build .\\solution\\ImageUiSlicer.sln -c Release + dotnet run --project .\\solution\\ImageUiSlicer.Tests\\ImageUiSlicer.Tests.csproj -c Release ``` ## Commit style diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b9dd925..0f7cc5e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,8 +20,8 @@ body: description: Exact clicks/inputs so we can reproduce. placeholder: | 1. Open image ... - 2. Select Polygon ... - 3. Click Commit Cutout ... + 2. Select Click Around ... + 3. Select Save Cut-out ... validations: required: true - type: input @@ -29,7 +29,7 @@ body: attributes: label: App version description: Release version or commit hash. - placeholder: v1.0.0 + placeholder: v1.2.0 - type: dropdown id: os attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c610da2..1c28cf1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,7 +12,8 @@ Describe the change and why it is needed. ## Verification -- [ ] `dotnet build .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release` passes +- [ ] `dotnet build .\\solution\\ImageUiSlicer.sln -c Release` passes +- [ ] Regression checks pass - [ ] Manual run tested - [ ] Screenshots attached (if UI changed) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index a5f5ebc..5a882a5 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -8,9 +8,7 @@ Photo Cutter currently supports the latest release on the `main` branch and late Please do not open public issues for security vulnerabilities. -Report privately by email: - -- `mratcliff1995@gmail.com` +Use [GitHub's private vulnerability reporting form](https://github.com/Awetspoon/Photo-Cutter/security/advisories/new). This keeps reports and follow-up details private until a fix is ready. Include: @@ -19,4 +17,4 @@ Include: - Potential impact - Optional proof-of-concept -We will acknowledge receipt and provide an update as soon as possible. +The project maintainers will acknowledge the report and provide an update as soon as possible. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c73514c..ec91663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,10 @@ jobs: dotnet-version: 8.0.x - name: Restore - run: dotnet restore .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj + run: dotnet restore .\\solution\\ImageUiSlicer.sln - name: Build (Release) - run: dotnet build .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release --no-restore + run: dotnet build .\\solution\\ImageUiSlicer.sln -c Release --no-restore + + - name: Test runtime workflows + run: dotnet run --project .\\solution\\ImageUiSlicer.Tests\\ImageUiSlicer.Tests.csproj -c Release --no-build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a154a9d..427452c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,12 +4,6 @@ on: push: tags: - 'v*' - workflow_dispatch: - inputs: - tag: - description: 'Release tag (example: v1.0.0)' - required: true - type: string permissions: contents: write @@ -30,41 +24,60 @@ jobs: - name: Resolve release tag id: tag shell: pwsh + env: + RELEASE_TAG: ${{ github.ref_name }} run: | - if ("${{ github.ref_type }}" -eq "tag") { - $tag = "${{ github.ref_name }}" - } - else { - $tag = "${{ inputs.tag }}" + $tag = $env:RELEASE_TAG + if ($tag -notmatch '^v(?\d+\.\d+\.\d+)$') { + throw "Release tag must use full semantic versioning (example: v1.2.0)." } - if ([string]::IsNullOrWhiteSpace($tag)) { - throw "Release tag is required." - } + "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "version=$($Matches['version'])" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - if (-not $tag.StartsWith("v")) { - throw "Release tag must start with 'v' (example: v1.0.0)." - } + - name: Restore solution + run: dotnet restore .\\solution\\ImageUiSlicer.sln - "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + - name: Build solution + run: dotnet build .\\solution\\ImageUiSlicer.sln -c Release --no-restore - - name: Restore - run: dotnet restore .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj + - name: Run regression checks + run: dotnet run --project .\\solution\\ImageUiSlicer.Tests\\ImageUiSlicer.Tests.csproj -c Release --no-build - name: Publish single-file executable - run: dotnet publish .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:EnableCompressionInSingleFile=true /p:DebugType=None /p:DebugSymbols=false + run: dotnet publish .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:EnableCompressionInSingleFile=true /p:DebugType=None /p:DebugSymbols=false /p:Version=${{ steps.tag.outputs.version }} /p:AssemblyVersion=${{ steps.tag.outputs.version }}.0 /p:FileVersion=${{ steps.tag.outputs.version }}.0 /p:InformationalVersion=${{ steps.tag.outputs.version }} - - name: Prepare release assets + - name: Prepare verified release assets and notes shell: pwsh + env: + RELEASE_TAG: ${{ steps.tag.outputs.tag }} run: | New-Item -ItemType Directory -Force .\\release-assets | Out-Null - Copy-Item .\\solution\\ImageUiSlicer\\bin\\Release\\net8.0-windows\\win-x64\\publish\\ImageUiSlicer.exe .\\release-assets\\PhotoCutter.exe -Force + $publishedApp = '.\\solution\\ImageUiSlicer\\bin\\Release\\net8.0-windows\\win-x64\\publish\\ImageUiSlicer.exe' + $releaseApp = '.\\release-assets\\PhotoCutter.exe' + if (-not (Test-Path -LiteralPath $publishedApp)) { + throw "Published executable was not found: $publishedApp" + } + + Copy-Item -LiteralPath $publishedApp -Destination $releaseApp -Force + $hash = (Get-FileHash -LiteralPath $releaseApp -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash PhotoCutter.exe" | Set-Content -LiteralPath '.\\release-assets\\PhotoCutter.exe.sha256' -Encoding ascii + + $releaseNotes = ".\\docs\\releases\\$env:RELEASE_TAG.md" + if (-not (Test-Path -LiteralPath $releaseNotes)) { + throw "Release notes were not found: $releaseNotes" + } + + Copy-Item -LiteralPath $releaseNotes -Destination '.\\release-assets\\release-notes.md' -Force - name: Publish GitHub release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: tag_name: ${{ steps.tag.outputs.tag }} name: Photo Cutter ${{ steps.tag.outputs.tag }} - generate_release_notes: true + body_path: release-assets/release-notes.md + fail_on_unmatched_files: true + make_latest: true files: | release-assets/PhotoCutter.exe + release-assets/PhotoCutter.exe.sha256 diff --git a/.gitignore b/.gitignore index 583f7ac..8365549 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,11 @@ **/bin/ **/obj/ -# Local SDK/NuGet caches -.dotnet/ -.dotnet-cli/ -.nuget/ +# Local SDK/NuGet caches and agent workspaces +/.agents/ +/.audit*/ +/.dotnet*/ +/.nuget/ # Visual Studio / Rider / VS Code .vs/ @@ -38,7 +39,6 @@ Desktop.ini # Environment and secrets .env .env.* -!.env.example # Publish artifacts publish/ diff --git a/README.md b/README.md index 02d9ccb..900ab34 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,71 @@ -# Photo Cutter +# Photo Cutter [![Release](https://img.shields.io/github/v/release/Awetspoon/Photo-Cutter?display_name=release)](https://github.com/Awetspoon/Photo-Cutter/releases/latest) [![Build](https://img.shields.io/github/actions/workflow/status/Awetspoon/Photo-Cutter/ci.yml?branch=main&label=build)](https://github.com/Awetspoon/Photo-Cutter/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -[![Platform](https://img.shields.io/badge/Platform-Windows%2010%2F11-0078D6)](https://github.com/Awetspoon/Photo-Cutter/releases/latest) +[![Platform](https://img.shields.io/badge/Windows-10%20%7C%2011-0078D6)](https://github.com/Awetspoon/Photo-Cutter/releases/latest) -Windows desktop app for cutting transparent PNG assets from UI mockups and screenshots. +Turn screenshots and images into clean, transparent PNG cut-outs. -![Photo Cutter Wordmark](solution/ImageUiSlicer/Assets/Brand/ImageUiSlicer_Wordmark_Transparent.png) +**[Download Photo Cutter for Windows](https://github.com/Awetspoon/Photo-Cutter/releases/latest/download/PhotoCutter.exe)** -## Download +Photo Cutter is a privacy-friendly, portable Windows app. It runs as one file, needs no account, and keeps image processing on your computer. -- Latest release: [PhotoCutter.exe](https://github.com/Awetspoon/Photo-Cutter/releases/latest) -- Distribution format: single-file Windows executable (`PhotoCutter.exe`) +![Photo Cutter editor showing a cut-out workflow](docs/screenshots/photo-cutter-main.png) -## Core Features +## Make your first cut-out -- Selection tools: `Select`, `Lasso`, `Polygon`, `Shapes` -- Reusable custom shapes: save, apply, duplicate, paste -- Brush refinement (`Brush +` / `Brush -`) on active selections and cutouts -- Inspector preview with optional split compare -- Cutout Gallery for fast visual review -- Export presets, naming options, edge/outline controls -- Project save/load support (`.iusproj`) +1. Select **Open image** and choose a PNG, JPG, BMP, GIF, or WEBP file. +2. Use **Draw Around**, **Click Around**, or **Shape Cut** to outline what you want to keep. +3. Select **Save Cut-out**, give it a useful name, then export it as a transparent PNG. -## Quick Start +The labels are deliberately plain English. **Draw Around** is the freehand tool, while **Click Around** builds an outline point by point. -1. Open an image. -2. Draw a selection with `Shapes`, `Lasso`, or `Polygon`. -3. Click `Commit Cutout`. -4. Optional: save selection as reusable shape. -5. Export selected/all cutouts as PNG. +## What you can do -## Build From Source +- Cut with freehand drawing, connected points, or ready-made shapes. +- Zoom without changing the source image or exported resolution. +- Use a 5x–10x magnifier and choose the cutting-line colour for close edge work. +- Switch between smooth enlarged viewing and exact-pixel viewing. +- Save an exact-size reusable shape, rename it, place copies, move them, and commit each copy as a new cut-out. +- Rename and reorder committed cut-outs before exporting. +- Refine edges, compare previews, and review results in the Cut-out Gallery. +- Save a workspace and continue later without losing your cut-outs or saved shapes. +- Use the complete interface in dark or light appearance. -### Requirements +## Download and requirements -- Windows 10/11 -- .NET SDK 8.0.124+ (see `global.json`) +- Windows 10 or Windows 11, 64-bit. +- One portable `PhotoCutter.exe`; no separate .NET installation is required. +- Input: PNG, JPG/JPEG, BMP, GIF, and WEBP. +- Output: transparent PNG files at the intended source-image geometry. -### Restore +Download the latest release, place `PhotoCutter.exe` wherever you prefer, and open it. The app is currently unsigned, so Windows SmartScreen may ask you to confirm the first launch. Use the SHA-256 file supplied with each release if you want to verify the download. -```powershell -dotnet restore .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -``` +## Privacy -### Run +Photo Cutter does not upload your images and does not require an account. Cutting, previews, workspaces, and exports are handled locally on your PC. -```powershell -dotnet run --project .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -``` +## Help and updates -### Build - -```powershell -dotnet build .\\solution\\ImageUiSlicer\\ImageUiSlicer.csproj -c Release -``` +- [Download the latest release](https://github.com/Awetspoon/Photo-Cutter/releases/latest) +- [Read what changed](docs/CHANGELOG.md) +- [Report a problem or request an improvement](https://github.com/Awetspoon/Photo-Cutter/issues) +- [Report a security issue privately](https://github.com/Awetspoon/Photo-Cutter/security/advisories/new) -## Releasing +## Build from source -Tag and push a semantic version to publish a GitHub Release: +You need Windows and the .NET 8 SDK. From the repository folder: ```powershell -git tag v1.1.1 -git push origin v1.1.1 +dotnet restore ".\solution\ImageUiSlicer.sln" +dotnet build ".\solution\ImageUiSlicer.sln" -c Release --no-restore +dotnet run --project ".\solution\ImageUiSlicer.Tests\ImageUiSlicer.Tests.csproj" -c Release --no-build +dotnet run --project ".\solution\ImageUiSlicer\ImageUiSlicer.csproj" -c Release ``` -Release workflow uploads one asset: `PhotoCutter.exe`. - -Detailed process: [docs/RELEASE.md](docs/RELEASE.md) - -## Repository Structure - -```text -.github/ # workflows, templates, community standards -docs/ # release guide, changelog, specs, archives -solution/ImageUiSlicer/ # WPF app source and assets -``` - -## Project Standards - -- Contributing: [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) -- Security: [.github/SECURITY.md](.github/SECURITY.md) -- Changelog: [docs/CHANGELOG.md](docs/CHANGELOG.md) -- Code of conduct: [.github/CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md) +Developer references: [architecture](docs/ARCHITECTURE.md), [behaviour contract](docs/BEHAVIOR_CONTRACT.md), [contributing](.github/CONTRIBUTING.md), and [release process](docs/RELEASE.md). ## License -MIT. See [LICENSE](LICENSE). +Photo Cutter is available under the [MIT License](LICENSE). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..5f041cf --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,284 @@ +# Photo Cutter Architecture + +Photo Cutter is being reorganised without changing what the product is for: +accurate, repeatable transparent PNG cut-outs from one source image. The +non-negotiable user-visible outcomes live in +[`BEHAVIOR_CONTRACT.md`](BEHAVIOR_CONTRACT.md). A structural change is not +complete unless it preserves that contract. + +The folders below describe the architecture that exists now. The final section +also records boundaries that are still staged so they are not mistaken for +finished separation. + +## Shared Cut-out Pipeline + +```text +Canvas tool or saved shape + | + v +PathGeometryModel in original image pixels + | + v +MainViewModel.SetActiveSelection + | + v +MainViewModel.CommitSelection + | + v +CutoutModel in Project.Cutouts + | + +--> Presentation/Preview + | `--> CutoutRenderService --> WPF preview + | + `--> ExportBatchCoordinator + `--> ExportService --> CutoutRenderService --> PNG +``` + +Important rules: + +- Canvas zoom, pan, magnifier level, smoothing, and theme are display-only. +- Geometry is stored in original source-image pixel coordinates. +- `CommitSelection` is the normal production path that creates a committed + `CutoutModel`. +- Inspector preview and PNG export share `CutoutRenderService`, including crop, + mask, padding, scaling, and outline behaviour. +- Saved shapes are exact-size workspace data. Placement translates their + geometry and never silently resizes it. +- Built-in tools may create geometry differently, but they do not get private + commit, preview, or export pipelines. + +## Current Folder Ownership + +### `Core/Geometry` + +Shared geometry rules that are used by the editor, renderers, and workflows: + +- `GeometryHelper` validates paths, calculates bounds, clips to the source + image, builds Skia paths, and translates geometry. +- `GeometryMovementPolicy` provides the shared image-boundary clamping rules + for precise and whole-pixel movement. +- `ShapePresetCatalog`, `ShapeGeometryFactory`, and `ShapeCutoutPreset` define + protected built-in shapes and generate their geometry. + +This folder does not own WPF controls or editor state. It does use SkiaSharp for +path operations that must agree with rendering. + +### `CanvasEngine` + +`CanvasController` is a partial class split by interaction responsibility: + +| File | Responsibility | +| --- | --- | +| `CanvasController.Input.cs` | Tool changes and keyboard dispatch | +| `CanvasController.PointerInput.cs` | Pointer gesture routing | +| `CanvasController.Viewport.cs` | Fit, native pixels, zoom, pan, and coordinate conversion | +| `Viewport/ViewportScalePolicy.cs` | Fit-scale rules, including the native-size cap | +| `CanvasController.Selection.cs` | Hit testing and active-outline movement | +| `CanvasController.Drawing.cs` | Draw Around and Click Around tools | +| `CanvasController.Shapes.cs` | Built-in and saved-shape placement | +| `CanvasController.Brush.cs` | Add-to-edge and remove-from-edge gestures | +| `CanvasController.Rendering.cs` | Source image and committed/active overlays | +| `CanvasController.RenderingGuides.cs` | In-progress tool overlays | +| `CanvasController.PolygonAngleLabels.cs` | Click Around angle labels | +| `CanvasController.Magnifier.cs` | Precision lens, pixel grid, and magnified cutting lines | +| `CanvasController.cs` | Attachment lifecycle and shared state reset | + +`CanvasEngine/Contracts/ICanvasEditorContext.cs` is the boundary between the +canvas engine and application state. The controller depends on +`ICanvasEditorState` and `ICanvasEditorActions`, not on `MainViewModel`. +`MainViewModel` currently implements the combined contract. + +### `Models` + +Models hold workspace and editing data: source metadata, geometry, selections, +cut-outs, saved shapes, export settings, and application settings. Persistent +geometry remains independent of zoom and screen size. + +Compatibility fields may remain even when they are no longer shown. Removing a +field from the interface is not permission to remove it from saved workspace +data. + +### `ViewModels` + +`MainViewModel` remains one partial class, split by workflow: + +| File | Responsibility | +| --- | --- | +| `MainViewModel.cs` | Construction and core project state | +| `MainViewModel.CanvasState.cs` | Tools, zoom, pan, status, and brush state | +| `MainViewModel.InspectorState.cs` | Source and selection labels | +| `MainViewModel.CommandState.cs` | Shared command refresh | +| `MainViewModel.CutoutWorkflow.cs` | Active-outline handoff, commit, delete, order, and nudge | +| `MainViewModel.SavedShapeLibrary.cs` | Saved-shape state, selection, and commands | +| `MainViewModel.SavedShapeManagement.cs` | Save, rename, and delete reusable shapes | +| `MainViewModel.SavedShapePlacement.cs` | Exact-size saved-shape placement | +| `MainViewModel.SelectionMovement.cs` | Drag, duplicate, placement, and shared boundary clamping | +| `MainViewModel.ShapeTool.cs` | Combined built-in and saved-shape selector | +| `MainViewModel.BrushRefinement.cs` | Edge-refinement stroke state and results | +| `MainViewModel.Export.cs` | Export workflow invocation | +| `MainViewModel.ExportSettings.cs` | Destination, naming, scale, and outline choices | +| `MainViewModel.ExportPresets.cs` | Preset application and reconciliation | +| `MainViewModel.Preview.cs` | Inspector and gallery preview refresh | +| `MainViewModel.ProjectLifecycle.cs` | Image and workspace lifecycle | +| `MainViewModel.ProjectTracking.cs` | Collection and model change subscriptions | +| `MainViewModel.History.cs` | Undo and redo snapshots | +| `MainViewModel.CutoutNaming.cs` | Committed cut-out rename workflow | +| `MainViewModel.Layout.cs` | Panel sizing and appearance preferences | + +`ViewModels/Options/ShapeLibraryOption.cs` is the presentation option used by +the Shape Cut selector. Built-in and user-saved shapes share this option type, +while only user-saved shapes expose deletion. + +### `Workflows/Exporting` + +`ExportBatchCoordinator` owns ordered batch execution, selected-only filtering, +invalid-item skipping, safe numbering, and the final result summary. It calls +`ExportService` for each valid item. + +Export presets and editable settings remain in focused view-model partials; the +coordinator is the extracted execution boundary, not a claim that the whole +export feature has moved out of the view model. + +### `Services` + +Services own reusable processing and persistence rather than WPF layout: + +- `BrushGeometryService` performs edge-refinement geometry operations. +- `CutoutRenderService` is the shared preview/export renderer. +- `ExportService` writes uniquely named PNG files. +- `ExportPresetCatalog` and `ExportOptionsPolicy` define export choices and + normalisation rules. +- `ImageService` validates, decodes, orients, and limits imported images. +- `ProjectService` validates, repairs, loads, and saves workspace JSON. +- `SettingsService` stores local preferences. +- `AtomicFile` provides replace-safe settings and workspace writes. + +### `Presentation/Preview` + +Preview-specific WPF work is separate from processing services: + +- `CutoutPreviewFactory` asks `CutoutRenderService` for pixels and converts the + result into frozen WPF bitmap sources. +- `CutoutPreviewItem` is the display item used by preview surfaces. + +This keeps file export free from WPF bitmap conversion while retaining the +single shared pixel-rendering route. + +### `Views/Components` + +`MainWindow.xaml` is now a layout shell. Its main surfaces are focused controls: + +| Component | Responsibility | +| --- | --- | +| `EditorHeader` | Open/save, cut tools, undo/redo, zoom, focus, and view settings | +| `CutoutLibraryPanel` | Cut-out list, ordering, naming, reusable-shape management | +| `InspectorPanel` | Source details, active outline, selected preview, edge refinement | +| `ExportBar` | Destination, export options, and selected/all export actions | + +The shell still owns pane splitters and the central `SkiaCanvasView`. Component +events route window-only actions, such as file dialogs and viewport buttons, +back to the existing window coordinator. + +The main window code-behind remains split into: + +| File | Responsibility | +| --- | --- | +| `MainWindow.xaml.cs` | Construction, shared fields, and view-model wiring | +| `MainWindow.Appearance.cs` | Theme application, pane sizing, and gallery refresh | +| `MainWindow.SelectionActions.cs` | Selection sync, commit, delete, and nudge | +| `MainWindow.Keyboard.cs` | Shortcuts and Click Around Escape handling | +| `MainWindow.Viewport.cs` | Fit, native pixels, and zoom actions | +| `MainWindow.ImageOpening.cs` | Open, drag/drop, multi-image choice, and source relocation | +| `MainWindow.WorkspaceSaving.cs` | Workspace saving, destination choice, prompts, and shutdown | + +`Views/Canvas/SkiaCanvasView` is also a small partial adapter: its root file +owns the public WPF surface, while `Input`, `Rendering`, and +`ProjectObservation` files own their respective bridge duties. + +### `Themes` + +- `DarkTheme.xaml` and `LightTheme.xaml` contain matching named colour and brush + resources for the editor and gallery. +- `EditorButtonStyles.xaml`, `EditorInputStyles.xaml`, + `EditorCollectionStyles.xaml`, and `EditorScrollStyles.xaml` own reusable + control templates and geometry. +- `ThemeResourceManager` swaps only the active palette dictionary. +- Views use dynamic theme resources so light and dark appearances expose the + same controls and behaviour. + +## Saved Shapes + +Built-in shapes do not need one code file per shape. Add one through +`ShapePresetCatalog` and `ShapeGeometryFactory`, producing a +`PathGeometryModel`. + +User-saved shapes do not generate code or script files. Each is a +`SavedShapeModel` stored in the workspace. It appears under **My Saved Shapes** +in the Shape Cut selector, keeps its exact saved dimensions, and can be renamed +or deleted independently. + +## Dependency Direction + +The current intended direction is: + +```text +Views / Views.Components / Views.Canvas + | + v +ViewModels -----> Workflows -----> Services + | | | + +-----------------+---------------+ + v + Core.Geometry + Models + +Presentation.Preview -----> CutoutRenderService + Models +CanvasController ---------> ICanvasEditorContext + Core.Geometry + Models +``` + +Avoid adding a direct `CanvasController` dependency on `MainViewModel`, moving +shared geometry back into the canvas folder, or adding WPF preview conversion to +export services. + +## Regression Checks + +`solution/ImageUiSlicer.Tests` is a package-free executable check suite for +restricted development machines and CI. It covers source-resolution loading, +native-size Fit, geometry and image edges, transparent rendering, +inspector/export agreement, adaptive shapes, brush behaviour, selection state, +saved-shape exact sizing, renaming and undo, multi-image drop choice, workspace +compatibility, order-preserving export, movement clamping, plain-English tool +labels, five lens levels, theme resource parity, WPF control loading, compact +ordering controls, light-theme text contrast, protected preview pane bounds, +visible action wording, and the canvas contract boundary. + +## Remaining Staged Boundaries + +The rebuild is deliberately incremental. These boundaries still exist and +should be improved only with matching behaviour checks: + +- `MainViewModel` still coordinates several workflows even though it is split + into focused partial files. Export execution is extracted; other workflow + coordinators remain candidates for later extraction. +- `SkiaCanvasView` is now split by bridge responsibility, but it still observes + `MainViewModel` and project changes directly. The controller itself is + contract-based; the WPF adapter is not yet generic. +- `CutoutModel.PreviewImage` is a transient, `[JsonIgnore]` WPF bitmap cache on a + persistence model. Preview creation has moved to `Presentation/Preview`, but + moving this cache needs a deliberate binding migration. +- Main-window code-behind still owns WPF dialogs, unsaved-change prompts, + keyboard routing, and pane coordination. These are WPF shell duties for now, + not processing or geometry duties. + +These items are not dead features. Do not remove them merely to reduce file +size; preserve the behaviour contract and migrate one responsibility at a time. + +## Adding Features Safely + +- Add a built-in shape through `Core/Geometry/ShapePresetCatalog` and + `ShapeGeometryFactory`. +- Add a canvas tool in a focused controller partial, then hand finished + geometry to `SetActiveSelection`. +- Add reusable processing as a service or workflow that accepts models and + returns a result without owning WPF controls. +- Keep one commit path and the shared preview/export renderer. +- Add or update a regression check before moving an established boundary. diff --git a/docs/BEHAVIOR_CONTRACT.md b/docs/BEHAVIOR_CONTRACT.md new file mode 100644 index 0000000..dc617fe --- /dev/null +++ b/docs/BEHAVIOR_CONTRACT.md @@ -0,0 +1,140 @@ +# Photo Cutter Behaviour Contract + +This document freezes the product intent while the internal code is rebuilt. +Refactoring may change file ownership, dependency direction, and implementation +details. It must not silently change these user-visible outcomes. + +## Product Intent + +Photo Cutter is a focused Windows editor for turning parts of a source image +into accurate transparent PNG cut-outs. + +The core journey is: + +1. Open one source image or a saved workspace. +2. create an outline with Draw Around, Click Around, or Shape Cut; +3. position or refine that outline against the original image; +4. save it as a committed cut-out; +5. optionally save its exact geometry as a reusable shape; +6. review, name, order, and export cut-outs as transparent PNG files. + +Photo Cutter is not a photo enhancer. Zoom, smoothing, the precision magnifier, +themes, and previews may change how the image is displayed, but they must never +alter source pixels or saved geometry. + +## Non-negotiable Data Rules + +- Source images are decoded once at their oriented pixel dimensions. +- All geometry is stored in original source-image pixel coordinates. +- Display scale and pan are never written into cut-out or saved-shape geometry. +- A committed cut-out owns an independent copy of its geometry and export + options. +- A reusable shape preserves its exact saved width, height, and proportions. +- Placing a reusable shape translates it; placement does not silently resize it. +- Built-in shapes are protected application definitions. User-saved shapes are + workspace data and can be renamed or deleted. +- Preview and export use the same crop, mask, padding, scaling, and outline + rules. +- Original-size export preserves the source pixels inside the cut-out mask. +- Transparent areas remain transparent. +- Invalid or fully off-image geometry is rejected with a clear result instead + of producing a misleading 1 × 1 or blank export. + +## Selection and Editing Rules + +- Only one unfinished active outline exists at a time. +- An active outline and committed cut-out selection are mutually exclusive. +- Select & Move changes the position of an active outline or selected committed + cut-outs without changing their size. +- Draw Around creates a closed freehand outline from a held pointer gesture. +- Click Around creates a point-by-point outline and can be finalised explicitly. +- Shape Cut creates either a built-in shape or an exact-size saved shape. +- Add to Edge and Remove from Edge refine one target: the active outline or one + selected committed cut-out. +- Multiple selected cut-outs are never merged into one brush target. +- Discard Selection removes only the unfinished outline. +- Save Cut-out is the single normal route from an unfinished outline to a + committed cut-out. +- Delete removes selected committed cut-outs, not the source image. +- The up and down ordering controls change list and export order only; they do + not move geometry on the image. +- Undo and redo cover geometry edits, placement, naming, ordering, reusable + shape changes, and export-setting changes that affect the workspace. + +## Reusable Shape Rules + +- A shape can be saved from the current active outline or one selected cut-out. +- Saving creates a user-owned shape with its own geometry copy. +- A saved shape appears under My Saved Shapes in the Shape Cut selector. +- Selecting and placing it creates an unfinished exact-size outline at the + chosen image position. +- The placed outline can be moved, refined, and saved as a new cut-out. +- Renaming updates the shape library and selector immediately. +- Deleting removes only the selected user-saved shape. +- Right-click deletion is available only for user-saved shapes. +- Reusable shapes stay in the workspace when its source image is replaced. + +## Naming and Ordering Rules + +- Committed cut-outs can be explicitly renamed. +- Saved shapes can be explicitly renamed. +- Blank or conflicting names are repaired to a usable unique name. +- Cut-out names are used by the gallery, workspace, and named-export mode. +- Ordering controls are enabled only when the selected cut-out can move in the + requested direction. +- Export All follows the visible committed cut-out order. + +## View Rules + +- Fit shows the whole image and does not enlarge a small image beyond native + 100% scale. +- 100% shows one source pixel as one display pixel. +- Zoom changes only the view and is centred predictably. +- Smooth enlarged preview affects display filtering only. +- Exact-pixel viewing remains available for inspecting source pixels. +- The precision magnifier follows the pointer, supports the five configured + levels, and shows the active cutting line as well as source pixels. +- Cutting-line colour is a view preference and does not alter exported PNGs. +- Focus Preview hides side panels without changing the project. +- Dark and light appearances expose the same controls and behaviour. + +## Workspace Rules + +- Opening an image starts a new image project while retaining the reusable shape + library as designed. +- Opening a workspace restores its image reference, cut-outs, saved shapes, + ordering, display options, and export options. +- A missing or invalid source image prevents an unusable workspace from being + applied. +- Cancelling an Open dialog leaves the current work untouched. +- Replacing or closing dirty work asks the user to save, discard, or cancel. +- An unfinished outline is included in the unsaved-work decision. +- Workspace and settings writes use replace-safe files. +- Supported older workspace fields may remain for backward compatibility even + when they are no longer displayed. +- Corrupt optional entries are contained or repaired without allowing extreme + geometry into rendering and history. + +## Export Rules + +- Export Selected processes the selected committed cut-outs. +- Export All processes every valid committed cut-out in visible order. +- The chosen export folder is used directly. +- Original and fixed-size presets share the same rendering pipeline. +- Naming mode supports automatic numbering and cut-out names. +- Invalid Windows filename characters are removed safely. +- Existing files receive a unique name instead of being overwritten silently. +- One invalid cut-out does not prevent other valid cut-outs from exporting. +- Export reports completed, skipped, and failed items honestly. + +## Refactoring Gate + +A structural change is acceptable only when: + +1. the solution builds without warnings or errors; +2. the relevant behaviour checks pass; +3. XAML handlers and bindings still resolve; +4. workspace compatibility is preserved; +5. preview and export still share one render path; and +6. any intentional visible change fixes a documented defect or improves wording + without changing the product intent. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0572856..8af7712 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,13 +6,77 @@ The format is based on Keep a Changelog and this project follows Semantic Versio ## [Unreleased] +## [1.2.0] - 2026-07-17 + ### Added - Professional GitHub workflows for CI and release publishing - Public project documentation and community templates +- Replace-safe project and settings persistence to prevent partial JSON files +- Added a product-intent behaviour contract and structural regression gates so internal reorganisation cannot silently change exact-size shapes, source-pixel geometry, commit, preview, or export behaviour ### Changed -- Release pipeline now publishes a single executable asset: `PhotoCutter.exe` +- Release pipeline now tests the complete solution before publishing `PhotoCutter.exe` and its SHA-256 checksum - Repository structure cleaned for a public app-facing layout +- Cutout geometry validation, fractional bounds, export size limits, and Windows filename handling are hardened +- Malformed project geometry, names, and duplicate IDs are repaired during load +- Renamed the user-facing Lasso and Polygon tools to the clearer `Draw Around` and `Click Around` labels +- Added a cursor-following precision magnifier with five remembered levels (5x, 6x, 7x, 8x, and 10x), pixel grid, and crosshair +- Simplified the first-use workflow with plain-English editing labels, visible Undo/Redo, an unobstructed canvas, and collapsed advanced export options +- Unified image and saved-workspace opening, added original-resolution export, allowed true fit-to-view for very large images, and capped preview-only memory use +- Standardized button, text-field, and dropdown geometry and fixed clipped selected values in narrow dropdowns +- Split the left pane into Cut-outs and Reusable Shapes tabs, with outcome-based copy and shape-library actions +- Reworked the left panel tabs into a full-width segmented switch with equal options and a clear content separator +- Removed duplicated side-panel spacing and added dedicated scrollbar gutters so content no longer overlaps panel edges +- Added remembered cutting-line colours, magnified line rendering, and a light appearance mode for the interface and canvas +- Changed Fit so small images remain at native 100% instead of being automatically enlarged to fill the canvas +- Rebuilt the command bar into clear cutting and view controls with zoom out, zoom percentage, zoom in, Fit, 100%, Focus Preview, and a themed View Settings panel +- Added smooth enlarged previews with a switchable exact-pixel mode; 100% remains pixel-aligned and unfiltered +- Removed obsolete canvas guidance, stats overlays, duplicate actions, clipped preview status, unreachable automatic/AI detection branches, and stale settings +- Fixed canvas hit-testing outside the image, reciprocal zoom steps, persistent Fit behaviour, pixel-aligned magnifier grids, and no-op undo entries +- Changed exports to use the folder selected by the user directly and enabled high-quality resampling only for resized exports +- Added image-size preflight, single-pass decoding, EXIF orientation handling, and a clear 64 MP memory-safety limit +- Corrected light appearance across native controls and the Cut-out Gallery +- Added unfinished-cut-out protection before opening another image or closing the app +- Rebuilt reusable shapes around exact-size copies with rename, place, move, and save-as-new-cut-out actions, and retained the shape library when switching images +- Added an explicit, undoable rename action for committed cut-outs with unique-name protection and immediate list, gallery, workspace, and named-export updates +- Unified built-in and user-saved shapes in the Shape Cut selector; saved shapes place at their exact size and alone provide right-click deletion +- Removed the duplicated saved-shape placement panel and stale paste path, shortened crowded labels, and hardened collection subscriptions during undo and project changes +- Split the canvas controller and main editor view model into focused responsibility files while preserving one shared selection, commit, preview, and export pipeline +- Centralized built-in shape metadata, export presets, export defaults, and brush geometry so labels and output behavior no longer drift across UI and processing code +- Revalidated and recalculated selection geometry during commit, corrected shape/refinement mode labels, and protected workspace saves from silently excluding unfinished outlines +- Corrected the edge-refinement boolean operation so Add to Edge and Remove from Edge use the selected outline and brush as inputs instead of an empty result path +- Added an architecture guide covering file ownership, exact-size saved shapes, and the shared-output rules for future tools +- Reduced header and command-bar height, padding, label width, button spacing, dropdown chrome, and branding scale for a cleaner compact layout +- Rebuilt Saved Shapes management around directly selectable cards, an honest empty state, visible source readiness, and explicit rename/delete actions +- Allowed exact-size shapes to be saved from an active outline or one selected cut-out, and added undo/redo for saved-shape creation, renaming, and deletion +- Fixed full-image cuts losing the last pixel row/column and rejected off-image outlines instead of exporting transparent 1 × 1 files +- Fixed split preview transparency and aligned it to a clear left-before/right-after comparison +- Made high-resolution circles, rounded corners, and brush contours adaptive instead of using visibly coarse fixed point counts +- Fixed brush targeting, active-outline history, saved-shape selector synchronization, export-default history, and resilient multi-file export +- Repaired custom scrollbar geometry, editor shortcut focus, source-resolution display, gallery refresh, and real-confidence visibility +- Added automated geometry, rendering, brush, selection, and saved-shape regression checks to CI +- Split the main window coordinator into appearance, editor-command, and workspace files and delayed unsaved-change prompts until a valid replacement file is ready +- Rebuilt the main editor XAML as a small layout shell with focused header, cut-out library, inspector, and export-bar components +- Moved shared shape, validation, crop, translation, and movement rules into `Core/Geometry`, and moved shape-selector options and WPF preview types into their presentation-facing folders +- Added a canvas editor contract so `CanvasController` no longer depends directly on `MainViewModel` +- Extracted ordered selected/all export execution into `ExportBatchCoordinator` while retaining the existing render and file-writing pipeline +- Split dark/light palettes from reusable button, input, collection, and scrollbar templates, with parity checks for both appearances +- Replaced the wide cut-out order labels with compact up/down arrows while retaining explicit tooltips and the same list/export-order commands +- Added a clear chooser when several images are dropped, with Cancel leaving the current workspace untouched +- Locked original source dimensions and native-size Fit behavior with regression checks so display zoom cannot alter image data +- Split canvas input/rendering, the WPF canvas adapter, window opening/saving/keyboard actions, and view-model state into focused files without changing command routes +- Replaced the legacy Windows Forms folder picker with the .NET 8 WPF folder dialog and removed the unused Windows Forms dependency +- Removed unused icon PNG exports and the obsolete Image UI Slicer wordmarks while retaining the packaged application icon +- Removed global WPF type aliases so view and canvas files declare their UI dependencies directly +- Darkened light-appearance accent text to readable contrast, constrained side-pane splitters so the preview cannot collapse, and aligned saved-shape/copy/refinement wording with the visible controls +- Flattened the oversized header into a compact title row and shared Cut Tools/View row, with shorter slightly wider tool buttons and grouped wrapping that returns substantially more height to large-photo previews +- Compacted both side panels and reset the previous oversized saved layout once; resize handles now keep Cut-outs at 208–260 px, Inspector at 220–280 px, and preserve at least 400 px for the photo preview +- Matched side-panel tabs, actions, fields, ordering arrows, cards, and Inspector controls to the compact 28 px toolbar scale, with shorter wrapping help text that remains readable at the minimum pane widths + +## [1.1.0] - 2026-03-11 + +### Changed +- Reorganized the public repository, documentation, and automated Windows release flow ## [1.0.1] - 2026-03-11 diff --git a/docs/RELEASE.md b/docs/RELEASE.md index e492d26..5ddad10 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -5,25 +5,21 @@ This repository publishes Photo Cutter through GitHub Releases. ## Automated release flow (recommended) 1. Ensure `main` contains your final release commit. -2. Create and push a semantic version tag: +2. Create an annotated semantic version tag and push only that tag: ```powershell - git tag v1.0.2 - git push origin v1.0.2 + git tag -a v1.2.0 -m "Photo Cutter v1.2.0" + git push origin v1.2.0 ``` 3. GitHub Actions workflow `Release Windows App` will: - - Restore and build the project + - Restore and build the complete solution + - Run every regression check - Publish a single-file Windows executable - Create/update a GitHub Release - - Upload one asset: `PhotoCutter.exe` + - Upload `PhotoCutter.exe` and its SHA-256 checksum -## Manual release flow - -1. Open GitHub Actions -> `Release Windows App`. -2. Click `Run workflow`. -3. Enter a valid version tag (example `v1.0.2`). -4. Run workflow and confirm release asset upload. +The release workflow is tag-only so the published executable always comes from the tagged commit. ## Local publish command diff --git a/docs/archive/AUDIT_REPORT.md b/docs/archive/AUDIT_REPORT.md deleted file mode 100644 index ddbc0f0..0000000 --- a/docs/archive/AUDIT_REPORT.md +++ /dev/null @@ -1,287 +0,0 @@ -# Photo Cutter Audit Report - -Date: 2026-03-10 -Workspace: C:\Users\Marcus\Desktop\Marcus APPs\windows\Photo-Cutter -Primary application: solution/ImageUiSlicer - -## Scope - -This audit reviewed the maintained source tree under `solution/ImageUiSlicer`, the repository-level documentation and assets, and the generated build trees committed under `solution/ImageUiSlicer/bin` and `solution/ImageUiSlicer/obj`. - -Maintained code reviewed: -- App startup: `App.xaml`, `App.xaml.cs`, `GlobalUsings.cs`, `ImageUiSlicer.csproj` -- Infrastructure: `ObservableObject.cs`, `RelayCommand.cs` -- Models: `BBox.cs`, `PointF.cs`, `SourceImageModel.cs`, `SelectionModel.cs`, `ProjectModel.cs`, `CutoutPreviewItem.cs`, `PathGeometryModel.cs`, `ExportOptionsModel.cs`, `CutoutModel.cs` -- Canvas engine: `CanvasTool.cs`, `GeometryHelper.cs`, `CanvasController.cs` -- Services: `ImageService.cs`, `ProjectService.cs`, `SettingsService.cs`, `ExportService.cs`, `CutoutPreviewFactory.cs`, `AutoCutoutService.cs`, `AutoCutoutService.Ai.cs`, `AiCutoutDetectionService.cs`, `ShapeReuseDetectionService.cs` -- View models: `MainViewModel.cs`, `MainViewModel.ExportAndBrush.cs`, `MainViewModel.Ai.cs`, `MainViewModel.ShapeReuse.cs` -- Views: `MainWindow.xaml`, `MainWindow.xaml.cs`, `CutoutGalleryWindow.xaml`, `CutoutGalleryWindow.xaml.cs`, `Views/Canvas/SkiaCanvasView.xaml`, `Views/Canvas/SkiaCanvasView.xaml.cs` -- Documentation/assets: root spec `.txt` files, `specs/`, `brand/`, `png/`, `README_Scaffold.txt` -- Generated artifacts: `solution/ImageUiSlicer/bin`, `solution/ImageUiSlicer/obj` - -## Architecture Summary - -Current architecture is workable but not cleanly modular: -- UI layer: WPF XAML views plus code-behind for dialogs, window lifecycle, and keyboard routing. -- Application layer: one very large `MainViewModel` split across partial files, but still owning project state, command state, export policy, preview generation, undo/redo, brush refinement, auto detect, AI detect, and shape reuse. -- Domain/data layer: simple observable models with JSON persistence. -- Services layer: image IO, project IO, settings IO, cutout export, preview generation, heuristic auto-detect, AI-assisted detect, and shape reuse matching. -- Rendering/editor engine: `CanvasController` + `GeometryHelper` + Skia canvas view. - -There are no hard compile-time circular dependencies between namespaces, but there are several soft architectural leaks: -- `MainViewModel` directly instantiates all services instead of depending on abstractions or a composition root. -- `MainViewModel` exposes a view-specific `SelectionSyncRequested` event for listbox synchronization. -- `CanvasController` is tightly coupled to WPF input types and directly depends on `MainViewModel`. -- Export preview rendering is split between `ExportService` and `CutoutPreviewFactory` with duplicated outline policy logic. - -## Findings - -### High Risk - -1. God-object view model mixes orchestration, editor state, persistence rules, export rules, preview generation, undo/redo, AI, and shape reuse. -- Risk: High maintainability risk and fragile feature interactions. -- Files: - - `solution/ImageUiSlicer/ViewModels/MainViewModel.cs` - - `solution/ImageUiSlicer/ViewModels/MainViewModel.ExportAndBrush.cs` - - `solution/ImageUiSlicer/ViewModels/MainViewModel.Ai.cs` - - `solution/ImageUiSlicer/ViewModels/MainViewModel.ShapeReuse.cs` -- Evidence: - - Service construction and command wiring live in one class. - - The same object handles project loading/saving, canvas/editor state, export policy, preview generation, brush refinement, auto detect, AI detect, and smart shape reuse. -- Recommendation: - - Split into a small shell view model plus focused coordinators/services such as `ProjectSessionController`, `SelectionEditorService`, `ExportProfileService`, `CutoutDetectionCoordinator`, `PreviewService`, and `UndoHistoryService`. - - Keep partial files only for presentation groupings, not as the main modularization mechanism. - -2. Generated build output is committed to the repository and there was no `.gitignore`. -- Risk: High repository instability and noisy diffs; stale artifacts can hide the real source of a regression. -- Files/directories: - - `solution/ImageUiSlicer/bin` - - `solution/ImageUiSlicer/obj` - - repository root had no `.gitignore` -- Evidence: - - `bin` + `obj` contain 200+ generated files and third-party binaries. - - Multiple transient `*_wpftmp*` files are present under `obj`. -- Action taken: - - Added `.gitignore` to block future `bin/` and `obj/` churn. -- Recommendation: - - In a dedicated cleanup change, remove tracked generated artifacts from source control and let CI/build recreate them. - -### Medium Risk - -3. View-model-to-view selection synchronization leaks UI responsibilities into the application layer. -- Risk: Medium architectural coupling; harder to test and replace the list view behavior. -- Files: - - `solution/ImageUiSlicer/ViewModels/MainViewModel.cs` - - `solution/ImageUiSlicer/Views/MainWindow.xaml.cs` -- Evidence: - - `SelectionSyncRequested` exists only to tell the window to imperatively sync `ListBox.SelectedItems`. -- Recommendation: - - Introduce a dedicated selection model / adapter owned by the view layer, or use a bindable selected-items behavior so the view model stays UI-agnostic. - -4. Canvas editing logic is heavily coupled to the full `MainViewModel` and WPF input types. -- Risk: Medium; editor bugs become harder to isolate, test, or port. -- Files: - - `solution/ImageUiSlicer/CanvasEngine/CanvasController.cs` - - `solution/ImageUiSlicer/Views/Canvas/SkiaCanvasView.xaml.cs` -- Evidence: - - `CanvasController` consumes `Point`, `Key`, `ModifierKeys`, and directly mutates `MainViewModel`. -- Recommendation: - - Extract an `ICanvasEditorSession` or similar interface that exposes only the editor operations/state required by the controller. - -5. AI detection is wired directly as a raw HTTP integration inside app code with no abstraction seam or offline fallback contract. -- Risk: Medium; hard to mock, test, or swap providers. Failure modes are mostly runtime-only. -- File: - - `solution/ImageUiSlicer/Services/AiCutoutDetectionService.cs` -- Evidence: - - Static `HttpClient`, raw JSON body construction, manual response parsing, environment variable resolution, and OpenAI-specific prompt/schema all live in one concrete class. -- Recommendation: - - Introduce an `ICutoutDetectionProvider` abstraction with `Heuristic`, `ShapeReuse`, and `OpenAiVision` implementations. - - Keep provider-specific HTTP and schema parsing behind that boundary. - -6. Preview rendering responsibilities are duplicated across export and preview services. -- Risk: Medium maintenance drift. -- Files: - - `solution/ImageUiSlicer/Services/ExportService.cs` - - `solution/ImageUiSlicer/Services/CutoutPreviewFactory.cs` -- Evidence: - - Both services independently resolve outline color policy and render clipped geometry. -- Recommendation: - - Centralize cutout rasterization in one renderer service and have preview/export call it with different render options. - -### Low Risk - -7. `ProjectService.Load` uses defensive null-coalescing against non-nullable model properties, which signals a schema/model mismatch. -- Risk: Low, but it hides the boundary between trusted model defaults and defensive deserialization. -- File: - - `solution/ImageUiSlicer/Services/ProjectService.cs` -- Recommendation: - - Either mark persistence properties nullable and normalize after load, or keep them non-nullable and validate with explicit load normalization methods. - -8. `SettingsService` swallows all settings-load failures silently. -- Risk: Low operational visibility; corrupt settings silently reset with no diagnostics. -- File: - - `solution/ImageUiSlicer/Services/SettingsService.cs` -- Recommendation: - - Log or surface a one-time warning when settings are unreadable. - -9. `RelayCommand` is too minimal for the current feature set. -- Risk: Low now, but it encourages more `async void` command handlers and prevents command parameter/cancellation patterns. -- File: - - `solution/ImageUiSlicer/Infrastructure/RelayCommand.cs` -- Recommendation: - - Replace with typed sync/async commands or adopt a small MVVM toolkit command abstraction. - -10. Repository documentation is duplicated and one scaffold file is stale. -- Risk: Low, but guaranteed documentation drift over time. -- Files: - - root `*.txt` spec files - - matching files under `specs/` - - `solution/ImageUiSlicer/README_Scaffold.txt` -- Evidence: - - Root `.txt` files and `specs/` contain exact duplicate name/length pairs. - - `README_Scaffold.txt` still describes the project as a scaffold and contains encoding corruption. -- Recommendation: - - Keep one canonical spec location (`specs/`) and archive or remove the duplicates. - - Replace the scaffold README with current product/operator docs or delete it. - -## Dead Code / Redundant Code Review - -### Removed safely - -1. No-op assignment removed. -- File: - - `solution/ImageUiSlicer/ViewModels/MainViewModel.cs` -- Removed line: - - `project.SourceImage.Path = project.SourceImage.Path;` -- Reason: - - It had no effect and did not contribute to project loading behavior. - -### Consolidated / guarded - -2. Repository ignore rules added. -- File: - - `.gitignore` -- Reason: - - Prevents future accidental inclusion of `bin/` and `obj/` output. - -### Flagged for manual review instead of deleting - -3. `ExportOptionsModel.Format` -- File: - - `solution/ImageUiSlicer/Models/ExportOptionsModel.cs` -- Why flagged: - - Confirmed unused by runtime code today, but it is part of the serialized project contract. - - Removing it would subtly change saved project shape. -- Recommended action: - - Either remove in a schema-versioned cleanup or mark deprecated in code comments and docs. - -4. `CutoutModel.Notes` -- File: - - `solution/ImageUiSlicer/Models/CutoutModel.cs` -- Why flagged: - - No runtime UI or service uses it, but it is persisted data and may be intended for future metadata. -- Recommended action: - - Remove only after deciding whether notes belong in the project format. - -5. Root duplicate spec files. -- Files: - - root `00_...13_...txt`, `MASTER_LOCK_SPEC_...txt` -- Why flagged: - - Exact duplicates of `specs/`, but documentation location is a repo policy decision rather than dead runtime code. -- Recommended action: - - Keep `specs/` as canonical and delete the root copies in a docs-only cleanup. - -6. `README_Scaffold.txt` -- File: - - `solution/ImageUiSlicer/README_Scaffold.txt` -- Why flagged: - - Stale and misleading, but documentation removal should be deliberate. - -## Data Flow Summary - -1. Image/project ingress -- `MainWindow.xaml.cs` opens image/project dialogs or handles drag-drop. -- `MainViewModel` uses `ImageService` and `ProjectService` to load state. -- `ApplyProject` replaces the active session, resets canvas state, refreshes previews, and raises command/property changes. - -2. Editing flow -- `SkiaCanvasView` translates WPF mouse events into canvas pixel coordinates. -- `CanvasController` interprets input for selection, lasso, polygon, brush refine, panning, and active-selection moves. -- `CanvasController` mutates editor state through `MainViewModel` methods such as `SetActiveSelection`, `TryMoveActiveSelectionFromOrigin`, `BeginBrushRefineStroke`, and `CommitSelectionCommand`. - -3. Detection flow -- Quick detect: `MainViewModel.AutoDetectCutouts` -> `AutoCutoutService.Detect` -> cutout suggestions -> project cutouts. -- AI detect: `MainViewModel.AiDetectCutouts` -> `AiCutoutDetectionService.DetectAsync` -> OpenAI response -> `AutoCutoutService.CreateSuggestionFromHint` -> project cutouts. -- Shape reuse: `MainViewModel.MatchSelectedCutout` -> `ShapeReuseDetectionService.FindMatches` -> project cutouts. - -4. Preview/export flow -- Committed cutout previews and inspector previews are built through `CutoutPreviewFactory`. -- Exports go through `ExportService`, which always writes PNG files into `\cut outs`. - -5. Persistence/settings flow -- Project save/load uses JSON via `ProjectService`. -- App settings load/save use `SettingsService` under `%APPDATA%\ImageUiSlicer`. - -## Dependency Structure Review - -Healthy boundaries: -- Models are mostly dependency-light. -- Geometry helper is reusable across services and canvas/editor code. -- Services generally depend on models plus SkiaSharp. - -Fragile boundaries: -- `MainViewModel` is a central dependency sink for nearly every major feature. -- `CanvasController` depends on the concrete view model rather than an editor interface. -- `AiCutoutDetectionService` depends on OpenAI request/response details directly, not behind a provider contract. -- `CutoutPreviewFactory` partially duplicates rasterization behavior instead of delegating to one renderer. - -## Recommended Rebuild Plan - -### Phase 1: Stabilize boundaries without changing UX -- Introduce interfaces for editor session, preview rendering, cutout detection, and project/session persistence. -- Move service construction into a small composition root at app startup. -- Replace `SelectionSyncRequested` with a view-layer selected-items adapter. - -### Phase 2: Split the main view model -- Keep `MainShellViewModel` for top-level window state. -- Move project session state into `ProjectSessionController`. -- Move selection/editor logic into `SelectionEditorService`. -- Move export preset/naming logic into `ExportProfileService`. -- Move undo/redo into `UndoHistoryService`. -- Move detect orchestration into `DetectionCoordinator`. - -### Phase 3: Unify rendering -- Create a shared `CutoutRenderService` that owns: - - outline policy - - crop bounds - - transparent render output - - preview render variants -- Have gallery preview, inspector preview, and export all depend on that single renderer. - -### Phase 4: Formalize detection providers -- `ICutoutDetectionProvider` - - `HeuristicCutoutDetectionProvider` - - `OpenAiCutoutDetectionProvider` - - `ShapeReuseDetectionProvider` -- Add provider health/state objects so UI can show readiness without embedding provider logic in the main view model. - -### Phase 5: Repository hygiene -- Remove tracked `bin/` and `obj/` artifacts. -- Keep only one documentation source of truth under `specs/`. -- Replace stale scaffold docs with current developer/operator docs. -- Add tests for geometry transforms, export rendering, project load/save normalization, and detection suggestion normalization. - -## Open Questions - -1. Is per-cutout export customization part of the intended product, or should export options remain project-wide only? -2. Should AI detection remain cloud-only, or do you want an offline/local provider contract next? -3. Are root spec files intentionally duplicated for convenience, or can `specs/` become the only canonical location? -4. Do you want `CutoutModel.Notes` kept as hidden future metadata, or removed from the project schema? - -## Audit Outcome - -- Runtime/source audit completed across the maintained application code. -- Generated artifacts and duplicated docs were identified and categorized. -- One dead no-op statement was removed safely. -- Future build-artifact churn is now guarded by `.gitignore`. -- No automated tests exist yet, so residual risk remains around editor interactions, render edge cases, and persistence regressions. diff --git a/docs/archive/README_Scaffold.txt b/docs/archive/README_Scaffold.txt deleted file mode 100644 index 8dabfff..0000000 --- a/docs/archive/README_Scaffold.txt +++ /dev/null @@ -1,14 +0,0 @@ -Image UI Slicer — WPF + SkiaSharp Scaffold (v0.2 ABCD) - -Open ImageUiSlicer.sln in Visual Studio 2026. -This scaffold wires: -- Export folder: BaseExportFolder\cut outs\ -- Naming mode + Prefix UI -- Multi-select + reorder controls -- Nudge placeholder - -Next (Copilot): -1) Drag/drop routing per Spec 12 -2) CanvasController + lasso tools + commit selection->cutout -3) Real mask-based PNG export -4) Nudge engine hook per Spec 13 diff --git a/docs/releases/v1.2.0.md b/docs/releases/v1.2.0.md new file mode 100644 index 0000000..c6fbd28 --- /dev/null +++ b/docs/releases/v1.2.0.md @@ -0,0 +1,23 @@ +# Photo Cutter v1.2.0 + +Photo Cutter v1.2.0 is a major usability, precision, and reliability update. It introduces a cleaner compact editor and stronger high-resolution cutting while preserving the familiar cut-out and workspace workflow. + +## Highlights + +- Cleaner compact header, side panels, resizable layout, and first-use guidance. +- Plain-English **Draw Around** and **Click Around** tools. +- Exact-size reusable shapes that can be named, placed, moved, committed, renamed, and deleted. +- Rename and reorder committed cut-outs before export. +- Five-level 5x–10x precision magnifier with the cutting line visible inside it. +- Native-resolution Fit behaviour, smooth enlarged previews, and switchable exact-pixel viewing. +- Remembered cutting-line colours and consistent dark and light appearances. +- High-resolution geometry, edge-refinement, preview, gallery, workspace, and export fixes. +- Reorganized editor components plus expanded automated regression coverage. + +## Download + +Download `PhotoCutter.exe` from the Assets section below. It is a portable, self-contained Windows 10/11 x64 app, so a separate .NET installation is not required. + +The executable is currently unsigned, which means Windows SmartScreen may ask you to confirm the first launch. `PhotoCutter.exe.sha256` is included so you can verify the downloaded file. + +All image processing remains on your computer. No account or upload is required. diff --git a/docs/screenshots/photo-cutter-main.png b/docs/screenshots/photo-cutter-main.png new file mode 100644 index 0000000..b1fffa3 Binary files /dev/null and b/docs/screenshots/photo-cutter-main.png differ diff --git a/docs/specs/00_Locked_Decisions_Summary_Image_UI_Slicer_v0_2_ABCD.txt b/docs/specs/00_Locked_Decisions_Summary_Image_UI_Slicer_v0_2_ABCD.txt deleted file mode 100644 index 8986fba..0000000 --- a/docs/specs/00_Locked_Decisions_Summary_Image_UI_Slicer_v0_2_ABCD.txt +++ /dev/null @@ -1,126 +0,0 @@ -LOCKED DECISIONS SUMMARY (Baseline) — Image UI Slicer -Version: v0.1-baseline -Target: Visual Studio 2026 • Windows Desktop - -Last updated: 2026-03-03 (Europe/London) - -============================================================ -A) What this app is (Locked) -============================================================ -A Windows desktop tool that loads any image (UI mockups, screenshots, AI-generated UI art, etc.) -and lets the user cut out UI elements by drawing freehand selections (Photoshop-ish), -previewing them, committing them into a cutout list, and exporting them as separate image assets -(primarily PNG with transparency) for use in building programs. - -============================================================ -B) Tech + Platform (Locked) -============================================================ -- Windows Desktop app built in Visual Studio 2026 -- UI framework: WPF (.NET) -- Canvas/rendering: SkiaSharp (selected to support smooth freehand selection + masking) - -============================================================ -C) Core Workflow (Locked) -============================================================ -1) Load image into Canvas preview -2) Draw Active Selection using freehand lasso (Photoshop-ish) -3) See selection overlay preview BEFORE committing -4) Commit Selection → Cutout -5) Repeat to create as many cutouts as desired (Cutouts count shows N) -6) Export Selected or Export All cutouts as image files - -============================================================ -D) Selection + Cutouts Model (Locked) -============================================================ -Key terms: -- Active Selection (temporary): the current selection drawn on the image (previewable) -- Cutout (committed): a saved mask/path derived from a selection, stored in the Cutouts list - -Locked interaction goals: -- Freehand cutting (not rectangle-only slicing) -- Visual overlay preview on the canvas -- Multiple committed cutouts per project (shows how many exist) - -============================================================ -E) UI Layout (High-level Locked) -============================================================ -Single main window with 3 panels + bottom export bar: - -LEFT — Cutouts Panel -- Header shows “Cutouts (N)” -- List shows thumbnails + names -- Controls: Commit Selection → Cutout, Delete, (Duplicate optional later) - -CENTER — Canvas -- Renders source image -- Overlays: - - Active Selection (tinted fill + outline) - - Cutout outlines (toggle on/off) - - Selected cutout highlight - -RIGHT — Inspector -- Shows details for: - - Active Selection (bbox + preview thumbnail) - - Selected Cutout (name, bbox, export settings) - -BOTTOM — Export Bar -- Export Selected -- Export All -- Export folder picker + status - -============================================================ -F) Export Rules (Locked for v1) -============================================================ -- Primary export format: PNG with transparency -- Each cutout export: - - Crop to cutout bounding box - - Apply mask/clip from freehand path - - Save sanitized name, add suffix if collisions - -============================================================ -G) Data Representation (Partially Locked) -============================================================ -- Cutouts store geometry in IMAGE coordinates (not screen coordinates) -- Each cutout stores: - - id, name - - path points (x,y list), mode (freehand/polygon), closed=true - - cached bounding box (x,y,w,h) - - export settings (format=png, scale, padding) - -Not yet locked: -- Whether Active Selection is saved to project file as a draft - -============================================================ -H) Not Locked Yet (Open Decisions) -============================================================ -1) Project files (save/load): - - include slice/cutout geometry and source image reference -2) Commit behavior: - - clear selection after commit vs keep selection -3) Polygon lasso included in v1 or later -4) Add/Subtract selection modes included in v1 or later -5) Undo/Redo depth and scope -6) Export scales (1x/2x/custom) in v1 -7) Settings storage: - - AppData vs portable mode -8) Post-commit cutout editing: - - move/scale cutout masks, rename only, etc. - -============================================================ -I) Current Spec Files (References) -============================================================ -- 01_Master_Structure_Image_UI_Slicer_v0_1.txt -- 02_Freehand_Selection_and_Cutouts_WPF_SkiaSharp_v0_1.txt - -============================================================ -J) Next Spec File to Create (Recommended) -============================================================ -03_Data_Model_and_MVVM_Classes.txt -- ViewModels: Main, Canvas, Cutouts, Inspector -- Services: ImageService, ProjectService, ExportService, RecentFiles -- Commands + state machine (Active Selection vs Selected Cutout) -- SkiaSharp canvas integration boundary - - - -v0.2 LOCKS: A+B+C+D (multi-select+reorder, prefix naming, drag&drop, move/nudge) diff --git a/docs/specs/01_Master_Structure_Image_UI_Slicer_v0_1.txt b/docs/specs/01_Master_Structure_Image_UI_Slicer_v0_1.txt deleted file mode 100644 index 3f4f34e..0000000 --- a/docs/specs/01_Master_Structure_Image_UI_Slicer_v0_1.txt +++ /dev/null @@ -1,216 +0,0 @@ -IMAGE UI SLICER — Master Structure Spec (v0.1) -For Visual Studio 2026 • Windows Desktop App - -============================================================ -0) Purpose (what this app is) -============================================================ -A Windows desktop tool that lets you load any image (PNG/JPG/WebP/etc.), visually mark “segments/slices” on top of it in a preview panel, then export those segments as separate image assets (PNG with transparency, etc.) for use in building program UIs. - -Primary use-case: -- Take a full “AI-generated UI mockup” image and cut out buttons, panels, icons, corners, borders, and backgrounds into reusable assets. - -Non-goals (for v1): -- Not a full Photoshop replacement -- Not a full layout builder / UI designer -- Not a generative AI tool (we can add AI assist later, but v1 focuses on slicing/export) - -============================================================ -1) Platform + Tech (recommended default) -============================================================ -Recommended stack (default): -- WPF (.NET) + MVVM pattern -- Visual Studio 2026 solution -- Image decoding via built-in .NET (plus optional ImageSharp later if needed) -- Export formats: PNG (default), JPG (optional), WEBP (later) - -Storage (v1): -- Project file: JSON (stores image path + slice definitions) -- App settings: JSON in AppData -- Optional: “Portable mode” later - -============================================================ -2) Core User Workflow (v1) -============================================================ -A) Create / Open Project -1. File > New Project (or “New” on Home) -2. User selects a source image (UI mockup / screenshot / artwork) -3. The image loads in the Preview Panel - -B) Slice / Segment -User can create segments in these ways: -- Rectangle tool: click-drag to draw a rectangle -- Move/Resize: drag handles on a selected segment -- Duplicate segment -- Snap / Guides (basic) -- Zoom + Pan while editing - -C) Organize -- Each segment gets: - - Name (editable) - - Tags (optional) - - Export settings (format, padding, scale) - - Visibility toggle (optional) -- Segment list (left panel) for selecting/renaming/reordering - -D) Export -- Export selected segments or all -- Export folder picker -- Export naming rules: - - .png (default) - - auto numbering if duplicates -- Optional “export spritesheet” later (v2) - -============================================================ -3) UI Layout (single main window) -============================================================ -Main Window layout (3-column + bottom bar): - -LEFT: “Slices” panel -- Search / filter -- List of segments (name + small thumbnail + checkbox) -- Buttons: Add, Duplicate, Delete, Group (v2), Auto-Name (v2) - -CENTER: Preview Panel -- Image canvas with overlay -- Tools bar: - - Select - - Rectangle Slice - - Pan - - Zoom in/out - - Fit to screen / 100% -- Overlay: - - Bounding boxes for slices - - Resize handles - - Optional grid toggle - - Optional safe margin toggle - -RIGHT: “Inspector” panel -- Segment properties: - - Name - - X, Y, W, H - - Padding - - Export format (PNG/JPG) - - Transparency option (PNG only) - - Scale (1x, 2x, custom) -- Project properties: - - Source image path - - Default export folder - - Default naming rules - -BOTTOM: Action bar -- Open / Save project -- Export Selected -- Export All -- Status text (cursor position, zoom %, slice count) - -============================================================ -4) Segment Types (v1) -============================================================ -Supported slice shapes (v1): -- Rectangle only (fast + most useful for UI slicing) - -Planned (v2+): -- Rounded rectangle (auto mask) -- Polygon / freehand (lasso) -- “9-slice” guides (for scalable UI panels) -- Auto-detect UI elements (AI-assisted) - -============================================================ -5) Project File (JSON) — Draft Schema -============================================================ -{ - "version": "0.1", - "projectName": "My UI Slices", - "sourceImage": { - "path": "C:\\path\\to\\image.png", - "width": 0, - "height": 0 - }, - "defaults": { - "exportFolder": "C:\\exports", - "exportFormat": "png", - "padding": 0, - "scale": 1 - }, - "slices": [ - { - "id": "guid-or-int", - "name": "button_primary", - "x": 100, - "y": 200, - "w": 320, - "h": 90, - "padding": 0, - "format": "png", - "scale": 1, - "notes": "" - } - ] -} - -============================================================ -6) Export Rules (v1) -============================================================ -- Export crops from the original image -- Apply padding (expand crop bounds, clamp inside image) -- If exporting PNG: - - keep alpha if source has it - - if no alpha, still export as PNG (fine) -- Naming: - - sanitize to [a-zA-Z0-9_-] - - spaces -> underscore - - collisions -> _01, _02 -- Output folder structure (v1): - - single folder or optional subfolders by tag (v2) - -============================================================ -7) Keyboard + Mouse (v1) -============================================================ -- Mouse wheel: zoom -- Middle mouse drag (or Space + drag): pan -- Ctrl+S: save project -- Ctrl+O: open project -- Delete: delete selected slice -- Arrow keys: nudge slice (Shift = 10px) - -============================================================ -8) Quality-of-life (v1) -============================================================ -- Undo/Redo (basic for slice operations) -- Autosave recovery (optional small v1.1) -- “Fit image” on load -- Toggle overlay visibility -- Zoom indicator - -============================================================ -9) Planned Expansion Slots (v2+) -============================================================ -(Leave architectural “hooks” for these) -- Auto slice detection (edge/contrast UI element detection) -- AI-assisted element selection (“detect buttons/panels”) -- 9-slice editor export (Unity/Godot friendly metadata) -- Sprite sheet packing + metadata export (JSON, TexturePacker, etc.) -- Batch mode: load multiple images, export all projects -- Template packs: common UI parts naming sets - -============================================================ -10) Build Deliverables (what I will generate next) -============================================================ -Next spec files we’ll create (downloadable): -- 02_UI_Wireframe_and_Controls.txt -- 03_Data_Model_and_Classes.txt -- 04_Commands_Tools_and_Shortcuts.txt -- 05_Export_System_Details.txt -- 06_VS_Solution_Folder_Structure.txt -Then I’ll generate: -- A full Visual Studio solution scaffold (WPF + MVVM) with placeholder views, viewmodels, and services. - -============================================================ -11) Open Questions (you can answer now or later) -============================================================ -1) Do you want “project files” (save/load slices) in v1? - - Recommended: yes -2) Do you want export at multiple scales (1x/2x) in v1? -3) Do you want the app to be “portable” (save next to exe) or AppData? -4) Any target engines/frameworks for export presets? - - e.g. Unity, Godot, WinForms, WPF, web diff --git a/docs/specs/02_Freehand_Selection_and_Cutouts_WPF_SkiaSharp_v0_1.txt b/docs/specs/02_Freehand_Selection_and_Cutouts_WPF_SkiaSharp_v0_1.txt deleted file mode 100644 index d5233b2..0000000 --- a/docs/specs/02_Freehand_Selection_and_Cutouts_WPF_SkiaSharp_v0_1.txt +++ /dev/null @@ -1,296 +0,0 @@ -02 — Freehand Selection + Cutouts (Photoshop-ish) Spec -Image UI Slicer • v0.1 • WPF + SkiaSharp • Visual Studio 2026 - -============================================================ -0) Goal of this section -============================================================ -Define exactly how “freehand cutouts” work: -- User draws a freehand selection (lasso) on the image. -- The selection is previewed BEFORE committing. -- User can create multiple selections/cutouts and see how many they have. -- User exports committed cutouts as PNGs with transparency. - -This section locks: -- Tools -- States (Selection vs Cutout) -- UI panels -- Mouse/keyboard behavior -- Data representation (what we store in the project JSON for freehand shapes) -- Export behavior - -============================================================ -1) Key Concepts (Terminology) -============================================================ -A) Canvas -- The main preview area where the source image is rendered. - -B) Selection (temporary) -- A “work-in-progress” mask drawn by the user. -- Can be edited / adjusted. -- Not exported until committed. -- There can be: - - One “Active Selection” at a time (simpler v1), - - OR multiple “Pending Selections” (optional). -Recommended v1: ONE Active Selection (fast + clear), then commit it into Cutouts. - -C) Cutout (committed) -- A saved mask + crop bounds derived from the selection. -- Listed in the Cutouts panel, can be toggled visible, renamed, exported. -- Multiple cutouts can exist per project. - -D) Mask -- A shape/path that defines which pixels remain visible. -- Everything outside the mask becomes transparent when exporting PNG. - -============================================================ -2) Recommended v1 Behavior (simple + powerful) -============================================================ -- The user works with ONE Active Selection. -- When ready, user clicks “Commit Selection → Cutout”. -- This creates a new Cutout item and clears the Active Selection. -- Repeat until the user has as many cutouts as they want. -- The Cutouts list shows how many cutouts exist and previews each. - -Why this is best for v1: -- Matches your “show before selecting” requirement (active selection preview). -- Still allows “how many cutouts I want” (create many committed cutouts). -- Keeps UI and code manageable for the first build. - -============================================================ -3) Tools (Canvas toolbar) -============================================================ -Tools (v1): -1) Select/Move Tool - - Select a Cutout in the canvas (click its outline) OR via list - - Move cutout (moves its mask coordinates) (optional v1.1) - - For v1, we can keep cutouts “fixed” and not allow moving after commit. - Recommended v1: allow select/highlight only; no moving to reduce complexity. - -2) Lasso (Freehand) - - Click+drag to draw a freehand path. - - On mouse-up: closes the path automatically (connect end to start). - - Creates/updates the Active Selection. - -3) Polygon Lasso - - Click to place points. - - Hover shows preview edge to next point. - - Double-click or Enter closes shape. - -Selection modify modes (v1): -- Replace (default): new selection replaces active. -- Add: adds area to active selection. -- Subtract: subtracts area from active selection. -UI control: -- 3 toggle buttons: Replace / Add / Subtract (only one active) - -View controls: -- Pan (Space + drag) (works in any tool) -- Zoom (mouse wheel) -- Fit / 100% - -Optional tools (v1.1+): -- Brush select + Eraser -- Feather / Smooth / Expand-Shrink - -============================================================ -4) UI Layout (Updated) -============================================================ -Single Main Window with 3 columns + bottom bar. - -LEFT: Cutouts Panel -- Header: “Cutouts (N)” -- Buttons row: - - Commit Selection → Cutout (enabled only if Active Selection exists) - - Delete Cutout (enabled if a cutout is selected) - - Duplicate Cutout (v1.1 optional) -- Cutouts list: - - Each row: checkbox (visible), thumbnail, name, size (WxH) - - Selecting a row highlights cutout outline on canvas - -CENTER: Canvas -- Displays source image -- Overlays: - - Active Selection overlay (tinted fill + outline) - - Cutout outlines (thin lines) for committed cutouts - - Selected cutout outline (thicker line) -- Top toolbar: - - Select | Lasso | Poly Lasso - - Replace | Add | Subtract - - Fit | 100% | Zoom slider (optional) - - Overlay toggle (show/hide cutouts) -- Status bar (bottom of window is fine too): - - Zoom %, cursor X/Y, selection exists yes/no - -RIGHT: Inspector -- If Active Selection exists: - - Section title: “Active Selection” - - Read-only: - - Bounding box: X,Y,W,H - - Estimated pixel size: W×H - - Buttons: - - Clear Selection -- If a Cutout is selected: - - Name (editable) - - Export format (v1 = PNG only, locked) - - Export scale: 1x / 2x / custom (custom optional) - - Bounding box read-only - - Notes (optional) - -BOTTOM: Export Bar -- Export Selected Cutout -- Export All Cutouts -- Export Folder (picker + display) -- Status text (“Exported 12 cutouts to …”) - -============================================================ -5) Visual Preview Requirements (your key request) -============================================================ -A) Active Selection Preview -- While drawing, user sees: - - The in-progress path line - - A translucent fill for the “inside” area (once closed) -- After completing, user sees: - - Outline + tinted interior - - Small preview thumbnail (right panel) of the cutout result: - - Show transparency checkerboard behind it - - Crop to bounding box - -B) “How many cutouts do I want” -- The Cutouts list always shows: - - Cutouts (N) - - Thumbnails -- User can create as many cutouts as desired by repeating commit. - -C) Cutout Overlays -- All committed cutouts are optionally visible as outlines. -- Toggle: “Show Cutouts Overlay” -- Selected cutout is highlighted. - -============================================================ -6) Mouse + Keyboard Behavior (locked for v1) -============================================================ -General: -- Mouse wheel: zoom at cursor -- Space + left-drag: pan -- Ctrl+0: fit -- Ctrl+1: 100% -- Ctrl+S: save project -- Ctrl+O: open project -- Ctrl+N: new project - -Selection: -- Esc: cancel polygon lasso in progress OR clear active selection (if no in-progress) -- Enter: close polygon lasso -- Backspace (poly lasso): remove last point -- Delete: - - If cutout selected: delete cutout (confirm) - - Else if active selection exists: clear selection - -Undo/Redo (v1 basic): -- Ctrl+Z: undo last selection/cutout operation -- Ctrl+Y: redo - -============================================================ -7) Data Representation (Project JSON additions) -============================================================ -We need to store mask geometry. - -Recommended representation: -- Store each cutout as a Path with points in IMAGE coordinates (not screen coordinates) -- Each cutout has: - - id - - name - - points (list of x,y) - - mode: “freehand” or “polygon” - - boundingBox: x,y,w,h (cached for fast UI) - - optional: holes / multiple contours (v2) - -Example: -{ - "id": "c1", - "name": "button_glow", - "shape": { - "type": "path", - "mode": "freehand", - "points": [ - {"x": 120.5, "y": 88.0}, - {"x": 121.0, "y": 89.2} - ], - "closed": true - }, - "bbox": {"x": 110, "y": 70, "w": 260, "h": 120}, - "export": {"format": "png", "scale": 1, "padding": 0} -} - -Active selection is NOT saved unless we choose to. -Recommended v1: do not save active selection in project file (keep simple). -Optional v1.1: save it as “draftSelection”. - -============================================================ -8) Export Implementation Rules (SkiaSharp) -============================================================ -Export output must match Photoshop-like cutout: -- Input: original image (bitmap) -- For each cutout: - 1) Compute bounding box (bbox) of the path points - 2) Create new transparent bitmap sized bbox (plus padding) - 3) Translate canvas so bbox origin maps to (0,0) - 4) Clip to path (mask) - 5) Draw the original image into the clipped area - 6) Save as PNG - -Naming: -- Use cutout name -- sanitize + collision suffix - -Transparency: -- Always PNG with alpha in v1 - -============================================================ -9) Performance + Quality (v1) -============================================================ -- Canvas should feel smooth: - - Use SkiaSharp for rendering image + overlays - - Keep points count reasonable: - - For freehand, we can simplify points after draw (light smoothing) - - Option: sample points every N pixels while dragging - -- High DPI: - - Store coordinates in image pixels - - Render transform handles mapping image->screen - -============================================================ -10) “Auto Cut” (future) placeholder -============================================================ -Not in v1, but leave a slot: -- Button: “Auto Detect Elements (Beta)” (disabled / hidden v1) -- Future module will propose cutouts: - - shows suggestions as dashed outlines - - user accepts/rejects - -============================================================ -11) Acceptance Criteria (v1) -============================================================ -A) Must Have -- Load image -- Draw lasso selection (freehand) -- See tinted preview overlay -- Commit selection into cutout list (N count updates) -- Select cutout from list and see outline on canvas -- Export selected/all as PNG with transparency - -B) Nice to have (if time) -- Polygon lasso -- Replace/Add/Subtract -- Undo/Redo for selection/commit/delete - -============================================================ -12) Next Spec File (what we do next) -============================================================ -Next downloadable file to generate: -- 03_Data_Model_and_MVVM_Classes.txt - - ViewModels - - Services (ProjectService, ExportService, ImageService) - - Skia canvas control integration layer - - Commands and state machine - diff --git a/docs/specs/03_Data_Model_and_MVVM_Classes_WPF_SkiaSharp_v0_1.txt b/docs/specs/03_Data_Model_and_MVVM_Classes_WPF_SkiaSharp_v0_1.txt deleted file mode 100644 index 77d4a85..0000000 --- a/docs/specs/03_Data_Model_and_MVVM_Classes_WPF_SkiaSharp_v0_1.txt +++ /dev/null @@ -1,362 +0,0 @@ -03 — Data Model + MVVM Classes (WPF + SkiaSharp) Spec -Image UI Slicer • v0.1 • Visual Studio 2026 - -LOCKED INPUTS USED -- Commit behavior: 1A → Commit Selection clears Active Selection after creating Cutout. - -============================================================ -0) Goal of this section -============================================================ -Define the internal architecture so we can generate a clean Visual Studio scaffold: -- MVVM ViewModels and responsibilities -- Models (Project, Cutout, Selection) -- Services (Image loading, Project save/load, Export) -- Commands and application state rules -- SkiaSharp canvas integration boundary (no MVVM leakage into drawing code) - -Note: -- Polygon Lasso is included as a supported tool in the architecture (toggleable). - If you later decide “polygon lasso later”, we can hide/disable the tool without redesign. - -============================================================ -1) Solution + Project Layout (recommended) -============================================================ -Solution: ImageUiSlicer.sln -Project: ImageUiSlicer (WPF .NET) - -Folders (inside project): -- App/ - - App.xaml, App.xaml.cs - - Bootstrapper.cs (DI + startup) -- Views/ - - MainWindow.xaml - - Panels/ (UserControls) - - CutoutsPanel.xaml - - InspectorPanel.xaml - - ToolbarPanel.xaml - - ExportBar.xaml - - Canvas/ - - SkiaCanvasView.xaml (hosts SKElement) -- ViewModels/ - - MainViewModel.cs - - CanvasViewModel.cs - - CutoutsViewModel.cs - - InspectorViewModel.cs - - ExportViewModel.cs -- Models/ - - ProjectModel.cs - - SourceImageModel.cs - - CutoutModel.cs - - SelectionModel.cs - - PathGeometryModel.cs - - ExportOptionsModel.cs -- Services/ - - ImageService.cs - - ProjectService.cs - - ExportService.cs - - DialogService.cs (file/folder pickers) - - RecentFilesService.cs (optional) - - SettingsService.cs (optional) -- Infrastructure/ - - RelayCommand.cs - - ObservableObject.cs - - MessageBus.cs (optional event aggregator) - - JsonConverters/ (for geometry) -- CanvasEngine/ - - CanvasController.cs (tool/state coordinator) - - RenderEngine.cs (Skia render loop wrapper) - - Tools/ - - ITool.cs - - SelectTool.cs - - LassoTool.cs - - PolygonLassoTool.cs - - Geometry/ - - PathBuilder.cs - - PathSimplifier.cs (optional v1.1) - - BBox.cs - - HitTest/ - - CutoutHitTester.cs - -============================================================ -2) Application State Machine (locked rules) -============================================================ -Top-level state is managed by MainViewModel + CanvasController. - -States: -- NoImageLoaded -- ImageLoaded (default) -- ActiveSelectionExists (sub-state of ImageLoaded) -- CutoutSelected (sub-state of ImageLoaded) - -Rules (v1): -- There is at most ONE Active Selection at a time. -- Commit Selection → Cutout: - - Creates new CutoutModel from Active Selection - - Adds to ProjectModel.Cutouts - - Clears Active Selection (LOCKED by user: 1A) - - Selects the newly created Cutout (recommended behavior) -- Deleting a cutout: - - Removes it from list - - Clears selection if it was selected -- Switching tools does NOT destroy selection unless user clears. - -============================================================ -3) Models (data-only) -============================================================ -3.1 ProjectModel -- string Version -- string ProjectName -- SourceImageModel SourceImage -- ObservableCollection Cutouts -- ExportOptionsModel Defaults -- string? ProjectFilePath (not serialized) - -3.2 SourceImageModel -- string Path -- int PixelWidth -- int PixelHeight -- string? Fingerprint (optional hash, later) - -3.3 CutoutModel -- string Id -- string Name -- PathGeometryModel Geometry -- BBox BBox (cached) -- ExportOptionsModel ExportOptions -- bool IsVisible (overlay toggle) -- string? Notes -- Bitmap thumbnail cache (not serialized; regenerated) - -3.4 SelectionModel (Active Selection) -- PathGeometryModel Geometry -- BBox BBox -- bool HasSelection - -3.5 PathGeometryModel -- string Type = "path" -- string Mode = "freehand" | "polygon" -- List Points -- bool Closed = true -(Reserved for v2: multi-contour paths / holes) - -3.6 ExportOptionsModel -- string Format = "png" (v1 fixed) -- int Scale = 1 (or float) -- int Padding = 0 - -============================================================ -4) ViewModels (MVVM) -============================================================ -4.1 MainViewModel (app orchestration) -Responsibilities: -- Holds current ProjectModel -- Coordinates save/open/new -- Wires child VMs -- Controls top-level commands and window title - -Key properties: -- ProjectModel CurrentProject -- bool HasImage -- string StatusText -- string WindowTitle -- bool ShowCutoutsOverlay (global toggle) - -Commands: -- NewProjectCommand -- OpenProjectCommand -- SaveProjectCommand -- SaveProjectAsCommand -- LoadImageCommand -- ExportAllCommand -- ExportSelectedCommand - -4.2 CanvasViewModel (UI-facing canvas state) -Responsibilities: -- Exposes zoom/pan values to UI (but actual rendering is in CanvasEngine) -- Exposes currently selected cutout id -- Exposes ActiveSelection summary (bbox, has selection) - -Key properties: -- double Zoom -- Point Pan -- SelectionModel ActiveSelection (read-only binding) -- CutoutModel? SelectedCutout -- ToolType ActiveTool -- SelectionModifyMode ModifyMode (Replace/Add/Subtract — optional toggles) - -Commands: -- SetToolCommand (Select/Lasso/Polygon) -- ClearSelectionCommand -- FitToScreenCommand -- Zoom100Command - -4.3 CutoutsViewModel (left panel) -Responsibilities: -- Cutouts list + selection -- Commit selection button enablement -- Delete/rename cutout flows - -Key properties: -- ObservableCollection Cutouts (bind to ProjectModel.Cutouts) -- CutoutModel? SelectedCutout -- int CutoutCount - -Commands: -- CommitSelectionToCutoutCommand -- DeleteCutoutCommand -- RenameCutoutCommand (or inline) -- ToggleVisibilityCommand - -4.4 InspectorViewModel (right panel) -Responsibilities: -- Shows either Active Selection details or Selected Cutout details -- Validates names, scale/padding settings - -Key properties: -- bool ShowingActiveSelection -- SelectionDetails (BBox, preview image) -- CutoutDetails (Name, bbox, export settings) - -4.5 ExportViewModel (bottom bar) -Responsibilities: -- Export folder selection -- Shows export status - -Properties: -- string ExportFolder -- bool CanExportSelected -- bool CanExportAll - -Commands: -- PickExportFolderCommand -- ExportSelectedCommand -- ExportAllCommand - -============================================================ -5) Services (testable logic) -============================================================ -5.1 ImageService -- LoadBitmap(string path) -> (SKBitmap + metadata) -- Supports: PNG/JPG/WebP (WebP depends on codec; can add ImageSharp if needed) -- Provides a “renderable” bitmap for Skia - -5.2 ProjectService (JSON) -- NewProject(imagePath) -> ProjectModel -- Save(ProjectModel, filePath) -- Load(filePath) -> ProjectModel -- Validates missing image path: - - prompt user to locate file (via DialogService) - -5.3 ExportService (mask-based export) -Core method: -- ExportCutout(ProjectModel project, CutoutModel cutout, string folder) -Implementation (Skia): -- Load source bitmap (or reuse cached) -- Compute bbox -- Create target SKBitmap (transparent) -- Build SKPath from cutout.Geometry.Points -- Translate so bbox aligns to 0,0 -- Clip to path -- Draw source image -- Encode PNG to disk - -Also: -- ExportAll(ProjectModel project, folder) - -5.4 DialogService -- OpenFileDialog / SaveFileDialog / FolderPicker wrappers -- Keeps ViewModels clean - -5.5 SettingsService (optional v1) -- Store recent export folder -- Store last open directory -(Actual storage location not locked yet; can default AppData) - -============================================================ -6) CanvasEngine (Skia integration boundary) -============================================================ -Goal: Keep Skia draw loop + tool interactions out of ViewModels. - -6.1 SkiaCanvasView (View) -- Hosts SKElement (or SKGLControl if desired) -- Subscribes mouse events and forwards to CanvasController -- Calls RenderEngine.Invalidate() - -6.2 CanvasController (core interaction) -Owns: -- Current bitmap reference -- Tool instances -- Current zoom/pan (or receives from VM) -- Current Active Selection geometry -- Hit testing to select cutouts - -Public methods: -- SetTool(ToolType) -- SetModifyMode(Replace/Add/Subtract) -- OnMouseDown/Move/Up/Wheel -- ClearSelection() -- CommitSelectionToCutout() -> CutoutModel -- SelectCutoutAt(point) -- Render(SKCanvas canvas) - -Data flow: -- CanvasController writes to ProjectModel / SelectionModel via injected references -- ViewModels observe changes and update UI (INotifyPropertyChanged) - -6.3 Tools (ITool) -Interface: -- OnMouseDown/Move/Up -- RenderOverlay(SKCanvas) -- bool IsBusy (e.g., polygon in-progress) - -LassoTool: -- Collect points while dragging -- OnUp: close path, finalize Active Selection -PolygonLassoTool: -- Add point on click -- Preview edge to cursor -- Close on double click/Enter -SelectTool: -- Hit test cutouts, set SelectedCutout - -Selection Modify (optional v1): -- Replace: set ActiveSelection = new path -- Add/Subtract: combine paths (requires path boolean ops) -NOTE: Path boolean ops add complexity. -Recommended v1: implement Replace only; Add/Subtract can be stubbed. - -============================================================ -7) Commands + Enablement Rules (key UX) -============================================================ -CommitSelectionToCutoutCommand enabled when: -- ActiveSelection.HasSelection == true -AND -- Image is loaded - -ExportSelected enabled when: -- SelectedCutout != null - -ExportAll enabled when: -- Cutouts.Count > 0 - -Save enabled when: -- Project exists + has image path - -============================================================ -8) Error Handling (v1) -============================================================ -- Missing image file on project load: - - show dialog “Locate Image” -- Export folder missing: - - prompt user to pick folder -- Invalid geometry (too few points): - - show status “Selection too small” and do not commit - -============================================================ -9) Next file to generate -============================================================ -04_Commands_Tools_and_Shortcuts.txt -- Exact UI buttons list -- Exact shortcut list -- Precise mouse interactions -- Confirm polygon lasso inclusion + add/subtract decision diff --git a/docs/specs/04_Commands_Tools_and_Shortcuts_WPF_SkiaSharp_v0_1.txt b/docs/specs/04_Commands_Tools_and_Shortcuts_WPF_SkiaSharp_v0_1.txt deleted file mode 100644 index bfde3f3..0000000 --- a/docs/specs/04_Commands_Tools_and_Shortcuts_WPF_SkiaSharp_v0_1.txt +++ /dev/null @@ -1,251 +0,0 @@ -04 — Commands, Tools, and Shortcuts (Locked Interaction Spec) -Image UI Slicer • v0.1 • WPF + SkiaSharp • Visual Studio 2026 - -LOCKED INPUTS USED -- Commit behavior: 1A → Commit Selection clears Active Selection after creating Cutout. -- Polygon lasso: YES → Included in v1 toolbar and functional. - -============================================================ -0) Goal of this section -============================================================ -Lock the exact user interactions: -- Which tools exist in v1 -- What each toolbar button does -- Mouse + keyboard shortcuts -- What is previewed, when, and how -- Confirmed “Photoshop-ish” feel without feature bloat - -This section should be sufficient to implement the interaction layer and UI. - -============================================================ -1) Toolset (v1) — LOCKED -============================================================ -A) Select Tool -Purpose: -- Select a committed cutout by clicking its outline on canvas or selecting in the list. -Behavior: -- Click on a cutout outline → selects it. -- Clicking empty area → clears cutout selection (does NOT clear active selection). -- Does not move/transform cutouts in v1 (selection + highlight only). - (Moving masks is reserved for v1.1+) - -B) Lasso Tool (Freehand) -Purpose: -- Draw a freehand closed path to create/replace the Active Selection. -Behavior: -- Mouse down + drag → draws a continuous path line. -- Mouse up → closes the path (connect end → start). -- If the path has too few points / tiny area → reject with status “Selection too small”. - -C) Polygon Lasso Tool — LOCKED IN v1 -Purpose: -- Draw a closed polygon selection by placing points. -Behavior: -- Click to place points. -- A preview edge is drawn from last point to current cursor position. -- Close selection: - - Double-click, OR press Enter, OR click the first point again (optional) -- Backspace removes last point. -- Esc cancels polygon-in-progress (does not delete existing active selection unless none in progress). - -============================================================ -2) Selection Modify Mode (v1 decision) -============================================================ -To keep v1 achievable, we lock: -- Replace mode ONLY for v1 (default). -- Add/Subtract are reserved for v1.1+ (buttons may be hidden or disabled). -Reason: -- True add/subtract needs path boolean ops (more complexity). -If you want them visible in v1, we can include the buttons as disabled placeholders labeled “Coming soon”. - -LOCK (v1): -- ModifyMode = Replace only (functional) - -============================================================ -3) Canvas View Controls (v1) -============================================================ -Zoom: -- Mouse wheel zooms in/out centered on cursor position. -- Zoom indicator shows %. - -Pan: -- Space + Left mouse drag pans (works in any tool). -- Optional: Middle mouse drag pans (if you want, we’ll add v1.1). - -Fit/Reset: -- Ctrl+0 or “Fit” button fits image in view. -- Ctrl+1 or “100%” sets zoom to 100%. - -Overlay: -- Toggle: “Show Cutouts Overlay” - - ON: shows outlines for all visible cutouts - - OFF: hides outlines (except active selection overlay still shows if it exists) - -============================================================ -4) Left Panel (Cutouts) Commands — v1 -============================================================ -UI Elements: -- Header: “Cutouts (N)” -- Buttons: - 1) Commit Selection → Cutout - 2) Delete Cutout -- List: - - Each item shows: checkbox (visible), thumbnail, name, (optional W×H) - -Command behavior: -A) Commit Selection → Cutout (LOCKED) -Enabled when: -- Active Selection exists and is valid -Action: -- Creates a new Cutout from the Active Selection -- Adds to Cutouts list -- Clears Active Selection (LOCKED) -- Selects the new Cutout in list (recommended) -- Updates status: “Cutout created: ” - -B) Delete Cutout -Enabled when: -- A cutout is selected -Action: -- Confirmation dialog: - - “Delete cutout ‘’? This cannot be undone.” -- If yes: remove cutout, clear selection, update status. - -C) Rename -- Inline rename in list OR rename field in Inspector. -Rule: -- Name auto-sanitized for export (spaces -> underscore), but display name remains what user typed. - -D) Visibility toggle -- Checkbox controls whether the cutout’s overlay outline is shown when “Show Cutouts Overlay” is ON. - -============================================================ -5) Right Panel (Inspector) — v1 -============================================================ -Inspector shows one of two contexts: - -A) Active Selection Context -Shown when: -- Active Selection exists (even if a cutout is selected) -Contents: -- Title: “Active Selection” -- Read-only bbox: X, Y, W, H (image pixels) -- Preview thumbnail: - - Shows checkerboard background - - Shows the masked selection result (cropped to bbox) -Buttons: -- Clear Selection - -B) Selected Cutout Context -Shown when: -- No Active Selection exists AND a cutout is selected -Contents: -- Name (editable) -- Read-only bbox: X, Y, W, H -- Export settings: - - Format: PNG (locked) - - Scale: 1x (v1 default; 2x/custom later) - - Padding: 0 (editable optional; can be v1.1) -- Notes (optional; can be v1.1) - -============================================================ -6) Bottom Export Bar — v1 -============================================================ -Elements: -- Export Folder picker + display (path) -- Export Selected -- Export All -- Status text - -Rules: -A) Export Selected -Enabled: -- A cutout is selected -Action: -- Export only the selected cutout to the export folder as PNG - -B) Export All -Enabled: -- Cutouts.Count > 0 -Action: -- Export all cutouts in list order - -C) Export folder missing -- If no folder set, prompt folder picker before exporting. - -Naming: -- Uses cutout name -- Sanitizes filename -- Collisions -> _01, _02 … - -============================================================ -7) Keyboard Shortcuts — v1 LOCKED -============================================================ -File: -- Ctrl+N: New project -- Ctrl+O: Open project -- Ctrl+S: Save project -- Ctrl+Shift+S: Save as - -Canvas: -- Ctrl+0: Fit -- Ctrl+1: 100% -- Esc: - - Cancel polygon lasso in progress - - If no polygon in progress: clears active selection (optional) -- Enter (polygon lasso): close polygon -- Backspace (polygon lasso): remove last point - -Edit: -- Delete: - - If cutout selected AND no active selection: delete cutout (with confirm) - - Else if active selection exists: clear selection -- Ctrl+Z / Ctrl+Y: - - Undo/redo selection/cutout operations (basic) - -Tools: -- V: Select tool -- L: Freehand lasso -- P: Polygon lasso - -============================================================ -8) Undo/Redo Scope — v1 -============================================================ -Undoable actions (v1): -- Create/replace active selection -- Commit selection to cutout -- Delete cutout -- Rename cutout (optional) - -Not in v1: -- Moving cutout masks (since movement not supported) - -============================================================ -9) Visual Feedback Requirements — v1 -============================================================ -- Active selection: - - Tinted overlay inside selection - - Outline visible - - In-progress drawing line visible while dragging/clicking -- Polygon: - - Preview segment from last point to cursor - - Points visible as small nodes -- Cutouts: - - Thin outlines for visible cutouts (toggleable) - - Selected cutout outline thicker - -Status text updates for: -- Image loaded -- Selection too small -- Cutout created/deleted -- Export finished + count - -============================================================ -10) Next Spec File to generate -============================================================ -05_Project_Save_Load_and_JSON.txt -Locks: -- Project file schema final -- Save/load UX flows (missing image, relink) -- Settings storage (AppData vs portable) -- Recent files (optional) diff --git a/docs/specs/05_Project_Save_Load_and_JSON_AppData_v0_1.txt b/docs/specs/05_Project_Save_Load_and_JSON_AppData_v0_1.txt deleted file mode 100644 index 2ebbea7..0000000 --- a/docs/specs/05_Project_Save_Load_and_JSON_AppData_v0_1.txt +++ /dev/null @@ -1,214 +0,0 @@ -05 — Project Save/Load + JSON + Settings Storage (LOCKED) -Image UI Slicer • v0.1 • WPF + SkiaSharp • Visual Studio 2026 - -LOCKED INPUTS USED -- Storage location: A → AppData (recommended) is LOCKED for settings + default project location behavior. - -============================================================ -0) Goal of this section -============================================================ -Lock how projects are saved/loaded and where settings live: -- Project file format and schema (JSON) -- Save/Open UX flows -- Missing image relink flow -- Settings storage in AppData -- Recent files list behavior -- Default export folder persistence - -============================================================ -1) Storage Locations (LOCKED) -============================================================ -A) App Settings location (LOCKED) -Windows standard per-user location: -- %APPDATA%\ImageUiSlicer\ - - settings.json - - recent.json (optional) - - cache\ (thumbnails; optional) - -B) Project files -Projects can be saved anywhere the user chooses, but defaults are: -- Default “Save As” starting folder: - - %USERPROFILE%\Documents\ImageUiSlicer Projects\ -(We create this folder if missing.) - -C) Export folder default -- Default export folder persists in settings.json -- User can override per project (saved inside project file) - -============================================================ -2) Project File Type (LOCKED) -============================================================ -- Extension: .iusproj (Image UI Slicer Project) -- Format: JSON (UTF-8) -- Human readable, versioned - -============================================================ -3) Project JSON Schema (LOCKED v0.1) -============================================================ -Top-level: -{ - "version": "0.1", - "projectName": "My Project", - "createdUtc": "2026-03-03T00:00:00Z", - "modifiedUtc": "2026-03-03T00:00:00Z", - - "sourceImage": { - "path": "C:\\path\\to\\image.png", - "pixelWidth": 1920, - "pixelHeight": 1080, - "fingerprint": null - }, - - "defaults": { - "exportFolder": "C:\\exports", - "format": "png", - "scale": 1, - "padding": 0 - }, - - "ui": { - "showCutoutsOverlay": true, - "zoom": 1.0, - "panX": 0.0, - "panY": 0.0 - }, - - "cutouts": [ - { - "id": "c1", - "name": "button_primary", - "isVisible": true, - "geometry": { - "type": "path", - "mode": "freehand", - "closed": true, - "points": [ { "x": 120.5, "y": 88.0 } ] - }, - "bbox": { "x": 110, "y": 70, "w": 260, "h": 120 }, - "export": { "format": "png", "scale": 1, "padding": 0 }, - "notes": "" - } - ] -} - -Notes (locked decisions): -- Cutouts geometry points are stored in IMAGE pixel coordinates. -- Active Selection is NOT saved to disk in v0.1 (keeps project clean). - (We can add "draftSelection" later if desired.) -- bbox is stored as cached data; on load, we can recompute and validate. - -============================================================ -4) Settings File (AppData) (LOCKED v0.1) -============================================================ -File: -- %APPDATA%\ImageUiSlicer\settings.json - -Schema: -{ - "version": "0.1", - "lastOpenFolder": "C:\\Users\\...\\Documents\\ImageUiSlicer Projects", - "lastImageFolder": "C:\\Users\\...\\Pictures", - "defaultExportFolder": "C:\\Exports", - "showCutoutsOverlay": true, - "theme": "system", - "window": { - "width": 1400, - "height": 900, - "x": 50, - "y": 50, - "isMaximized": false - } -} - -============================================================ -5) Recent Files (AppData) (LOCKED v0.1) -============================================================ -File: -- %APPDATA%\ImageUiSlicer\recent.json - -Schema: -{ - "version": "0.1", - "items": [ - { - "path": "C:\\...\\project.iusproj", - "lastOpenedUtc": "2026-03-03T00:00:00Z" - } - ] -} - -Rules: -- Keep max 15 items -- If missing on disk, remove automatically -- Display in File menu and optional Home screen (later) - -============================================================ -6) Save / Load UX Flows (LOCKED) -============================================================ -A) New Project -- User chooses image file -- App creates in-memory project: - - projectName = image filename (without extension) by default - - defaults.exportFolder = settings.defaultExportFolder (if set) -- User must Save As to create a .iusproj file - -B) Save -- If project has a file path: - - write JSON - - update modifiedUtc -- If no file path: - - redirect to Save As - -C) Save As -- Default folder: Documents\ImageUiSlicer Projects -- Suggested filename: .iusproj - -D) Open Project -- OpenFileDialog filters *.iusproj -- After loading JSON: - - validate version - - attempt to load sourceImage.path - -E) Missing Image Relink (LOCKED) -If sourceImage.path is missing: -- Show dialog: - - “Source image not found. Locate it to continue.” -- Buttons: - - Locate Image… (file picker) - - Cancel Open (returns to previous state) -Rules: -- If user locates a new file: - - update sourceImage.path - - update pixelWidth/pixelHeight from loaded image - - keep cutout geometry as-is - - If dimensions changed, show warning: - - “Image dimensions differ; cutouts may not align.” - - Save will persist the new path. - -============================================================ -7) Versioning + Compatibility (v0.1) -============================================================ -- If project.version > app-supported version: - - show “This project was created with a newer version.” - - open in read-only mode (optional v1.1) OR block open (v1) -Recommended v1: block open with clear message. - -- If project.version < supported: - - run migration steps if needed (v1.1+) - -============================================================ -8) Autosave / Recovery (not locked) -============================================================ -Not in v0.1 baseline. -Optional v1.1: -- periodic autosave to AppData\recovery\ - -============================================================ -9) Next Spec File to generate -============================================================ -06_Visual_Studio_Solution_Scaffold.txt -Locks: -- Exact .NET target framework -- NuGet packages (SkiaSharp) -- MainWindow composition (UserControls) -- Initial placeholder implementation plan for each file diff --git a/docs/specs/07_Export_Folder_Structure_Cut_Outs_Subfolder_v0_1.txt b/docs/specs/07_Export_Folder_Structure_Cut_Outs_Subfolder_v0_1.txt deleted file mode 100644 index bd13665..0000000 --- a/docs/specs/07_Export_Folder_Structure_Cut_Outs_Subfolder_v0_1.txt +++ /dev/null @@ -1,94 +0,0 @@ -07 — Export Folder Structure (LOCKED: “cut outs” subfolder) -Image UI Slicer • v0.1 • WPF + SkiaSharp • Visual Studio 2026 - -LOCKED INPUTS USED -- Export outputs create/use a sibling subfolder named exactly: "cut outs" - -============================================================ -0) Goal of this section -============================================================ -Lock export destination rules so exports are consistent for: -- Export Selected -- Export All - -Requirement: -- When exporting, the app must create (if missing) and use a folder named “cut outs” - inside the chosen export folder. All exports go into that subfolder. - -============================================================ -1) Export Folder Selection (Base Folder) -============================================================ -User selects an Export Folder in the bottom bar (the “Base Export Folder”). - -Definitions: -- Base Export Folder = user-chosen folder path -- Cut Outs Export Folder = Base Export Folder\cut outs\ - -Rules (LOCKED): -- Export Selected and Export All ALWAYS write files to Cut Outs Export Folder. -- If Base Export Folder is not set, prompt user to pick it. -- If Cut Outs Export Folder does not exist, create it automatically. -- If creation fails (permissions), show error and abort export. - -============================================================ -2) Single vs Bulk Export (LOCKED) -============================================================ -A) Export Selected -- Exports only the currently selected cutout -- Output folder: BaseExportFolder\cut outs\ - -B) Export All -- Exports all cutouts in list order -- Output folder: BaseExportFolder\cut outs\ - -No other folders are created in v0.1 (tags/subfolders reserved for later). - -============================================================ -3) Naming + Output Paths (v0.1) -============================================================ -For each cutout: -- output path = BaseExportFolder\cut outs\.png - -Filename rules: -- Use cutout name -- Sanitize for filesystem: - - Trim whitespace - - Replace spaces with underscores in filenames ONLY (folder name stays "cut outs") - - Remove invalid filename characters: \ / : * ? " < > | - - Collapse repeated underscores -- Handle collisions within the export folder: - - .png - - _01.png - - _02.png - - etc. - -Examples: -- Cutout name: "Primary Button" - - Exports to: ...\cut outs\Primary_Button.png - -============================================================ -4) Safety Rules (v0.1) -============================================================ -- Never overwrite existing files silently: - - If a collision occurs, auto-suffix as above. -- If a cutout name becomes empty after sanitization: - - fallback name: "cutout" - - then apply suffixing if needed. - -============================================================ -5) UI Text (v0.1) -============================================================ -In the bottom bar: -- Show Base Export Folder path (user chosen) -- Show small hint text (optional): - - “Exports are saved to: \cut outs\” - -============================================================ -6) Next Spec File to generate -============================================================ -06_Visual_Studio_Solution_Scaffold.txt -(or if already started) -08_Export_Implementation_Details_SkiaSharp.txt -- exact Skia clipping steps -- encoder settings -- thumbnail generation diff --git a/docs/specs/08_Export_Auto_Naming_With_Override_v0_1.txt b/docs/specs/08_Export_Auto_Naming_With_Override_v0_1.txt deleted file mode 100644 index 8990545..0000000 --- a/docs/specs/08_Export_Auto_Naming_With_Override_v0_1.txt +++ /dev/null @@ -1,6 +0,0 @@ -08 — Export Auto-Naming + Optional Override (LOCKED) -- Default: numbered 001,002,003… in cutouts list order -- Override at export time: Use Cutout Names -- No silent overwrites; suffix _01, _02… - -Export folder: BaseExportFolder\cut outs\ diff --git a/docs/specs/09_Big_Update_Keep_Simple_v0_2_Draft.txt b/docs/specs/09_Big_Update_Keep_Simple_v0_2_Draft.txt deleted file mode 100644 index e1a7d7d..0000000 --- a/docs/specs/09_Big_Update_Keep_Simple_v0_2_Draft.txt +++ /dev/null @@ -1,5 +0,0 @@ -09 — Big Update, Keep It Simple (v0.2 Draft) -A) Multi-select + reorder cutouts -B) Export prefix + naming controls -C) Drag & drop image/project -D) Move/nudge cutouts after commit diff --git a/docs/specs/10_LOCK_MultiSelect_and_Reorder_v0_2.txt b/docs/specs/10_LOCK_MultiSelect_and_Reorder_v0_2.txt deleted file mode 100644 index 62b1bee..0000000 --- a/docs/specs/10_LOCK_MultiSelect_and_Reorder_v0_2.txt +++ /dev/null @@ -1,5 +0,0 @@ -10 — LOCKED: Multi-Select + Reorder (v0.2) -- Multi-select (Ctrl/Shift/Ctrl+A) -- Export Selected exports ALL selected (list order filtered) -- Reorder via Move Up/Down (multi-selection moves as a block) -- Order persists in project JSON diff --git a/docs/specs/11_LOCK_Export_Prefix_and_Naming_v0_2.txt b/docs/specs/11_LOCK_Export_Prefix_and_Naming_v0_2.txt deleted file mode 100644 index 0710817..0000000 --- a/docs/specs/11_LOCK_Export_Prefix_and_Naming_v0_2.txt +++ /dev/null @@ -1,5 +0,0 @@ -11 — LOCKED: Export Prefix + Naming Controls (v0.2) -- Naming dropdown: Auto Numbered (default) / Use Cutout Names -- Prefix textbox applies to both modes -- Auto numbered uses list order (or filtered selection order) -- No silent overwrite; suffix _01, _02… diff --git a/docs/specs/12_LOCK_DragDrop_Images_Projects_v0_2.txt b/docs/specs/12_LOCK_DragDrop_Images_Projects_v0_2.txt deleted file mode 100644 index f0dae88..0000000 --- a/docs/specs/12_LOCK_DragDrop_Images_Projects_v0_2.txt +++ /dev/null @@ -1,6 +0,0 @@ -12 — LOCKED: Drag & Drop (v0.2) -- Drop .iusproj -> open project -- Drop image -> new project from image -- Project priority in mixed drops -- Multiple images -> user picks one -- Unsaved changes prompt: Save / Discard / Cancel diff --git a/docs/specs/13_LOCK_Move_Nudge_Cutouts_v0_2.txt b/docs/specs/13_LOCK_Move_Nudge_Cutouts_v0_2.txt deleted file mode 100644 index fd99d6f..0000000 --- a/docs/specs/13_LOCK_Move_Nudge_Cutouts_v0_2.txt +++ /dev/null @@ -1,5 +0,0 @@ -13 — LOCKED: Move/Nudge Cutouts After Commit (v0.2) -- Arrow: 1px, Shift+Arrow: 10px -- Applies to single or multi-selection -- Clamped within image bounds -- Undoable diff --git a/docs/specs/MASTER_LOCK_SPEC_Image_UI_Slicer_v0_2_ABCD.txt b/docs/specs/MASTER_LOCK_SPEC_Image_UI_Slicer_v0_2_ABCD.txt deleted file mode 100644 index b304ceb..0000000 --- a/docs/specs/MASTER_LOCK_SPEC_Image_UI_Slicer_v0_2_ABCD.txt +++ /dev/null @@ -1,13 +0,0 @@ -IMAGE UI SLICER — FULL MASTER LOCK SPEC (v0.2 — A+B+C+D) -Target: Visual Studio 2026 • Windows • WPF + SkiaSharp -Last updated: 2026-03-04 - -LOCKED: -- Freehand + polygon lasso selection with preview before commit -- Commit Selection -> Cutout clears selection (1A) -- Multi-select + reorder cutouts (Up/Down) -- Export folder: BaseExportFolder\cut outs\ -- Default export naming: numbered 001.. in list order -- Export naming override + Prefix textbox -- Drag & drop image/project with unsaved-changes prompt -- Move/nudge cutouts after commit (Arrow / Shift+Arrow), clamped, undoable diff --git a/solution/ImageUiSlicer.Tests/ImageUiSlicer.Tests.csproj b/solution/ImageUiSlicer.Tests/ImageUiSlicer.Tests.csproj new file mode 100644 index 0000000..99de233 --- /dev/null +++ b/solution/ImageUiSlicer.Tests/ImageUiSlicer.Tests.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0-windows + true + false + enable + enable + + + + + diff --git a/solution/ImageUiSlicer.Tests/Program.DropChecks.cs b/solution/ImageUiSlicer.Tests/Program.DropChecks.cs new file mode 100644 index 0000000..5e046ad --- /dev/null +++ b/solution/ImageUiSlicer.Tests/Program.DropChecks.cs @@ -0,0 +1,30 @@ +using System.IO; +using ImageUiSlicer.Views.Dialogs; + +namespace ImageUiSlicer.Tests; + +internal static partial class Program +{ + private static void MultipleDroppedImagesRequireConfirmedChoice() + { + var firstPath = Path.Combine(Path.GetTempPath(), "first-folder", "sample.png"); + var secondPath = Path.Combine(Path.GetTempPath(), "second-folder", "sample.png"); + var choice = new DroppedImageChoiceModel([firstPath, secondPath]); + + Require(choice.Options.Count == 2, "The chooser did not retain every dropped image."); + Require( + choice.Options.All(option => option.FileName == "sample.png"), + "The chooser does not display image filenames."); + Require( + choice.Options.Select(option => option.FolderPath).Distinct().Count() == 2, + "Images with the same filename cannot be distinguished by folder."); + Require( + choice.ResolveSelection(accepted: false) is null, + "Cancelling the chooser unexpectedly selected an image."); + + choice.SelectedOption = choice.Options[1]; + Require( + choice.ResolveSelection(accepted: true) == secondPath, + "The chooser ignored the user's selected image."); + } +} diff --git a/solution/ImageUiSlicer.Tests/Program.ImageChecks.cs b/solution/ImageUiSlicer.Tests/Program.ImageChecks.cs new file mode 100644 index 0000000..95a79c7 --- /dev/null +++ b/solution/ImageUiSlicer.Tests/Program.ImageChecks.cs @@ -0,0 +1,53 @@ +using System.IO; +using ImageUiSlicer.CanvasEngine; +using ImageUiSlicer.Services; +using SkiaSharp; + +namespace ImageUiSlicer.Tests; + +internal static partial class Program +{ + private static void FitViewNeverEnlargesSmallSourceImage() + { + var smallSourceScale = ViewportScalePolicy.ComputeFitScale(1200, 800, 320, 240); + var largeSourceScale = ViewportScalePolicy.ComputeFitScale(1200, 800, 4000, 3000); + + Require( + Math.Abs(smallSourceScale - 1f) < 0.0001f, + "Fit view enlarged a source image above its native pixel size."); + Require( + largeSourceScale > 0f && largeSourceScale < 1f, + "Fit view did not reduce a source image that exceeds the viewport."); + } + + private static void ImageLoadingPreservesOriginalPixelDimensions() + { + var imagePath = Path.Combine(Path.GetTempPath(), $"photo-cutter-source-{Guid.NewGuid():N}.png"); + try + { + using (var source = new SKBitmap(2048, 1024, SKColorType.Bgra8888, SKAlphaType.Premul)) + { + source.Erase(SKColors.CornflowerBlue); + using var image = SKImage.FromBitmap(source); + using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); + using var output = File.Create(imagePath); + encoded.SaveTo(output); + } + + using var loaded = new ImageService().LoadBitmap(imagePath); + Require( + loaded.Width == 2048 && loaded.Height == 1024, + "The source image was resized while loading."); + Require( + loaded.GetPixel(2047, 1023) == SKColors.CornflowerBlue, + "The source image's final pixel was not preserved."); + } + finally + { + if (File.Exists(imagePath)) + { + File.Delete(imagePath); + } + } + } +} diff --git a/solution/ImageUiSlicer.Tests/Program.ProjectChecks.cs b/solution/ImageUiSlicer.Tests/Program.ProjectChecks.cs new file mode 100644 index 0000000..acc15fc --- /dev/null +++ b/solution/ImageUiSlicer.Tests/Program.ProjectChecks.cs @@ -0,0 +1,158 @@ +using System.IO; +using System.Collections.ObjectModel; +using ImageUiSlicer.CanvasEngine; +using ImageUiSlicer.Core.Geometry; +using ImageUiSlicer.Models; +using ImageUiSlicer.Services; +using ImageUiSlicer.ViewModels; +using SkiaSharp; + +namespace ImageUiSlicer.Tests; + +internal static partial class Program +{ + private static void VisibleActionWordingStaysConsistent() + { + using var source = CreateSolidBitmap(60, 60, SKColors.Coral); + var viewModel = new MainViewModel(); + viewModel.ApplyImageProject("wording-check.png", source); + + viewModel.SetActiveTool(CanvasTool.BrushAdd); + Require(viewModel.StatusText.StartsWith("Add to Edge", StringComparison.Ordinal), "The Add to Edge status uses a different action name."); + viewModel.SetActiveTool(CanvasTool.BrushErase); + Require(viewModel.StatusText.StartsWith("Remove from Edge", StringComparison.Ordinal), "The Remove from Edge status uses a different action name."); + + var cutout = CreateCutout( + "Badge", + new PointF(5, 5), + new PointF(25, 5), + new PointF(25, 25), + new PointF(5, 25)); + viewModel.Project.Cutouts.Add(cutout); + viewModel.SetSelectedCutouts(new[] { cutout }); + Require(viewModel.DuplicateSelectedCutoutCommand.CanExecute(null), "Place Copy was not available for one selected cut-out."); + viewModel.DuplicateSelectedCutoutCommand.Execute(null); + Require(viewModel.StatusText.StartsWith("Placed a copy", StringComparison.Ordinal), "Place Copy reported unrelated duplicate wording."); + } + + private static void MovementClampingPreservesImageBoundaries() + { + PointF[] points = + [ + new(0.5f, 2.5f), + new(9.5f, 2.5f), + new(9.5f, 12.5f), + new(0.5f, 12.5f), + ]; + + var precise = GeometryMovementPolicy.Clamp(points, -4f, 20f, 20, 20); + Require(Math.Abs(precise.Dx - -0.5f) < 0.001f, "Precise movement crossed the left image boundary."); + Require(Math.Abs(precise.Dy - 7.5f) < 0.001f, "Precise movement crossed the bottom image boundary."); + + var integer = GeometryMovementPolicy.ClampInteger(points, -1, 20, 20, 20); + Require(integer.Dx == 0, "Integer nudging introduced an unexpected half-pixel move."); + Require(integer.Dy == 7, "Integer nudging did not preserve its whole-pixel boundary rule."); + } + + private static void WorkspaceRoundTripPreservesCompatibleData() + { + var projectPath = Path.Combine(Path.GetTempPath(), $"photo-cutter-roundtrip-{Guid.NewGuid():N}.iusproj"); + var createdUtc = new DateTime(2025, 4, 3, 2, 1, 0, DateTimeKind.Utc); + try + { + var cutout = CreateCutout( + "Legacy details", + new PointF(10, 10), + new PointF(30, 10), + new PointF(30, 30), + new PointF(10, 30)); + cutout.Notes = "kept for older workspaces"; + cutout.AutoConfidence = 0.75; + cutout.Export.Format = "legacy-value"; + cutout.Export.Padding = 4; + + var savedShape = CreateSavedShape("Exact twenty", 0); + var project = new ProjectModel + { + ProjectName = "Round trip", + CreatedUtc = createdUtc, + SourceImage = new SourceImageModel + { + Path = "source.png", + PixelWidth = 100, + PixelHeight = 100, + }, + Cutouts = new ObservableCollection { cutout }, + SavedShapes = new ObservableCollection { savedShape }, + }; + + var service = new ProjectService(); + service.Save(project, projectPath); + var loaded = service.Load(projectPath); + + Require(loaded.CreatedUtc == createdUtc, "The workspace creation date was lost."); + Require(loaded.Cutouts.Single().Notes == cutout.Notes, "Legacy cut-out notes were lost."); + Require(loaded.Cutouts.Single().AutoConfidence == cutout.AutoConfidence, "Legacy confidence data was lost."); + Require(loaded.Cutouts.Single().Export.Format == "png", "The legacy output format was not safely normalised to PNG."); + Require(loaded.Cutouts.Single().Export.Padding == 4, "Cut-out padding changed during the round trip."); + Require(loaded.SavedShapes.Single().BBox == savedShape.BBox, "The exact saved-shape size changed during the round trip."); + } + finally + { + File.Delete(projectPath); + } + } + + private static void SavedShapePlacementKeepsExactSize() + { + using var bitmap = CreateSolidBitmap(200, 200, SkiaSharp.SKColors.Coral); + var viewModel = new MainViewModel(); + viewModel.ApplyImageProject("placement.png", bitmap.Copy()); + + var savedShape = CreateSavedShape("Exact shape", 0); + viewModel.Project.SavedShapes.Add(savedShape); + viewModel.SelectedShapeLibraryOption = viewModel.ShapeLibraryOptions.Single( + option => option.SavedShape?.Id == savedShape.Id); + + var placed = viewModel.TryPlaceSelectedSavedShapeAt(new PointF(100, 100)); + + Require(placed, "The saved shape was not accepted for placement."); + Require(viewModel.HasActiveSelection, "Placement did not create an active outline."); + Require( + viewModel.ActiveSelection.BBox.W == savedShape.BBox.W && + viewModel.ActiveSelection.BBox.H == savedShape.BBox.H, + "Saved-shape placement silently resized the geometry."); + } + + private static void CutoutOrderingDoesNotMoveGeometry() + { + var viewModel = new MainViewModel(); + var first = CreateCutout( + "First", + new PointF(1, 1), + new PointF(11, 1), + new PointF(11, 11), + new PointF(1, 11)); + var second = CreateCutout( + "Second", + new PointF(20, 20), + new PointF(30, 20), + new PointF(30, 30), + new PointF(20, 30)); + var originalGeometry = first.Geometry.DeepClone(); + + viewModel.Project.Cutouts.Add(first); + viewModel.Project.Cutouts.Add(second); + viewModel.SelectSingleCutout(first); + Require(viewModel.MoveDownCommand.CanExecute(null), "The down-order command was unavailable."); + viewModel.MoveDownCommand.Execute(null); + + Require(ReferenceEquals(viewModel.Project.Cutouts[1], first), "The cut-out did not move later in export order."); + Require( + first.Geometry.Points.SequenceEqual(originalGeometry.Points), + "Changing export order moved the cut-out on the image."); + + viewModel.UndoCommand.Execute(null); + Require(viewModel.Project.Cutouts[0].Name == "First", "Undo did not restore the original export order."); + } +} diff --git a/solution/ImageUiSlicer.Tests/Program.RenderingChecks.cs b/solution/ImageUiSlicer.Tests/Program.RenderingChecks.cs new file mode 100644 index 0000000..84782e9 --- /dev/null +++ b/solution/ImageUiSlicer.Tests/Program.RenderingChecks.cs @@ -0,0 +1,150 @@ +using System.IO; +using ImageUiSlicer.Workflows.Exporting; +using ImageUiSlicer.Core.Geometry; +using ImageUiSlicer.Models; +using ImageUiSlicer.Services; +using SkiaSharp; + +namespace ImageUiSlicer.Tests; + +internal static partial class Program +{ + private static void InspectorPreviewIncludesExportPadding() + { + using var source = CreateSolidBitmap(20, 20, SKColors.Coral); + var cutout = CreateCutout( + "Padded", + new PointF(5, 5), + new PointF(15, 5), + new PointF(15, 15), + new PointF(5, 15)); + cutout.Export.Padding = 3; + cutout.Export.TargetPixelSize = -1; + + using var exported = new CutoutRenderService().RenderCutoutBitmap(source, cutout); + using var preview = new CutoutRenderService().RenderInspectorPreviewBitmap( + source, + cutout, + splitPreview: false, + splitRatio: 0.5); + + Require(preview is not null, "The padded inspector preview was not rendered."); + Require( + preview!.Width == exported.Width && preview.Height == exported.Height, + "The inspector crop did not include the same padding as the exported cut-out."); + } + + private static void EveryBuiltInShapeCreatesValidGeometry() + { + foreach (var definition in ShapePresetCatalog.BuiltIns) + { + var geometry = ShapeGeometryFactory.Create( + definition.Preset, + new PointF(100, 100), + new PointF(145, 135), + useUniformRadius: false, + imageWidth: 200, + imageHeight: 200); + + Require( + GeometryHelper.IsValidGeometry(geometry), + $"{definition.Label} did not create valid closed geometry."); + Require( + GeometryHelper.IntersectsImage(geometry, 200, 200), + $"{definition.Label} did not remain inside the source image."); + Require( + string.Equals(geometry.Mode, definition.GeometryMode, StringComparison.Ordinal), + $"{definition.Label} did not retain its registered geometry mode."); + } + } + + private static void PngExportUsesSafeUniqueFilenames() + { + var folder = Path.Combine(Path.GetTempPath(), $"photo-cutter-export-{Guid.NewGuid():N}"); + try + { + using var source = CreateSolidBitmap(20, 20, SKColors.Coral); + var cutout = CreateCutout( + "Export", + new PointF(2, 2), + new PointF(18, 2), + new PointF(18, 18), + new PointF(2, 18)); + var service = new ExportService(); + + var first = service.ExportCutout(source, cutout, folder, "CON"); + var second = service.ExportCutout(source, cutout, folder, "CON"); + + Require(File.Exists(first) && File.Exists(second), "The PNG files were not written."); + Require(!string.Equals(first, second, StringComparison.OrdinalIgnoreCase), "An existing PNG was overwritten."); + Require(Path.GetFileName(first).StartsWith("_CON", StringComparison.OrdinalIgnoreCase), "A reserved Windows filename was not repaired."); + using var decoded = SKBitmap.Decode(first); + Require(decoded is not null && decoded.Width == 16 && decoded.Height == 16, "The exported file was not a readable, correctly sized PNG."); + } + finally + { + if (Directory.Exists(folder)) + { + Directory.Delete(folder, recursive: true); + } + } + } + + private static void BatchExportPreservesOrderAndSkipsInvalidItems() + { + var folder = Path.Combine(Path.GetTempPath(), $"photo-cutter-batch-{Guid.NewGuid():N}"); + try + { + using var source = CreateSolidBitmap(40, 40, SKColors.Coral); + var first = CreateCutout( + "First", + new PointF(1, 1), + new PointF(10, 1), + new PointF(10, 10), + new PointF(1, 10)); + var invalid = CreateCutout( + "Outside", + new PointF(100, 100), + new PointF(110, 100), + new PointF(110, 110), + new PointF(100, 110)); + var third = CreateCutout( + "Third", + new PointF(20, 20), + new PointF(30, 20), + new PointF(30, 30), + new PointF(20, 30)); + var ordered = new[] { first, invalid, third }; + var coordinator = new ExportBatchCoordinator(); + + var selectedResult = coordinator.Export( + source, + ordered, + new[] { third }, + folder, + "autoNumbered", + string.Empty); + Require(selectedResult.ExportedCount == 1 && selectedResult.Failures.Count == 0, "Selected export did not process exactly one valid item."); + Require(File.Exists(Path.Combine(folder, "003.png")), "Selected export lost the cut-out's project-order number."); + + var allResult = coordinator.Export( + source, + ordered, + selectedCutouts: null, + folder, + "autoNumbered", + "all_"); + Require(allResult.ExportedCount == 2, "Batch export did not continue past an invalid item."); + Require(allResult.Failures.Count == 1, "The invalid item was not reported exactly once."); + Require(File.Exists(Path.Combine(folder, "all_001.png")), "The first ordered cut-out was not exported."); + Require(File.Exists(Path.Combine(folder, "all_003.png")), "The third ordered cut-out lost its project-order number."); + } + finally + { + if (Directory.Exists(folder)) + { + Directory.Delete(folder, recursive: true); + } + } + } +} diff --git a/solution/ImageUiSlicer.Tests/Program.UiContractChecks.cs b/solution/ImageUiSlicer.Tests/Program.UiContractChecks.cs new file mode 100644 index 0000000..e784eeb --- /dev/null +++ b/solution/ImageUiSlicer.Tests/Program.UiContractChecks.cs @@ -0,0 +1,374 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using ImageUiSlicer.CanvasEngine; +using ImageUiSlicer.CanvasEngine.Contracts; +using ImageUiSlicer.ViewModels; +using ImageUiSlicer.Views; +using ImageUiSlicer.Views.Components; + +namespace ImageUiSlicer.Tests; + +internal static partial class Program +{ + private static void CompactHeaderProtectsPhotoPreviewSpace() + { + const double maxTwoRowHeaderHeight = 104; + const double wideHeaderContractWidth = 1436; + var window = new MainWindow + { + Width = 1460, + Height = 780, + ShowActivated = false, + ShowInTaskbar = false, + Opacity = 0, + }; + try + { + window.Show(); + window.UpdateLayout(); + + var header = FindLogicalChildren(window).Single(); + header.Width = wideHeaderContractWidth; + window.UpdateLayout(); + var cutTools = (StackPanel)header.FindName("CutToolsGroup"); + var viewTools = (StackPanel)header.FindName("ViewToolsGroup"); + var compactHeaderHeight = header.ActualHeight; + Require( + compactHeaderHeight <= maxTwoRowHeaderHeight, + $"The two-row header grew back to {compactHeaderHeight:0.#} px."); + Require( + Math.Abs(cutTools.TranslatePoint(new Point(), header).Y - + viewTools.TranslatePoint(new Point(), header).Y) < 1, + "Cut and View controls did not share the compact toolbar line."); + + var toolButtons = FindLogicalChildren(header) + .Where(button => button.Content is string content && + content is "Select & Move" or "Draw Around" or "Click Around" or "Shape Cut") + .ToList(); + Require(toolButtons.Count == 4, "The compact header lost a cut tool."); + Require(toolButtons.All(button => button.ActualHeight <= 30), "A cut tool became tall again."); + Require(toolButtons.All(button => button.ActualWidth >= 88), "A cut tool became too narrow to scan quickly."); + + ((MainViewModel)window.DataContext).IsShapeTool = true; + window.UpdateLayout(); + Require( + header.ActualHeight <= compactHeaderHeight + 1, + "Showing the Shape Cut selector added an unnecessary header row at normal width."); + } + finally + { + window.Close(); + } + } + + private static void CutToolsUsePlainEnglishAndFiveLensLevels() + { + var window = new MainWindow(); + try + { + window.ShowActivated = false; + window.ShowInTaskbar = false; + window.Opacity = 0; + window.Show(); + window.UpdateLayout(); + + var toolLabels = FindLogicalChildren(window) + .Select(button => button.Content as string) + .Where(label => !string.IsNullOrWhiteSpace(label)) + .ToHashSet(StringComparer.Ordinal); + Require(toolLabels.Contains("Draw Around"), "The freehand tool lost its plain-English name."); + Require(toolLabels.Contains("Click Around"), "The point-by-point tool lost its plain-English name."); + Require(toolLabels.Contains("Shape Cut"), "The ready-shape tool lost its plain-English name."); + Require(!toolLabels.Contains("Lasso") && !toolLabels.Contains("Polygon"), "Technical tool names returned to the visible toolbar."); + + var buttonLabels = FindLogicalChildren