Files
hcie-rust-v3.05/.kilo/plans/1784990207115-clone-tool-specification.md
T
Your Name b31a949215
mandatory-regression-gate / deterministic-tests (push) Waiting to run
mandatory-regression-gate / protected-performance-path (push) Waiting to run
feat: implement Clone Stamp tool with GUI integration
- Added Clone Stamp implementation in `clone_tool.rs` for engine API, enabling pixel sampling and painting functionality.
- Introduced a new SVG icon for the Clone Stamp tool.
- Created `clone_tool.rs` in the GUI crate to manage Clone Stamp UI state and interactions.
- Developed `clone_tool_controls.rs` to render Clone Stamp controls, including size, opacity, hardness, and mirroring options.
- Implemented error handling and source status messaging for user feedback in the Clone Stamp workflow.
2026-07-26 06:08:07 +03:00

19 KiB

Clone Tool Technical Specification and Implementation Plan

Goal

Add a raster Clone Stamp tool optimized for object removal. The user selects a merged-visible source with Ctrl+Alt+Click, then paints sampled pixels onto the active raster layer using a stable source-to-target transform.

The implementation must preserve the existing engine API boundary, partial dirty-region rendering, pooled stroke masks, selection constraints, effects/cache behavior, and one-step undo per stroke.

Confirmed Product Decisions

  • Tool name: Clone Stamp (Tool::CloneStamp).
  • Source gesture: Ctrl+Alt+Click; use Command+Option+Click on macOS.
  • Source mode: merged visible image only for the first release.
  • Snapshot lifetime: capture and freeze a full merged-visible snapshot when the source is selected; retain it until clear/reselect/document close.
  • Source state is document-local and is not persisted to settings.
  • Add an Aligned toggle:
    • On: preserve the first source-to-target transform across strokes.
    • Off: restart from the selected source anchor on every target press.
  • Mirror X and Mirror Y are independent fixed affine sampling transforms, not random per-dab flips.
  • Color Variation is deterministic per-dab subtle HSL variation; preserve source alpha.
  • Clone painting targets editable raster pixels and respects the active selection mask.
  • Initial release rejects painting while editing a grayscale layer mask.
  • Out-of-canvas source samples are transparent; destination writes are clipped to canvas bounds.
  • Bilinear source sampling uses premultiplied alpha to avoid transparent-edge color fringes.
  • Existing project rules keep hcie-egui-app/ frozen. The egui section below is implementation-ready but blocked until project governance formally removes that restriction.

User Workflow

  1. Select Clone Stamp from the Retouch tool group.
  2. Panel shows No source selected and the canvas uses an awaiting-source cursor.
  3. Ctrl+Alt+Click a valid canvas point.
  4. Engine captures a merged-visible immutable snapshot and stores the source anchor; no pixels or history are changed.
  5. Normal press on an editable raster layer establishes the target anchor and starts one clone transaction.
  6. Dragging paints source pixels according to relative displacement, mirror settings, brush coverage, opacity, pressure, selection, and color variation.
  7. Release commits one Clone Stamp history item.
  8. Clear Source removes the snapshot and blocks painting. Reselect Source arms the next normal canvas click as an accessible alternative to the modifier gesture.

Sampling Transform

Let:

  • S be the selected source anchor.
  • T be the target reference anchor.
  • P be a destination pixel.
  • M = diag(mirror_x ? -1 : 1, mirror_y ? -1 : 1).

Sample coordinate:

Q = S + M * (P - T)

This affine mapping is global for the stroke and avoids seams between overlapping dabs.

  • Aligned on: retain the first T until source selection is cleared/replaced.
  • Aligned off: set T from each new stroke press.
  • Source reselection clears retained T.

Brush Coverage and Blending

  • Size range: 1..=500 px, default 40 px.
  • Opacity range: 0..=1, default 1.0.
  • Hardness range: 0..=1, default 0.75.
  • Internal spacing: default 0.15 * diameter, clamped to at least one pixel; use interpolated dabs between pointer events.
  • Radial coverage:
    • Radius r = size * pressure / 2.
    • Fully opaque core radius hardness * r.
    • Smoothstep transition from core to edge.
    • Retain a one-pixel antialias band at hardness 1.0.
  • Effective alpha:
source_alpha * coverage * opacity * pressure * selection_alpha
  • Composite sampled RGBA over the destination using the same straight/premultiplied conventions as existing brush operations.
  • Reuse active_stroke_mask and the pre-stroke destination baseline so overlapping dabs do not exceed the requested stroke opacity.

Color Variation

  • UI exposes an enable checkbox and amount 0..=100%; default off/zero.
  • Generate one deterministic variation tuple per dab from a stroke seed and dab index.
  • At 100% amount, cap variation at approximately:
    • Hue: ±12 degrees.
    • Saturation: ±0.08.
    • Lightness: ±0.06.
  • Scale each range linearly by amount.
  • Apply the same tuple to all source pixels in one dab to avoid pixel noise.
  • Clamp HSL channels and preserve source alpha.
  • Determinism is required for repeatable tests and stable redraw behavior.

Engine and Protocol Architecture

Protocol identity

Temporarily unlock hcie-protocol, then update hcie-protocol/src/tools.rs:

  • Append Tool::CloneStamp without renumbering/reordering existing external IDs.
  • Add it to Tool::ALL, label, icon, is_raster, is_raster_compatible, allowed_layer_type, shows_pen_tips, has_size, and has_opacity.
  • Classify it as Raster-only.
  • Update exhaustive tool matches and serialization tests.
  • Add a new stable FFI numeric mapping at the end of hcie-engine-api/src/ffi.rs; never shift existing IDs.

Do not add GUI dependencies on hcie-protocol; continue importing Tool only through hcie-engine-api.

New engine API module

Temporarily unlock hcie-engine-api and add hcie-engine-api/src/clone_tool.rs.

Public API-visible types:

pub struct CloneConfig {
    pub size: f32,
    pub opacity: f32,
    pub hardness: f32,
    pub aligned: bool,
    pub mirror_x: bool,
    pub mirror_y: bool,
    pub color_variation: f32,
}

pub struct CloneSourceInfo {
    pub x: f32,
    pub y: f32,
    pub width: u32,
    pub height: u32,
    pub has_aligned_target: bool,
}

pub enum CloneError {
    SourceNotSelected,
    SourceOutOfBounds,
    DestinationNotEditable,
    LayerMaskEditingUnsupported,
    StrokeAlreadyActive,
    StrokeNotActive,
    DimensionMismatch,
}

Expose methods on Engine:

set_clone_config(config) -> CloneConfig
clone_config() -> CloneConfig
set_clone_source(x, y) -> Result<CloneSourceInfo, CloneError>
clear_clone_source()
clone_source_info() -> Option<CloneSourceInfo>
begin_clone_stroke(x, y, pressure) -> Result<(), CloneError>
clone_stroke_to(x, y, pressure) -> Result<(), CloneError>
end_clone_stroke() -> Result<(), CloneError>
cancel_clone_stroke()

Clamp all incoming configuration values inside the API.

Engine-owned state

Add clone state to Engine, preferably grouped in a private CloneState:

  • config.
  • source_anchor.
  • immutable merged RGBA snapshot and dimensions.
  • optional aligned target anchor.
  • active stroke target anchor.
  • previous destination point and spacing accumulator.
  • deterministic stroke seed and dab index.

Memory note: a 4K snapshot costs about 33 MB per document with an active clone source. Free it on clear, document close, canvas resize/crop, or incompatible document replacement.

Source capture

set_clone_source must:

  1. Validate finite in-bounds coordinates.
  2. Produce a current merged-visible image including visible layers, masks, clipping, opacity, blend modes, and effects.
  3. Capture without consuming or losing pending GUI dirty-region state. Add/use a non-consuming internal composite snapshot path; do not call a public full-composite getter if it clears dirty state without restoring it.
  4. Store immutable pixels and source anchor.
  5. Clear any retained aligned target anchor.
  6. Return source info without creating history or marking the document modified.

Clone stroke lifecycle

begin_clone_stroke must:

  1. Require a source snapshot.
  2. Reject layer-mask editing and non-editable/non-raster destinations.
  3. Resolve/reset target anchor according to aligned.
  4. Enter the existing pooled stroke lifecycle: effect suspension, selection-mask cache, destination before-buffer/history capture, active stroke mask, below-layer cache.
  5. Mark history kind/name as Clone Stamp rather than Brush Stroke (...).
  6. Paint the initial dab.

clone_stroke_to must:

  1. Interpolate dabs using accumulated spacing.
  2. Iterate only each destination dab rectangle.
  3. Compute affine source coordinates per destination pixel.
  4. Bilinearly sample premultiplied RGBA from the frozen source snapshot.
  5. Apply deterministic HSL variation per dab.
  6. Apply coverage, pressure, opacity, selection mask, and pooled stroke-opacity cap.
  7. Union exact destination dirty bounds and history bounds.
  8. Invalidate effects/composite/tile regions using the same optimized path as normal raster strokes.

end_clone_stroke must delegate to the established asynchronous sub-rectangle history flow and retain the source snapshot for subsequent strokes.

cancel_clone_stroke must restore the pre-stroke destination and leave no history entry.

History extension

  • Extend active-stroke metadata with an explicit history label or stroke kind.
  • Existing brush behavior defaults to its current label.
  • Clone produces exactly one Clone Stamp item per completed press-drag-release.
  • Source selection, clear, and reselect produce no history entries.

Iced UI Architecture

Feature modules

Following the new-feature/new-file preference, add:

  • hcie-iced-app/crates/hcie-iced-gui/src/clone_tool.rs: GUI presentation/state helpers and engine-config conversion; no pixel mutation.
  • hcie-iced-app/crates/hcie-iced-gui/src/panels/clone_tool_controls.rs: shared Clone Tool panel controls.
  • Register the panel helper in panels/mod.rs.

Persistent versus transient state

Persist in settings.rs::ToolSettings, each with #[serde(default)] or explicit default functions:

  • clone size, opacity, hardness.
  • aligned.
  • mirror X/Y.
  • color variation enabled and amount.

Do not persist source coordinates, target anchor, source snapshot, or active-stroke state. Those remain inside each document's Engine.

Messages

Add dedicated messages, not shared brush messages:

CloneSizeChanged
CloneOpacityChanged
CloneHardnessChanged
CloneAlignedToggled
CloneMirrorXToggled
CloneMirrorYToggled
CloneColorVariationToggled
CloneColorVariationChanged
CloneClearSource
CloneArmSourceSelection
CloneSourceSelected / CloneSourceSelectionFailed

Each parameter message updates settings, saves, and sends a complete clamped CloneConfig to the active document engine. On tab switch, apply persisted config to that document while retaining its document-local source.

Shared panel component

clone_tool_controls::view(...) -> Element<Message> renders:

  1. Object Removal instruction: Ctrl+Alt+Click a clean area, then paint over the object.
  2. Source status banner:
    • No source selected.
    • Select a source on the canvas.
    • Source: x, y (Merged Visible).
  3. Choose/Reselect Source and Clear Source buttons.
  4. Size slider.
  5. Opacity slider.
  6. Hardness slider with edge-softness tooltip.
  7. Aligned checkbox.
  8. Mirror X and Mirror Y checkboxes on one row.
  9. Color Variation checkbox and conditional amount slider.

Reuse this component from both active surfaces:

  • panels/tool_settings.rs as the canonical configuration panel.
  • panels/properties.rs for contextual parity.

Keep panels/tool_options.rs out of scope because its _view() is unused. Update active panels/toolbar.rs only to expose Clone size/opacity if it already shows shared raster controls.

Toolbox/menu/shortcut

  • Add CloneStamp to the Retouch TOOL_SLOTS group in sidebar/mod.rs.
  • Add a dedicated clone-stamp SVG icon and exhaustive icon mapping.
  • Add Clone Stamp to the Tools menu near Spot Removal.
  • Assign S if the final shortcut audit confirms it is unclaimed for tool selection; otherwise use Shift+S and display the actual shortcut in the sidebar tooltip.
  • Add it to raster cursor/footprint and 60 Hz canvas-frame scheduling matches.

Input routing

  • Carry a modifier snapshot with CanvasPointerPressed (or equivalent canvas event) instead of relying solely on separately ordered global modifier messages.
  • On Clone Stamp, process Ctrl+Alt/Command+Option before generic Ctrl color picking.
  • Source-selection press:
    • call set_clone_source;
    • do not enter drawing state;
    • refresh source status and overlay.
  • Armed source selection consumes the next normal left press, then disarms.
  • Normal press calls begin_clone_stroke; move calls clone_stroke_to; release calls end_clone_stroke and existing pending-history polling.
  • If no source exists, consume normal paint presses and show a non-modal status message rather than silently doing nothing.

Canvas overlay

Extend the existing overlay Canvas in canvas/mod.rs; do not touch protected shader texture upload, viewport, nearest filtering, or dirty accumulator code.

Render:

  • destination brush footprint using clone size/hardness.
  • fixed selected-source marker.
  • while hovering/painting, current sampled-source marker derived from the affine transform.
  • optional thin line from sampled source to destination cursor while painting.
  • distinct awaiting-source cursor/status.

Clip overlay geometry to canvas bounds and reuse existing canvas-to-screen transforms.

egui Component Architecture (Conditionally Blocked)

This phase may begin only after the project instruction declaring hcie-egui-app/ frozen is formally changed. Until then, treat these files as read-only.

When unblocked:

  • Add clone configuration fields to legacy ToolConfigs in app/tool_state.rs, mirroring CloneConfig defaults.
  • Add CloneStamp to legacy TOOL_SLOTS, toolbox icon mapping, menu, and shortcut routing.
  • Add an immediate-mode reusable component:
fn show_clone_tool_controls(
    ui: &mut egui::Ui,
    config: &mut CloneConfig,
    source: Option<&CloneSourceInfo>,
) -> ClonePanelAction
  • Use legacy PlainSlider, ui.checkbox, source-status labels, and ui.horizontal action buttons.
  • Return actions such as None, ConfigChanged, ArmSource, and ClearSource; application code invokes only hcie-engine-api methods.
  • In the egui canvas event path, test Ctrl+Alt before color-pick/pan branches, then route press/move/release to the same engine API.
  • Draw source/target markers in the existing canvas overlay paint pass.
  • Do not duplicate clone pixel logic in egui.

Failure Modes and UX

  • No source: block stroke and display source-selection instruction.
  • Source outside canvas/non-finite: return error; retain prior valid source.
  • Canvas resize/crop/document reload: clear source snapshot and aligned anchor.
  • Active target is locked, hidden as non-editable, or non-raster: block with status/toast.
  • Editing layer mask: block with explicit unsupported message.
  • Source sample outside snapshot: transparent sample; never wrap.
  • Undo/redo during active clone stroke: cancel stroke first, then perform history action.
  • Tool/document switch during active stroke: end or cancel according to existing pointer-capture policy; never leave effects suspended.
  • Source remains valid if original source layers later change because snapshot lifetime is explicitly frozen until reselect.

Ordered Implementation Plan

  1. Add protocol CloneStamp identity and update all exhaustive metadata/classification/FFI mappings; rebuild and re-lock protocol.
  2. Add engine API clone types, state, clamping, source capture, query, and clear operations in a new module.
  3. Implement premultiplied bilinear sampling, affine mirror mapping, radial hardness coverage, deterministic HSL variation, and destination blending as private tested helpers.
  4. Implement clone begin/move/end/cancel using existing pooled stroke, selection, dirty-region, effects, tile, below-cache, and asynchronous history machinery.
  5. Extend history labeling without altering ordinary brush labels.
  6. Add engine/API tests before GUI integration.
  7. Add Iced clone GUI helper/state module, dedicated messages, persisted serde-safe settings, and per-document source querying.
  8. Add shared Iced controls to Tool Settings and Properties; update toolbar where appropriate.
  9. Add Iced toolbox/menu/icon/shortcut and modifier-snapshot input routing.
  10. Add Iced clone source/target overlays and frame scheduling without changing shader upload behavior.
  11. Capture Iced screenshots of Tool Settings, Properties, awaiting-source state, selected-source marker, and active clone stroke.
  12. Run full validation and re-lock engine/API crates.
  13. Conditional only after governance change: implement the egui adapter/component/input/overlay phase against the same API and capture equivalent screenshots.

Validation Plan

Engine/API unit tests

  • Reject painting before source selection.
  • Source selection creates no history and does not modify/dirty the document.
  • Merged snapshot includes visible layer composition/effects/masks.
  • Frozen snapshot does not self-feed when source and target overlap.
  • Identity, Mirror X, Mirror Y, and Mirror XY affine mappings.
  • Aligned offset persists; unaligned target resets per stroke.
  • Bilinear sampling correctness including transparent edges.
  • Hardness 0, intermediate, and 1; opacity 0 and 1; pressure boundaries.
  • Selection mask constrains destination pixels.
  • Deterministic bounded HSL variation preserves alpha.
  • Out-of-source-bounds samples are transparent and never panic.
  • One history item per completed stroke; cancel creates none; undo/redo restores exact pixels.
  • Dirty/history rectangles include all modified destination pixels only.
  • Source clear/resize invalidation frees state.

Integration and regression

  • cargo test -p hcie-engine-api --test visual_regression with new clone golden cases.
  • cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture.
  • Add a 4K clone-stroke benchmark that asserts no per-pointer full-canvas source copy/allocation.
  • Run protocol/engine API/active Iced workspace tests.
  • Test old settings JSON with all clone fields absent.
  • Test two documents with independent source anchors/snapshots.
  • Verify Ctrl alone still color-picks for existing paint tools and Ctrl+Alt is consumed by Clone Stamp first.
  • Verify protected partial uploads, viewport mapping, nearest filtering, and dirty-bounds union remain unchanged.

Visual acceptance

  • Clone panel controls fit narrow and wide docks without truncating essential status.
  • Hardness visibly changes edge softness, not sampled detail position.
  • Source and current-sample markers remain aligned during zoom/pan.
  • Mirrored strokes do not show per-dab seams.
  • Repeated object-removal strokes do not introduce source feedback or obvious periodic color noise.

Rollout and Compatibility

  • New settings fields must deserialize from old files via defaults.
  • Append tool/FFI identities; never renumber existing values.
  • No persisted source migration is needed.
  • Keep clone functionality behind the new tool path; ordinary brush behavior remains unchanged.
  • If the engine API is distributed as a static library, rebuild it before the GUI workspace.
  • Use unlock.sh <crate> and lock.sh <crate> for each locked crate and retain all protected performance mechanisms.

Explicitly Out of Scope for Initial Release

  • Healing/texture synthesis or Poisson blending.
  • Current-layer/current-and-below sampling modes.
  • Rotation/scale transforms beyond X/Y mirroring.
  • Random mirror switching.
  • Layer-mask cloning.
  • Script/AI clone commands unless required later for automation parity.
  • Editing the frozen egui workspace before its project restriction is formally removed.