From bc50fa782e21944d19a7727e5b290c96784960fb Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 14:27:38 +0000 Subject: [PATCH] Place: a placement read again when it completes stays within the roots it was read in Follow-up to #5306. A DXF placed off the UI thread against an artboard that changed meanwhile is read again in finish(), on the UI thread, outside the automation roots captured when the read started. Pending now keeps that scope and poll runs finish inside it. --- crates/ui-egui/src/placing.rs | 8 ++++-- crates/ui-egui/src/placing/tests.rs | 39 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/placing.rs b/crates/ui-egui/src/placing.rs index 294f22770..7bdc1f9d6 100644 --- a/crates/ui-egui/src/placing.rs +++ b/crates/ui-egui/src/placing.rs @@ -137,6 +137,9 @@ pub struct Pending { /// The number of the paste's chain ([`Placing::chains`]). chain: Option, done: Receiver>, + /// The automation roots in force when the read started, kept in force when it is placed: a + /// DXF placed against an artboard that changed meanwhile is read again then. + scope: ScopeHandle, } /// A read waiting for the thread that reads the files. @@ -318,6 +321,7 @@ fn spawn_labeled( let (tx, done) = channel(); // The automation roots in force here are in force there. let scope = ScopeHandle::capture(); + let completion = scope.clone(); #[cfg(test)] let reading = app.placing.reading.clone(); let job: Job = Box::new(move || { @@ -352,7 +356,7 @@ fn spawn_labeled( let _ = reader.send(job); app.placing.reader = Some(reader); } - app.placing.pending.push(Pending { name, label: label.clone(), doc, dropped, paste: chain.is_some(), chain, done }); + app.placing.pending.push(Pending { name, label: label.clone(), doc, dropped, paste: chain.is_some(), chain, done, scope: completion }); let status = format!("{label}…"); app.status(status.clone()); Ok(Some(json!({ "background": true, "status": status }))) @@ -384,7 +388,7 @@ pub fn poll(app: &mut VectorcraftApp) { if app.ui.status == format!("{}…", p.label) { app.ui.status.clear(); } - finish(app, &p, r); + p.scope.clone().run(|| finish(app, &p, r)); if let Some(chain) = p.chain && !app.placing.swatch_paste.as_ref().is_some_and(|waiting| waiting.chain == chain) { diff --git a/crates/ui-egui/src/placing/tests.rs b/crates/ui-egui/src/placing/tests.rs index 90d1a551a..fcd209016 100644 --- a/crates/ui-egui/src/placing/tests.rs +++ b/crates/ui-egui/src/placing/tests.rs @@ -1284,3 +1284,42 @@ fn a_file_read_between_a_tools_clicks_is_placed() { assert_eq!(app.session.tool_busy(), !other_document, "other document: {other_document}"); } } + +/// A DXF placed within automation roots is read again within them when it is placed against an +/// artboard that changed meanwhile: a path redirected outside the roots after the read is refused. +#[cfg(unix)] +#[test] +fn a_file_read_again_when_placed_stays_within_the_roots_it_was_read_in() { + let mut src = Session::new(); + src.execute("file.new", &json!({"width": 100, "height": 50})).unwrap(); + src.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 10})).unwrap(); + let v = src.execute("document.serialize", &json!({"format": "dxf"})).unwrap(); + let dxf = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap(); + let base = std::env::temp_dir().join(format!("vc-placing-reread-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + for d in ["in", "out"] { + std::fs::create_dir_all(base.join(d)).unwrap(); + std::fs::write(base.join(d).join("real.dxf"), &dxf).unwrap(); + } + let link = base.join("in").join("plan.dxf"); + std::os::unix::fs::symlink(base.join("in").join("real.dxf"), &link).unwrap(); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap().unwrap(); + let mut app = app(true); + let p = json!({"path": link.to_string_lossy(), "dxf": {"fit": true}, "at": [100, 150]}); + vectorcraft_engine::file_access::confine(Some(&roots), || start(&mut app, &p, false)).unwrap(); + let t0 = std::time::Instant::now(); + while !(app.placing.reading[1].load(std::sync::atomic::Ordering::SeqCst) == 1 + && app.placing.reading[0].load(std::sync::atomic::Ordering::SeqCst) == 0) + { + assert!(t0.elapsed().as_secs() < 60, "read within 60 s"); + std::thread::yield_now(); + } + // Read; now the path leads outside the roots and the artboard under `at` changes. + std::fs::remove_file(&link).unwrap(); + std::os::unix::fs::symlink(base.join("out").join("real.dxf"), &link).unwrap(); + app.run("artboard.setProps", json!({"index": 0, "width": 200})).unwrap(); + placed(&mut app); + assert_eq!(app.session.doc().unwrap().doc.layers.iter().flat_map(|l| l.children().into_iter().flatten()).count(), 0, "nothing placed"); + assert!(app.ui.status.contains("outside the read root"), "{}", app.ui.status); + let _ = std::fs::remove_dir_all(base); +}