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:
commit
c94d38e53c
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue