Merge pull request #1696 from YsDirector/pr/c11-next-frame

Capture after the next rendered frame

# Conflicts:
#	src/app/control/mod.rs
This commit is contained in:
Hakan Seven committed 2026-10-11 23:30:20 +03:00
commit 7b8397fe6b
4 files changed
+129 -15

No files matched your search

+111 -15
View File
@@ -120,6 +120,13 @@ pub(super) struct State {
/// Live `getpoint` request — the client asked for one picked point; Some
/// until the person clicks (answer) or presses Escape (cancel).
pub(super) get_point: Option<UserPointSession>,
/// `capture` request parked until the window has drawn another frame:
/// screenshotting in the same batch as the state change used to return the
/// previous frame (C11). Cleared by `on_control_capture_frame`.
pub(super) capture_frame_pending: bool,
/// Whether the parked capture actually waited for a redraw (vs. the
/// anti-hang timer). Reported in the capture result as `waited_for_frame`.
pub(super) capture_after_redraw: bool,
}
/// The pending half of an interactive `user_select` operation (`interactive`
@@ -887,7 +894,11 @@ impl OpenCADStudio {
// Interactive requests hold their slot open on purpose: the
// counter stays at one so settle keeps returning `running` until
// the person at the screen answers and the resolver zeroes it.
if !matches!(p.request["op"].as_str(), Some("user_select" | "getpoint")) {
// `capture` is parked the same way until the next drawn frame.
if !matches!(
p.request["op"].as_str(),
Some("user_select" | "getpoint" | "capture")
) {
p.pending -= 1;
}
}
@@ -1120,10 +1131,9 @@ impl OpenCADStudio {
Task::none()
}
"capture" => {
let window = self
.main_window
self.main_window
.ok_or_else(|| failure("gui_required", "Capture requires a GUI window"))?;
let path = string(req, "path")?.to_owned();
string(req, "path")?;
let was_minimized = crate::sys::restore_window_if_minimized();
if was_minimized {
std::thread::sleep(std::time::Duration::from_millis(60));
@@ -1181,17 +1191,11 @@ impl OpenCADStudio {
}
}
// A minimized window has a 0x0 surface and the renderer
// panics reading it back, so report instead of capturing.
iced::window::size(window).then(move |size| {
let path = path.clone();
if size.width <= 0.0 || size.height <= 0.0 {
Task::done(Message::ControlScreenshot(path, None))
} else {
iced::window::screenshot(window)
.map(move |s| Message::ControlScreenshot(path.clone(), Some(s)))
}
})
// C11: do not capture in the same batch. Park until the next
// rendered frame (or the timer fallback) so geometry changes are
// visible rather than returning a stale frame.
self.control.capture_frame_pending = true;
Task::none()
}
"stop" => {
self.control.enabled = false;
@@ -1401,6 +1405,7 @@ impl OpenCADStudio {
path: String,
screenshot: Option<iced::window::Screenshot>,
) {
let waited_for_frame = self.control.capture_after_redraw;
let result = (|| -> Result<Value, String> {
let s = screenshot
.ok_or("The window is minimized or has no size; restore it and capture again")?;
@@ -1549,6 +1554,7 @@ impl OpenCADStudio {
"width": image.width(),
"height": image.height(),
"scale_factor": s.scale_factor,
"waited_for_frame": waited_for_frame,
"document_id": self.tabs[self.active_tab].id,
"revision": self.tabs[self.active_tab].edit_revision,
"camera_revision": self.tabs[self.active_tab].scene.camera_generation,
@@ -1570,6 +1576,54 @@ impl OpenCADStudio {
}
Err(e) => self.command_line.push_error(&e),
}
// `capture` is parked until this result (or error) arrives, rather
// than being settled by the task counter used for ordinary actions.
if let Some(p) = self
.control
.pending
.as_mut()
.filter(|p| p.request["op"] == "capture")
{
p.pending = 0;
}
self.control.capture_after_redraw = false;
self.control_settle();
}
/// A frame finished rendering (or the anti-hang timer expired): start the
/// screenshot only after the capture request has been parked.
pub(super) fn on_control_capture_frame(&mut self, redrew: bool) -> Task<Message> {
if !self.control.capture_frame_pending {
return Task::none();
}
let Some(path) = self
.control
.pending
.as_ref()
.filter(|p| p.request["op"] == "capture")
.and_then(|p| p.request["path"].as_str())
.map(str::to_owned)
else {
self.control.capture_frame_pending = false;
return Task::none();
};
self.control.capture_frame_pending = false;
self.control.capture_after_redraw = redrew;
let Some(window) = self.main_window else {
return Task::done(Message::ControlScreenshot(path, None));
};
// A minimized window has a 0x0 surface and the renderer panics reading
// it back, so report instead of capturing.
iced::window::size(window).then(move |size| {
let path = path.clone();
if size.width <= 0.0 || size.height <= 0.0 {
Task::done(Message::ControlScreenshot(path, None))
} else {
iced::window::screenshot(window).map(move |screenshot| {
Message::ControlScreenshot(path.clone(), Some(screenshot))
})
}
})
}
}
mod actions;
@@ -1692,6 +1746,48 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn capture_parks_for_the_next_frame_then_finishes_with_its_result() {
let mut app = OpenCADStudio::new_for_test();
app.main_window = Some(iced::window::Id::unique());
assert_eq!(request(&mut app, json!({"op":"new"}))["ok"], true);
let path = std::env::temp_dir().join(format!("ocs-capture-frame-{}.png", session_id()));
let state = app.control_state();
let parked = app
.control_request(json!({
"protocol":1,
"op":"capture",
"request_id":"cap-frame",
"document_id": state["document_id"],
"revision": state["revision"],
"path": path.to_string_lossy(),
}))
.0;
assert_eq!(parked["status"], "accepted", "{parked}");
assert!(app.control.capture_frame_pending, "capture should wait for a frame");
let running = app
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
.0;
assert_eq!(running["status"], "running", "{running}");
let _frame_task = app.on_control_capture_frame(true);
assert!(!app.control.capture_frame_pending);
let screenshot = iced::window::Screenshot::new(
vec![0u8; 4 * 4 * 4],
iced::Size::new(4, 4),
1.0,
);
app.control_screenshot(path.to_string_lossy().into_owned(), Some(screenshot));
let done = app
.control_request(json!({"op":"operation","request_id":"cap-frame"}))
.0;
assert_eq!(done["status"], "completed", "{done}");
assert_eq!(done["result"]["waited_for_frame"], true, "{done}");
assert_eq!(done["result"]["width"], 4, "{done}");
assert!(path.exists(), "capture should be written");
let _ = std::fs::remove_file(&path);
}
#[test]
fn control_stepwise_drawing_undo_and_properties() {
let mut app = OpenCADStudio::new_for_test();
+4
View File
@@ -2158,6 +2158,10 @@ pub enum Message {
ControlStep(String, Box<Message>),
ControlTaskDone(String),
ControlScreenshot(String, Option<iced::window::Screenshot>),
/// A frame has been drawn (or the anti-hang timer expired) for a parked
/// `capture` request: take the screenshot now, so it shows the latest
/// state instead of the previous frame (C11).
ControlCaptureFrame { redrew: bool },
ControlToggle,
/// Node graph overlay interaction.
Graph(crate::ui::node_graph::GraphMsg),
+1
View File
@@ -661,6 +661,7 @@ impl OpenCADStudio {
self.control_screenshot(path, screenshot);
Task::none()
}
Message::ControlCaptureFrame { redrew } => self.on_control_capture_frame(redrew),
Message::Graph(message) => self.on_graph(message),
Message::ControlToggle => {
+13
View File
@@ -2603,6 +2603,18 @@ impl OpenCADStudio {
} else {
Subscription::none()
};
// C11: park a `capture` request until the window draws another frame;
// a timer fallback prevents a request from hanging when no redraw is
// pending.
let capture_frame = if self.control.capture_frame_pending {
Subscription::batch([
window::frames().map(|_| Message::ControlCaptureFrame { redrew: true }),
iced::time::every(std::time::Duration::from_millis(50))
.map(|_| Message::ControlCaptureFrame { redrew: false }),
])
} else {
Subscription::none()
};
// Blink the MText preview caret while the editor is open.
let caret_blink = if self.mtext_editor.is_some() {
iced::time::every(std::time::Duration::from_millis(530))
@@ -2858,6 +2870,7 @@ impl OpenCADStudio {
nav_settle,
gpu_probe,
thumbnail_capture,
capture_frame,
caret_blink,
web_fonts,
autosave,