CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
viewport_manager.cpp
Go to the documentation of this file.
1
9#include <spdlog/spdlog.h>
10#include <algorithm>
11#include <cmath>
12
14
15// =============================================================================
16// Construction
17// =============================================================================
18
20 : QObject(parent)
21{
22 spdlog::debug("[ViewportManager::ViewportManager]: Created");
23}
24
25// =============================================================================
26// Initialization
27// =============================================================================
28
29void ViewportManager::initialize(const QSize& viewport_logical, const ScreenInfo& screen)
30{
31 // =========================================================================
32 // Step 1: Validate inputs
33 // =========================================================================
34
35 if (!viewport_logical.isValid() || viewport_logical.isEmpty()) {
36 spdlog::error("[ViewportManager::initialize]: Invalid viewport size");
37 return;
38 }
39
40 if (!screen.isValid()) {
41 spdlog::error("[ViewportManager::initialize]: Invalid screen info");
42 return;
43 }
44
45 // =========================================================================
46 // Step 2: Store parameters
47 // =========================================================================
48
49 m_viewport_logical = viewport_logical;
50 m_screen = screen;
51
52 // =========================================================================
53 // Step 3: Calculate viewport physical size
54 // =========================================================================
55 // Physical size = logical size × DPR
56 // This is the actual number of pixels Qt will render
57
58 m_viewport_physical = QSize(
59 static_cast<int>(std::round(viewport_logical.width() * screen.dpr)),
60 static_cast<int>(std::round(viewport_logical.height() * screen.dpr))
61 );
62
63 // =========================================================================
64 // Step 4: Determine plafond based on screen physical size
65 // =========================================================================
66 // Screens >= 2000px use 3K plafond
67 // Screens < 2000px use 2K plafond
68
69 m_plafond = determinePlafond(screen.physical_width, screen.physical_height);
70
71 // =========================================================================
72 // Step 5: Calculate maximum downsample dimension
73 // =========================================================================
74
75 updateMaxDownsample();
76
77 // =========================================================================
78 // Step 6: Mark as initialized
79 // =========================================================================
80
81 m_initialized = true;
82
83 // =========================================================================
84 // Step 7: Log configuration
85 // =========================================================================
86
87 spdlog::info("[ViewportManager::initialize]: Initialized:");
88 spdlog::info("[ViewportManager::initialize]: Screen: {}x{}, DPR: {:.2f}",
89 m_screen.physical_width, m_screen.physical_height, m_screen.dpr);
90 spdlog::info("[ViewportManager::initialize]: Viewport: {}x{} (physical: {}x{})",
91 m_viewport_logical.width(), m_viewport_logical.height(),
92 m_viewport_physical.width(), m_viewport_physical.height());
93 spdlog::info("[ViewportManager::initialize]: Plafond: {}px, Max downsample: {}px",
94 m_plafond, m_max_downsample);
95
96 emit viewportSizeChanged(m_viewport_logical);
97}
98
99void ViewportManager::setViewportSize(const QSize& viewport_logical)
100{
101 if (!viewport_logical.isValid() || viewport_logical.isEmpty()) {
102 spdlog::warn("[ViewportManager::setViewportSize]: Invalid viewport size");
103 return;
104 }
105
106 if (m_viewport_logical == viewport_logical) {
107 return;
108 }
109
110 m_viewport_logical = viewport_logical;
111 m_viewport_physical = QSize(
112 static_cast<int>(std::round(viewport_logical.width() * m_screen.dpr)),
113 static_cast<int>(std::round(viewport_logical.height() * m_screen.dpr))
114 );
115
116 updateMaxDownsample();
117
118 spdlog::debug("[ViewportManager::setViewportSize]: Viewport resized: {}x{}",
119 m_viewport_logical.width(), m_viewport_logical.height());
120
121 emit viewportSizeChanged(m_viewport_logical);
122}
123
125{
126 m_quality_margin = std::clamp(margin, DisplayConfig::minQualityMargin(), DisplayConfig::maxQualityMargin());
127
128 if (m_initialized) {
129 updateMaxDownsample();
130 }
131
132 spdlog::debug("[ViewportManager::setQualityMargin]: Quality margin: {:.2f}", m_quality_margin);
133}
134
136{
137 m_max_downsample = std::max(max_dim, DisplayConfig::minDisplayDimension());
138 spdlog::info("[ViewportManager::setMaxDownsample]: Max downsample set: {}px", m_max_downsample);
139 emit maxDownsampleChanged(m_max_downsample);
140}
141
142// =============================================================================
143// Calculations
144// =============================================================================
145
147{
148 ViewportCalculation result;
149
150 // =========================================================================
151 // Step 1: Validate input
152 // =========================================================================
153
154 if (!source_size.isValid() || source_size.isEmpty()) {
155 spdlog::warn("[ViewportManager::calculateDisplay]: Invalid source size");
156 return result;
157 }
158
159 if (!m_initialized) {
160 spdlog::warn("[ViewportManager::calculateDisplay]: Not initialized");
161 return result;
162 }
163
164 const int src_w = source_size.width();
165 const int src_h = source_size.height();
166
167 // =========================================================================
168 // Step 2: Calculate fit-to-view zoom
169 // =========================================================================
170 // This is the zoom that makes the entire image visible in viewport
171 // Always <= 1.0 (never upscale)
172
173 result.fit_zoom = calculateFitZoom(source_size);
174
175 // =========================================================================
176 // Step 3: Calculate logical display size
177 // =========================================================================
178 // This is what the user sees in QML coordinates
179
180 result.display_size = QSize(
181 static_cast<int>(std::round(src_w * result.fit_zoom)),
182 static_cast<int>(std::round(src_h * result.fit_zoom))
183 );
184
185 // =========================================================================
186 // Step 4: Calculate downsample size
187 // =========================================================================
188 // Apply golden rule: NEVER UPSCALE
189 // downsample_size = min(source_size, max_downsample)
190
191 result.downsample_size = calculateDownsampleSize(source_size);
192
193 // =========================================================================
194 // Step 5: Determine if downsample is needed
195 // =========================================================================
196
197 result.needs_downsample = (result.downsample_size != source_size);
198
199 // =========================================================================
200 // Step 6: Calculate GPU memory usage
201 // =========================================================================
202
204
205 // =========================================================================
206 // Step 7: Log result (debug only)
207 // =========================================================================
208
209 spdlog::debug("[ViewportManager::calculateDisplay]: Display calc: source={}x{}, display={}x{}, downsample={}x{}, fit_zoom={:.3f}",
210 src_w, src_h,
211 result.display_size.width(), result.display_size.height(),
212 result.downsample_size.width(), result.downsample_size.height(),
213 result.fit_zoom);
214
215 return result;
216}
217
218float ViewportManager::calculateFitZoom(const QSize& source_size) const noexcept
219{
220 // =========================================================================
221 // Validate inputs
222 // =========================================================================
223
224 if (!source_size.isValid() || source_size.isEmpty()) {
225 return 1.0f;
226 }
227
228 if (!m_viewport_logical.isValid() || m_viewport_logical.isEmpty()) {
229 return 1.0f;
230 }
231
232 // =========================================================================
233 // Calculate scale factors for both dimensions
234 // =========================================================================
235
236 const float scale_x { static_cast<float>(m_viewport_logical.width()) /
237 static_cast<float>(source_size.width()) };
238 const float scale_y { static_cast<float>(m_viewport_logical.height()) /
239 static_cast<float>(source_size.height()) };
240
241 // =========================================================================
242 // Use the smaller scale to ensure image fits entirely
243 // =========================================================================
244
245 float fit_zoom { std::min(scale_x, scale_y) };
246
247 // =========================================================================
248 // Never upscale beyond 100%
249 // =========================================================================
250
251 return std::min(fit_zoom, 1.0f);
252}
253
254QSize ViewportManager::calculateDownsampleSize(const QSize& source_size) const
255{
256 // =========================================================================
257 // Validate input
258 // =========================================================================
259
260 if (!source_size.isValid() || source_size.isEmpty()) {
261 return {};
262 }
263
264 const int src_w { source_size.width() };
265 const int src_h { source_size.height() };
266
267 // =========================================================================
268 // GOLDEN RULE: Never upscale
269 // =========================================================================
270 // If source is smaller than max, return source unchanged
271
272 if (src_w <= m_max_downsample && src_h <= m_max_downsample) {
273 spdlog::trace("[ViewportManager::calculateDownsampleSize]: No downsample needed (source {}x{} <= max {})",
274 src_w, src_h, m_max_downsample);
275 return source_size;
276 }
277
278 // =========================================================================
279 // Calculate proportional scale
280 // =========================================================================
281 // Scale the larger dimension to max_downsample
282
283 const float scale { static_cast<float>(m_max_downsample) /
284 static_cast<float>(std::max(src_w, src_h)) };
285
286 const int down_w { static_cast<int>(std::round(src_w * scale)) };
287 const int down_h { static_cast<int>(std::round(src_h * scale)) };
288
289 return QSize(down_w, down_h);
290}
291
292ViewportState ViewportManager::calculateFitToView(const QSize& display_size) const noexcept
293{
294 ViewportState state;
295
296 // =========================================================================
297 // Validate inputs
298 // =========================================================================
299
300 if (!display_size.isValid() || !m_viewport_logical.isValid()) {
301 return state;
302 }
303
304 // =========================================================================
305 // Set zoom to 100% (fit-to-view means no zoom)
306 // =========================================================================
307
308 state.zoom = 1.0f;
309
310 // =========================================================================
311 // Calculate pan offset to center image in viewport
312 // =========================================================================
313
314 state.center_offset = QPointF(
315 (m_viewport_logical.width() - display_size.width()) / 2.0f,
316 (m_viewport_logical.height() - display_size.height()) / 2.0f
317 );
318
319 state.pan = state.center_offset;
320
321 return state;
322}
323
324// =============================================================================
325// Internal Methods
326// =============================================================================
327
328void ViewportManager::updateMaxDownsample()
329{
330 // =========================================================================
331 // Calculate max from viewport physical size with quality margin
332 // =========================================================================
333
334 const int viewport_max { std::max(m_viewport_physical.width(),
335 m_viewport_physical.height()) };
336 const int with_margin { static_cast<int>(std::round(viewport_max * m_quality_margin)) };
337
338 // =========================================================================
339 // Apply plafond (screen-based ceiling)
340 // =========================================================================
341 // max_downsample = min(viewport_physical × margin, plafond)
342
343 m_max_downsample = std::min(with_margin, m_plafond);
344
345 // =========================================================================
346 // Ensure minimum size
347 // =========================================================================
348
349 m_max_downsample = std::max(m_max_downsample, DisplayConfig::minDisplayDimension());
350
351 spdlog::debug("[ViewportManager::updateMaxDownsample]: Max downsample: {}px (viewport={}, plafond={})",
352 m_max_downsample, with_margin, m_plafond);
353
354 emit maxDownsampleChanged(m_max_downsample);
355}
356
357} // namespace CaptureMoment::UI::Display
QSize calculateDownsampleSize(const QSize &source_size) const
Calculates the downsample size for a source image.
void setMaxDownsample(int max_dim)
Sets the maximum downsample dimension manually.
float calculateFitZoom(const QSize &source_size) const noexcept
Calculates the zoom factor to fit an image in the viewport.
void setQualityMargin(float margin)
Sets the quality margin for zoom headroom.
void initialize(const QSize &viewport_logical, const ScreenInfo &screen)
Initializes the manager with screen and viewport parameters.
ViewportCalculation calculateDisplay(const QSize &source_size) const
Calculates optimal display parameters for a source image.
void viewportSizeChanged(const QSize &size)
ViewportState calculateFitToView(const QSize &display_size) const noexcept
Calculates viewport state for fit-to-view.
void setViewportSize(const QSize &viewport_logical)
Updates the viewport size.
Namespace containing Qt-specific UI components for CaptureMoment.
int determinePlafond(int screen_physical_width, int screen_physical_height) noexcept
Determines the plafond based on physical screen size.
size_t calculateTextureMemoryMB(const QSize &size, int bytes_per_pixel=4) noexcept
Calculates texture memory usage in megabytes.
static consteval float minQualityMargin() noexcept
Minimum quality margin.
static consteval float maxQualityMargin() noexcept
Maximum quality margin.
static consteval int minDisplayDimension() noexcept
Minimum display dimension to prevent too small textures.
Physical screen characteristics.
float dpr
Device pixel ratio (density)
int physical_width
Physical screen width in pixels.
int physical_height
Physical screen height in pixels.
Result of viewport calculation for an image.
QSize display_size
Size to display in QML (logical pixels)
bool needs_downsample
True if downsample is required.
QSize downsample_size
Size to downsample source image to (GPU texture)
float fit_zoom
Zoom factor to fit image in viewport (0.0-1.0)
size_t texture_memory_mb
Estimated GPU memory usage in MB.
Current viewport state for zoom/pan operations.
QPointF center_offset
Offset to center image in viewport.
QPointF pan
Current pan offset in logical pixels.
float zoom
Current zoom level (1.0 = 100%)
Central viewport management for optimal display across all screen types.
CaptureMoment — footer