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
//! Image rescale (Photoshop "Image Size").
//!
//! Unlike [`canvas_resize`](super::canvas_resize), which only moves the canvas
//! window, this **resamples every pixel-bearing node** (raster layers + mask
//! filters) to new document dimensions, scaling content about the canvas
//! origin. It is lossy and undoable: each node's pre-rescale pixels are
//! snapshotted, and the dims + per-node extents ride one [`ImageRescaleAction`]
//! (which also folds in the selection clear). The GPU resampling lives in
//! `Compositor::rescale_nodes`; the undo restore-across-extent-change lives in
//! `apply_undo` (`engine/rendering.rs`).
use super::canvas_resize::MAX_CANVAS_DIM;
use super::DarklyEngine;
use crate::coord::CanvasRect;
use crate::gpu::compositor::scaled_extent_about;
use crate::gpu::region_store::UndoRegionEntry;
use crate::layer::LayerId;
use crate::undo::CanvasGeometryAction;
impl DarklyEngine {
/// Resample all layer + mask pixels to `(new_width, new_height)`, scaling
/// content about the canvas origin (which stays fixed). Lossy + undoable.
/// No-ops on a zero/over-limit target, a no-op size, or if any node would
/// exceed the max texture dimension after scaling. Clears the active
/// selection (folded into the same undo step).
pub fn rescale_image(&mut self, new_width: u32, new_height: u32) {
self.auto_commit_floating();
let (old_w, old_h) = (self.doc.width, self.doc.height);
if new_width == 0
|| new_height == 0
|| new_width > MAX_CANVAS_DIM
|| new_height > MAX_CANVAS_DIM
|| (new_width, new_height) == (old_w, old_h)
{
return;
}
let sx = new_width as f32 / old_w as f32;
let sy = new_height as f32 / old_h as f32;
let origin = self.doc.canvas_origin;
// Enumerate pixel-bearing nodes by capability: raster layers + mask
// filters. Void layers answer `pixels() == None` and are skipped
// (they regenerate to fill the new canvas).
let mut candidate_ids: Vec<LayerId> = Vec::new();
for l in self.doc.all_content_layers() {
if l.pixels().is_some() {
candidate_ids.push(l.id());
}
}
for m in self.doc.all_filters() {
if m.pixels().is_some() {
candidate_ids.push(m.id);
}
}
// Keep only nodes that own a GPU texture in the unified pool. The
// selection filter's pixels live in the compositor's selection_state
// (window-sized), not node_textures — it's cleared separately below.
let mut nodes: Vec<(LayerId, CanvasRect, wgpu::TextureFormat)> = Vec::new();
for id in candidate_ids {
if let Some(t) = self.compositor.node_texture(id) {
nodes.push((id, t.canvas_extent(), t.format()));
}
}
// Refuse if any node would exceed the max texture dimension once scaled.
for (_, old_extent, _) in &nodes {
let ne = scaled_extent_about(*old_extent, origin, sx, sy);
if ne.width > MAX_CANVAS_DIM || ne.height > MAX_CANVAS_DIM {
return;
}
}
// 1. Snapshot each node's current pixels (old direction) for undo.
let mut regions: Vec<UndoRegionEntry> = Vec::with_capacity(nodes.len());
for (id, old_extent, format) in &nodes {
let entry = self
.snapshot_region_entry(*id, *old_extent, *format, "rescale-save", "rescale-commit")
.expect("node texture present");
regions.push(entry);
}
// 2. Clear the active selection while dims are still old (the selection
// texture is window-sized) — folded into this single undo step.
let selection = self.clear_selection_collecting_undo();
// 3. Resample all node textures on the GPU.
let ids: Vec<LayerId> = nodes.iter().map(|(id, _, _)| *id).collect();
self.gpu.encode("rescale-nodes", |enc| {
self.compositor
.rescale_nodes(&self.gpu.device, &self.gpu.queue, enc, &ids, sx, sy);
});
// 4. Mirror the resampled extents into the document + record bounds.
let mut bounds: Vec<(LayerId, CanvasRect, CanvasRect)> = Vec::with_capacity(nodes.len());
for (id, old_extent, _) in &nodes {
let new_extent = self
.compositor
.node_texture(*id)
.map(|t| t.canvas_extent())
.unwrap_or(*old_extent);
self.doc.set_node_pixel_bounds(*id, new_extent);
bounds.push((*id, *old_extent, new_extent));
}
// 5. Update canvas dimensions (origin fixed) + push the undo step.
self.doc.width = new_width;
self.doc.height = new_height;
self.push_undo(Box::new(CanvasGeometryAction::new(
(old_w, old_h),
(new_width, new_height),
origin,
origin,
bounds,
regions,
selection,
)));
// 6. Reconcile window-sized GPU resources + present view to new dims.
self.apply_canvas_rect_to_compositor();
self.compositor.mark_dirty();
}
}