1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! Layer / selection flip (horizontal / vertical) — exact mirror.
//!
//! Mirrors the active node's pixels in place about a region centre, via the
//! same exact ortho primitive as canvas flip/rotate (`gpu/ortho_transform`).
//! With an active selection only the selected region flips — about the
//! selection's bounding-box centre, masked to the selection *shape* (Krita's
//! `KisMirrorProcessingVisitor`); with none, the whole layer flips about its
//! own extent centre. Horizontal/vertical only (no layer rotate), so the node
//! extent never changes and undo is a single [`GpuRegionAction`].
use super::rendering::commit_undo_region;
use super::{DarklyEngine, PendingFlip};
use crate::coord::WindowRect;
use crate::gpu::ortho_transform::OrthoXform;
use crate::layer::LayerId;
use crate::undo::GpuRegionAction;
impl DarklyEngine {
/// Flip the given node (raster layer or mask filter) horizontally or
/// vertically. Returns `false` if the node isn't editable, has no texture,
/// or the flip was deferred waiting on the selection cache.
pub fn flip_node(&mut self, node_id: LayerId, xform: OrthoXform) -> bool {
debug_assert!(matches!(xform, OrthoXform::FlipH | OrthoXform::FlipV));
if !self.doc.is_node_editable(node_id) {
return false;
}
self.auto_commit_floating();
if self.doc.layer(node_id).is_none() && self.doc.find_filter(node_id).is_none() {
return false;
}
let (node_extent, format) = match self.compositor.node_texture(node_id) {
Some(t) => (t.canvas_extent(), t.format()),
None => return false,
};
// Region to mirror + (for a selection) the mask cropped to it.
let (region, mask_tex) = if self.has_selection() {
// Selection bbox (window-local) — recomputed from the cpu cache if
// the readback hasn't populated bounds yet, else deferred.
let bounds = match self.selection_pixel_bounds() {
Some(b) => b,
None => {
let recomputed = self.selection_cpu_cache().and_then(|d| {
crate::mask::pixel_bounds_r8(d, self.doc.width, self.doc.height)
.map(|[x, y, w, h]| WindowRect::from_xywh(x as i32, y as i32, w, h))
});
match recomputed {
Some(b) => {
self.set_selection_pixel_bounds(Some(b));
b
}
None => {
self.pending_flip = Some(PendingFlip { node_id, xform });
self.kick_selection_readback();
return false;
}
}
}
};
if bounds.width == 0 || bounds.height == 0 {
return false;
}
// Window-local bbox → plane, clipped to the node extent (keeps the
// mirror in-place; the pivot is the clipped region's centre).
let region_plane = bounds.to_canvas(self.doc.canvas_origin);
let region = match node_extent.intersect(region_plane) {
Some(r) if r.width > 0 && r.height > 0 => r,
_ => return false,
};
let win_origin = (
region.origin.x - self.doc.canvas_origin.x,
region.origin.y - self.doc.canvas_origin.y,
);
let mask_tex =
self.upload_cropped_selection_texture(win_origin, region.width, region.height);
(region, mask_tex)
} else {
(node_extent, None)
};
// Snapshot the affected region for a single-step undo.
let frame = self
.compositor
.node_texture(node_id)
.expect("checked above")
.canvas_frame();
let snap = self.gpu.encode_ret("flip-node-save", |enc| {
self.region_scratch
.save_region(&self.gpu.device, enc, &frame, format, region)
});
let entry = commit_undo_region(
&self.gpu,
&self.region_scratch,
&mut self.readbacks,
"flip-node-commit",
node_id,
&frame,
&snap,
region,
);
// Permute the region in place, masked to the selection shape when present.
let mask_view = mask_tex
.as_ref()
.map(|t| t.create_view(&wgpu::TextureViewDescriptor::default()));
self.gpu.encode("flip-node", |enc| {
self.compositor.flip_node_region(
&self.gpu.device,
&self.gpu.queue,
enc,
node_id,
region,
xform,
mask_view.as_ref(),
);
});
self.push_undo(Box::new(GpuRegionAction::new(entry)));
self.compositor.mark_dirty();
true
}
}