Merge pull request #1031 from KattKnight/link-watch-idle-wake

Links: wake for the first look at a link placed while the app is idle
This commit is contained in:
BFlat 2026-10-11 04:34:46 +07:00 committed by GitHub
commit c94d38e53c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 2 deletions

View File

@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
| Date | What landed |
|---|---|
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). |
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). |
| 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) |
| 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day |
| 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections |

View File

@ -58,3 +58,7 @@ sync_github_name = false
real_name = "Fabio William Conceição"
display_name = "Messhias"
sync_github_name = false
[people.KattKnight]
display_name = "KattKnight (ayla.dsc)"
sync_github_name = false

View File

@ -788,6 +788,8 @@ impl VectorcraftApp {
return;
}
let last: f64 = ctx.data(|d| d.get_temp(last_key)).unwrap_or(f64::NEG_INFINITY);
// The active document as the last look saw it: its uid and revision.
let (seen_key, doc) = (egui::Id::new("linkWatch.seen"), self.session.active().map(|d| (d.uid, d.revision)));
if now - last >= 2.0 || (focused && !was_focused) {
// Walking the document's links costs a pass over it: only when a look is due.
// `start_link_scan` starts none for a document without links, which then doesn't
@ -795,12 +797,21 @@ impl VectorcraftApp {
ctx.data_mut(|d| d.insert_temp(last_key, now));
self.session.start_link_scan();
let links = self.session.link_scan.is_some();
ctx.data_mut(|d| d.insert_temp(egui::Id::new("linkWatch.links"), links));
ctx.data_mut(|d| {
d.insert_temp(egui::Id::new("linkWatch.links"), links);
d.insert_temp(seen_key, doc);
});
}
if self.session.link_scan.is_some() {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
} else if ctx.data(|d| d.get_temp::<bool>(egui::Id::new("linkWatch.links"))).unwrap_or(false) {
ctx.request_repaint_after(std::time::Duration::from_millis(2000));
} else if ctx.data(|d| d.get_temp::<Option<(u64, u64)>>(seen_key)).is_some_and(|seen| seen != doc) {
// The document changed (a file placed into one without links) or another came to the
// front since that look: wake for the next one even if the app sits idle until then,
// or a file placed and changed meanwhile is first seen after the change, which is then
// never acted on.
ctx.request_repaint_after(std::time::Duration::from_secs_f64((last + 2.0 - now).clamp(0.0, 2.0)));
}
}

View File

@ -546,3 +546,68 @@ fn crop_image_shows_a_box_that_apply_crops_to() {
app.run("select.none", json!({})).unwrap();
assert!(!crate::menus::enabled(&app, "ui.cropImage") && app.run("ui.cropImage", json!({})).is_err());
}
/// One headless frame of the whole window at `time` → how long the app asked to wait before the
/// next one.
fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64) -> std::time::Duration {
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), time: Some(time), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
out.viewport_output.get(&egui::ViewportId::ROOT).map_or(std::time::Duration::MAX, |v| v.repaint_delay)
}
/// The window sits idle for `secs` from `*time` (a frame now, then only the frames the app asks
/// for) or until `done` → whether it was.
fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64, done: &dyn Fn(&VectorcraftApp) -> bool) -> bool {
let end = *time + secs;
loop {
let wait = frame_at(app, ctx, *time);
if done(app) {
return true;
}
let next = *time + wait.as_secs_f64().max(1.0 / 60.0);
if next > end {
*time = end;
return false;
}
if app.session.link_scan.is_some() {
// The look runs on a worker thread, in real time.
std::thread::sleep(std::time::Duration::from_millis(5));
}
*time = next;
}
}
/// A file placed into a document whose last look found no links, then changed by another app
/// while the window sits idle in the background, follows Update Links: Automatically: the app
/// wakes itself to see the new link before the change, so the change isn't taken for the file as
/// first seen.
#[test]
fn a_file_placed_while_idle_follows_its_changes() {
let mut app = app();
app.run("prefs.set", json!({"key": "updateLinks", "value": "automatically"})).unwrap();
let ctx = egui::Context::default();
let mut time = 0.0;
idle(&mut app, &ctx, &mut time, 3.0, &|_| false);
// A look finds the new document without links: no wakes of its own for it.
assert_eq!(frame_at(&mut app, &ctx, time), std::time::Duration::MAX, "{:?}", ctx.repaint_causes());
// Half a second later, before the next look is due, a file is placed. The place itself is a
// frame (as any command); then nothing happens for 3 s.
time += 0.5;
let path = temp_file("idle-watch.png", &png(40, 30, 72.0));
app.run("file.place", json!({"path": path})).unwrap();
let id = app.session.active().unwrap().selection.objects[0];
idle(&mut app, &ctx, &mut time, 3.0, &|_| false);
std::thread::sleep(std::time::Duration::from_millis(30));
let changed = png(60, 30, 72.0);
std::fs::write(&path, &changed).unwrap();
let hash = vectorcraft_doc::links::hash_bytes(&changed);
let updated = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
matches!(&st.doc.node(id).unwrap().kind, NodeKind::Image(im) if im.link.as_ref().and_then(|l| l.hash.as_deref()) == Some(hash.as_str()))
};
assert!(idle(&mut app, &ctx, &mut time, 10.0, &updated), "the changed file was never read again");
}