mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:20:56 -06:00
Session::execute ran check_params only in the engine's own tests, so a direct execute in a release build ran a misspelled key with the command's default. It now checks every call, the commands a command runs included, as MCP and the control port already did. A command.batch or an action.batch is checked before its first step runs, and each step is checked again as it runs, so an effect.setParams step is checked against the effects the steps before it added. Saved steps play through Session::replay, which takes keys a command no longer reads, so an action recorded by an older version still plays and Session::replay still plays an older journal. The Actions panel's Play (Accelerated, Step by Step and Pause For, its function keys and ui.action.play) and the Batch dialog use it. Docs: the cmd/params.rs module doc, the Parameter keys section of docs/mcp.md and a line in docs/target-app-parity.md describe both paths.
130 lines
7.2 KiB
Rust
130 lines
7.2 KiB
Rust
use super::{
|
||
MAX_PENDING_SCREENSHOTS, MAX_SCREENSHOT_BYTES, MAX_SCREENSHOT_TOTAL_BYTES, PendingScreenshot, QueuedScreenshot, SCREENSHOT_TIMEOUT_MS,
|
||
VectorcraftApp, file_access, json, now_ms,
|
||
};
|
||
use crate::control;
|
||
|
||
impl VectorcraftApp {
|
||
pub(crate) fn drain_control(&mut self, ctx: &egui::Context) {
|
||
let Some(rx) = self.control_rx.take() else { return };
|
||
let mut handled = 0;
|
||
while handled < control::MAX_CONTROL_REQUESTS_PER_FRAME {
|
||
let Some(req) = self.deferred_control.take().or_else(|| rx.try_recv().ok()) else { break };
|
||
// Synthetic input sent before this tool action was dropped (see
|
||
// `discard_stale_injected_batch`): it would reach the tool without that input.
|
||
let tool_action = matches!(req.method.as_str(), "ui.pointer" | "ui.tool.select");
|
||
if tool_action && req.made_before(self.synthetic_dropped_before) {
|
||
// One its caller gave up is skipped without counting toward this frame's limit.
|
||
if req.claim() {
|
||
handled += 1;
|
||
let _ = req.reply.send(json!({
|
||
"ok": false,
|
||
"error": "input sent before this tool action was dropped because the document, dialog or focus changed"
|
||
}));
|
||
}
|
||
continue;
|
||
}
|
||
// `ui.pointer` and `ui.tool.select` act at once, while `ui.key` and other synthetic
|
||
// input reach the tool in a later UI pass. Wait for that earlier input, so a key
|
||
// pressed during a drag reaches its tool before a pointer event or tool switch.
|
||
// One its caller gave up meanwhile is dropped below instead of holding back the rest.
|
||
if tool_action && !req.given_up() && (!self.synthetic.is_empty() || self.synthetic_injected) {
|
||
self.deferred_control = Some(req);
|
||
ctx.request_repaint();
|
||
break;
|
||
}
|
||
// Its caller stopped waiting (the app was busy or its window not drawn for too long)
|
||
// and was told it won't run: an edit applied now would come from nowhere (#1725).
|
||
if !req.claim() {
|
||
continue;
|
||
}
|
||
handled += 1;
|
||
let reply = req.reply.clone();
|
||
// Guarded per request: a panic must not drop the channel (taken out of `self` above).
|
||
let roots = self.automation_roots.clone();
|
||
self.ui.answering_control = true;
|
||
let answer = file_access::confine(roots.as_ref(), || vectorcraft_engine::guard::catch_panic(|| control::handle(self, ctx, &req)));
|
||
self.ui.answering_control = false;
|
||
let outcome = match answer {
|
||
Ok(outcome) => outcome,
|
||
Err(msg) => {
|
||
self.picks.recover();
|
||
self.session.set_replaying(false);
|
||
control::err(format!("internal error: {msg} (please report this bug)"))
|
||
}
|
||
};
|
||
match outcome {
|
||
control::Outcome::Done(v) => {
|
||
let _ = reply.send(v);
|
||
}
|
||
control::Outcome::Screenshot { path, data } => {
|
||
// Admit with the shared ceilings (4 requests, 128 MiB each, 256 MiB together)
|
||
// before issue, reserving the checked bytes this viewport would need.
|
||
match self.screenshot_pixels(ctx) {
|
||
Ok((width, height, bytes)) if bytes > MAX_SCREENSHOT_BYTES => {
|
||
let _ = reply.send(json!({"ok": false, "error": format!(
|
||
"screenshot would use {bytes} bytes ({width}×{height}), over the {MAX_SCREENSHOT_BYTES}-byte limit"
|
||
)}));
|
||
}
|
||
Ok((width, height, bytes)) if data && path.is_none() && bytes > control::MAX_SCREENSHOT_PNG_REPLY_BYTES => {
|
||
let _ = reply.send(json!({"ok": false, "error": format!(
|
||
"a PNG reply of this {width}×{height} screenshot ({bytes} bytes of pixels) would exceed the {}-byte reply-encoding limit; write it to a path instead",
|
||
control::MAX_SCREENSHOT_PNG_REPLY_BYTES
|
||
)}));
|
||
}
|
||
Ok((_, _, _)) if self.pending_screenshots.len() >= MAX_PENDING_SCREENSHOTS => {
|
||
let _ = reply.send(json!({"ok": false, "error": format!(
|
||
"already {MAX_PENDING_SCREENSHOTS} screenshots are waiting; wait for one to finish"
|
||
)}));
|
||
}
|
||
Ok((width, height, bytes)) if self.screenshot_reserved.saturating_add(bytes) > MAX_SCREENSHOT_TOTAL_BYTES => {
|
||
let _ = reply.send(json!({
|
||
"ok": false,
|
||
"error": format!(
|
||
"screenshots already reserved {} bytes; {bytes} more ({width}×{height}) would pass the {MAX_SCREENSHOT_TOTAL_BYTES}-byte limit",
|
||
self.screenshot_reserved
|
||
)
|
||
}));
|
||
}
|
||
Ok((_, _, bytes)) => {
|
||
self.screenshot_token += 1;
|
||
let id = self.screenshot_token;
|
||
let settle = ctx.global_style().animation_time as f64 * 2000.0 + 80.0;
|
||
let due_ms = now_ms() + settle;
|
||
self.screenshot_reserved = self.screenshot_reserved.saturating_add(bytes);
|
||
self.queued_screenshots.push(QueuedScreenshot { id, due_ms, frames: 0 });
|
||
let document = self.document_identity();
|
||
self.pending_screenshots.push(PendingScreenshot {
|
||
id,
|
||
path,
|
||
data,
|
||
reply,
|
||
deadline_ms: due_ms + SCREENSHOT_TIMEOUT_MS,
|
||
reserved_bytes: bytes,
|
||
document,
|
||
slot: None,
|
||
issued: None,
|
||
attempts: 0,
|
||
});
|
||
}
|
||
Err(e) => {
|
||
let _ = reply.send(json!({"ok": false, "error": e}));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if handled == control::MAX_CONTROL_REQUESTS_PER_FRAME {
|
||
// Leave later requests on the channel and schedule another frame to serve them.
|
||
ctx.request_repaint();
|
||
}
|
||
self.control_rx = Some(rx);
|
||
}
|
||
|
||
/// The active document's identity: its process-unique uid and revision. A capture records it
|
||
/// at admission and issuance and refuses to answer with a different document or undo state.
|
||
pub(super) fn document_identity(&self) -> Option<(u64, u64)> {
|
||
self.session.active().map(|document| (document.uid, document.revision))
|
||
}
|
||
}
|