forked from 1b-agent/opencadstudio-upstream
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:
commit
7b8397fe6b
4 files changed
+129
-15
No files matched your search
+111
-15
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user