- 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.
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; useCommand+Option+Clickon 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
- Select Clone Stamp from the Retouch tool group.
- Panel shows
No source selectedand the canvas uses an awaiting-source cursor. Ctrl+Alt+Clicka valid canvas point.- Engine captures a merged-visible immutable snapshot and stores the source anchor; no pixels or history are changed.
- Normal press on an editable raster layer establishes the target anchor and starts one clone transaction.
- Dragging paints source pixels according to relative displacement, mirror settings, brush coverage, opacity, pressure, selection, and color variation.
- Release commits one
Clone Stamphistory item. - 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:
Sbe the selected source anchor.Tbe the target reference anchor.Pbe 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
Tuntil source selection is cleared/replaced. - Aligned off: set
Tfrom each new stroke press. - Source reselection clears retained
T.
Brush Coverage and Blending
- Size range:
1..=500 px, default40 px. - Opacity range:
0..=1, default1.0. - Hardness range:
0..=1, default0.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.
- Radius
- 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_maskand 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.
- Hue:
- 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::CloneStampwithout 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, andhas_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:
- Validate finite in-bounds coordinates.
- Produce a current merged-visible image including visible layers, masks, clipping, opacity, blend modes, and effects.
- 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.
- Store immutable pixels and source anchor.
- Clear any retained aligned target anchor.
- Return source info without creating history or marking the document modified.
Clone stroke lifecycle
begin_clone_stroke must:
- Require a source snapshot.
- Reject layer-mask editing and non-editable/non-raster destinations.
- Resolve/reset target anchor according to
aligned. - Enter the existing pooled stroke lifecycle: effect suspension, selection-mask cache, destination before-buffer/history capture, active stroke mask, below-layer cache.
- Mark history kind/name as
Clone Stamprather thanBrush Stroke (...). - Paint the initial dab.
clone_stroke_to must:
- Interpolate dabs using accumulated spacing.
- Iterate only each destination dab rectangle.
- Compute affine source coordinates per destination pixel.
- Bilinearly sample premultiplied RGBA from the frozen source snapshot.
- Apply deterministic HSL variation per dab.
- Apply coverage, pressure, opacity, selection mask, and pooled stroke-opacity cap.
- Union exact destination dirty bounds and history bounds.
- 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 Stampitem 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:
- Object Removal instruction:
Ctrl+Alt+Click a clean area, then paint over the object. - Source status banner:
No source selected.Select a source on the canvas.Source: x, y (Merged Visible).
- Choose/Reselect Source and Clear Source buttons.
- Size slider.
- Opacity slider.
- Hardness slider with edge-softness tooltip.
- Aligned checkbox.
- Mirror X and Mirror Y checkboxes on one row.
- Color Variation checkbox and conditional amount slider.
Reuse this component from both active surfaces:
panels/tool_settings.rsas the canonical configuration panel.panels/properties.rsfor 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
CloneStampto the RetouchTOOL_SLOTSgroup insidebar/mod.rs. - Add a dedicated clone-stamp SVG icon and exhaustive icon mapping.
- Add Clone Stamp to the Tools menu near Spot Removal.
- Assign
Sif the final shortcut audit confirms it is unclaimed for tool selection; otherwise useShift+Sand 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+Optionbefore generic Ctrl color picking. - Source-selection press:
- call
set_clone_source; - do not enter drawing state;
- refresh source status and overlay.
- call
- Armed source selection consumes the next normal left press, then disarms.
- Normal press calls
begin_clone_stroke; move callsclone_stroke_to; release callsend_clone_strokeand 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
ToolConfigsinapp/tool_state.rs, mirroringCloneConfigdefaults. - Add
CloneStampto legacyTOOL_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, andui.horizontalaction buttons. - Return actions such as
None,ConfigChanged,ArmSource, andClearSource; application code invokes onlyhcie-engine-apimethods. - 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
- Add protocol
CloneStampidentity and update all exhaustive metadata/classification/FFI mappings; rebuild and re-lock protocol. - Add engine API clone types, state, clamping, source capture, query, and clear operations in a new module.
- Implement premultiplied bilinear sampling, affine mirror mapping, radial hardness coverage, deterministic HSL variation, and destination blending as private tested helpers.
- Implement clone begin/move/end/cancel using existing pooled stroke, selection, dirty-region, effects, tile, below-cache, and asynchronous history machinery.
- Extend history labeling without altering ordinary brush labels.
- Add engine/API tests before GUI integration.
- Add Iced clone GUI helper/state module, dedicated messages, persisted serde-safe settings, and per-document source querying.
- Add shared Iced controls to Tool Settings and Properties; update toolbar where appropriate.
- Add Iced toolbox/menu/icon/shortcut and modifier-snapshot input routing.
- Add Iced clone source/target overlays and frame scheduling without changing shader upload behavior.
- Capture Iced screenshots of Tool Settings, Properties, awaiting-source state, selected-source marker, and active clone stroke.
- Run full validation and re-lock engine/API crates.
- 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, and1; opacity0and1; 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_regressionwith 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>andlock.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.