mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Linked designs: one changed while the assembly was closed is read again when it opens (#9739) (#9872)
* Claim #9739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Linked designs: one changed while the assembly was closed is read when it opens (#9739) A link now keeps the SHA-256 of its design file's bytes as the assembly last read them (DesignLink::file_sha256, a pending format change), set by Insert Design, Relink, Extract to File and Update Links. component.links reports `changed` from it, hashing a file again only when its time or size changed (vfs::design_path_stamp, behind the design-path rule). The app treats a link it meets for the first time whose file changed as not read yet, so opening an assembly, in its own tab or in place of the blank design, reads those parts again as a tab coming back does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Linked designs: read what changed last when a design opens in place of the blank one (#9739) A read is taken up only by the design revision it started from, and adopt bumped the revision after starting it, so the read was dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2628,6 +2628,19 @@ pub struct DesignLink {
|
||||
/// local overrides and survive a source update. Older links start with no baseline.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub inherited: Vec<LinkedBodyProperties>,
|
||||
/// SHA-256 of the design file's bytes as this design last read them ([`Self::file_digest`]):
|
||||
/// opened again, this design tells which linked files changed meanwhile (#9739). Links made
|
||||
/// before it was kept have none and are taken as up to date.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub file_sha256: Option<String>,
|
||||
}
|
||||
|
||||
impl DesignLink {
|
||||
/// What [`Self::file_sha256`] keeps for a design file's `bytes`.
|
||||
pub fn file_digest(bytes: &[u8]) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// The values last inherited by a linked body, not another editable material library.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A linked component's link saves `file_sha256`, the SHA-256 of the linked design file's bytes
|
||||
//! as the assembly last read them, so an assembly opened again knows which linked designs changed
|
||||
//! while it was closed (#9739). Older designs read unchanged: their links have no digest and are
|
||||
//! taken as up to date until Update Links reads them. The last release refuses the newer format
|
||||
//! and asks for an update instead of dropping the digests.
|
||||
@@ -17,7 +17,7 @@ use serde_json::{Value, json};
|
||||
use solidcraft_doc::{DesignLink, Document, FeatureKind, ModelBody, ModelState};
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use super::{absolute, component_key, inherit_properties, resolve, session_bodies, stored};
|
||||
use super::{absolute, component_key, file_sha256, inherit_properties, resolve, session_bodies, stored};
|
||||
use crate::params::{bad, str_};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
@@ -597,7 +597,7 @@ pub(super) fn extract(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
f.link_faces = faces;
|
||||
}
|
||||
let path = stored(s.path.as_deref(), &file);
|
||||
s.doc_mut().links.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), feature: id, inherited: Vec::new() });
|
||||
s.doc_mut().links.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), feature: id, ..Default::default() });
|
||||
s.refresh();
|
||||
rename_joint_faces(s, &state, id, &sc.own);
|
||||
if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) {
|
||||
@@ -611,6 +611,12 @@ pub(super) fn extract(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
part.path = s.path.clone();
|
||||
part.execute("file.save", &json!({"path": shown}))?;
|
||||
// The link keeps what the new file holds, as Insert Design's does (#9739).
|
||||
if let Some(read) = file_sha256(&file, s.path.as_deref())
|
||||
&& let Some(link) = s.doc_mut().links.get_mut(&comp)
|
||||
{
|
||||
link.file_sha256 = Some(read);
|
||||
}
|
||||
Ok(json!({"component": comp, "feature": id, "path": path, "file": shown, "bodies": sc.bodies, "notices": notices, "warnings": warnings,
|
||||
"copies": named(s, &copies)}))
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
CommandSpec::new("component.links", "List Links", list_links).icon("chain").noundo().params(
|
||||
"→ linked components: component, name, path, file (resolved), found (null when the file is on a network location not allowed, \
|
||||
or names a device: it is not looked for), and then host (the network location) and reason",
|
||||
or names a device: it is not looked for), changed (whether the file is no longer what this design last read: Update Links \
|
||||
reads it again; null when not found or the link is older than this record), and then host (the network location) and reason",
|
||||
),
|
||||
];
|
||||
|
||||
@@ -170,9 +171,38 @@ fn read_link(file: &Path, own: Option<&str>) -> Result<Vec<u8>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The solid bodies of the design at `file`, evaluated, as an Import feature.
|
||||
fn linked_bodies(file: &Path, own: Option<&str>) -> Result<Linked> {
|
||||
design_bodies(file, &read_link(file, own)?)
|
||||
/// The solid bodies of the design at `file`, evaluated, as an Import feature, with the digest of
|
||||
/// the bytes read, which the link keeps ([`DesignLink::file_sha256`], #9739).
|
||||
fn linked_bodies_read(file: &Path, own: Option<&str>) -> Result<(Linked, String)> {
|
||||
let bytes = read_link(file, own)?;
|
||||
Ok((design_bodies(file, &bytes)?, DesignLink::file_digest(&bytes)))
|
||||
}
|
||||
|
||||
/// Linked files' digests as [`file_sha256`] last worked them out, by file, with the time and
|
||||
/// size the file had then. Bounded, oldest first out.
|
||||
static DIGESTS: Mutex<Vec<(PathBuf, (std::time::SystemTime, u64), String)>> = Mutex::new(Vec::new());
|
||||
const DIGESTS_SIZE: usize = 256;
|
||||
|
||||
/// The digest of the design file at `file` now ([`DesignLink::file_digest`]), `None` when it
|
||||
/// can't be read. `component.links` asks each time a tab is shown, so a file is read again only
|
||||
/// when its time or size changed since; the browser build keeps no times and reads it each time.
|
||||
pub(super) fn file_sha256(file: &Path, own: Option<&str>) -> Option<String> {
|
||||
let file = absolute(file);
|
||||
let stamp = solidcraft_io::vfs::design_path_stamp(&file.to_string_lossy(), own);
|
||||
if let Some(stamp) = stamp
|
||||
&& let Some(known) = DIGESTS.lock().ok()?.iter().find(|(f, s, _)| *f == file && *s == stamp).map(|(_, _, d)| d.clone())
|
||||
{
|
||||
return Some(known);
|
||||
}
|
||||
let digest = DesignLink::file_digest(&read_link(&file, own).ok()?);
|
||||
if let (Some(stamp), Ok(mut digests)) = (stamp, DIGESTS.lock()) {
|
||||
digests.retain(|(f, _, _)| *f != file);
|
||||
if digests.len() >= DIGESTS_SIZE {
|
||||
digests.remove(0);
|
||||
}
|
||||
digests.push((file, stamp, digest.clone()));
|
||||
}
|
||||
Some(digest)
|
||||
}
|
||||
|
||||
/// The solid bodies of `bytes`, the design at `file`, as an Import feature. The model the file
|
||||
@@ -478,7 +508,7 @@ fn insert_design(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return super::component::copy_occurrence(s, &json!({"component": component, "parent": parent, "translate": translate}));
|
||||
}
|
||||
}
|
||||
let (design_name, sf, faces, metadata, properties) = linked_bodies(&file, s.path.as_deref())?;
|
||||
let ((design_name, sf, faces, metadata, properties), read) = linked_bodies_read(&file, s.path.as_deref())?;
|
||||
let name = str_(p, "name").filter(|n| !n.trim().is_empty()).map(str::to_string).unwrap_or(design_name);
|
||||
let mut create = json!({"name": name, "activate": false});
|
||||
// Clipboard insertion uses reuse even for the first paste, which must stay movable.
|
||||
@@ -505,7 +535,9 @@ fn insert_design(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o.transform[3] = [translate.x, translate.y, translate.z, 1.0];
|
||||
}
|
||||
let path = stored(s.path.as_deref(), &file);
|
||||
s.doc_mut().links.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), feature: id, inherited: Vec::new() });
|
||||
s.doc_mut()
|
||||
.links
|
||||
.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), feature: id, inherited: Vec::new(), file_sha256: Some(read) });
|
||||
s.refresh();
|
||||
if let Some(e) = s.model.result(id).and_then(|r| r.error.clone()) {
|
||||
return Err(EngineError::Other(e));
|
||||
@@ -549,6 +581,14 @@ fn update_links(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
link_refused(s.path.as_deref(), &link.path).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
|
||||
let file = resolve(s.path.as_deref(), &link.path);
|
||||
let bytes = read_link(&file, s.path.as_deref()).map_err(|e| bad(cmd, format!("{cname}: {e}")))?;
|
||||
// The link keeps what it read, so opening this design again knows whether the file
|
||||
// changed while it was closed (#9739).
|
||||
let read = DesignLink::file_digest(&bytes);
|
||||
if link.file_sha256.as_deref() != Some(read.as_str())
|
||||
&& let Some(kept) = s.doc_mut().links.get_mut(&comp)
|
||||
{
|
||||
kept.file_sha256 = Some(read);
|
||||
}
|
||||
// A link that has not recorded the properties it inherited (made before they were) is read again.
|
||||
let kept_faces = s
|
||||
.doc
|
||||
@@ -654,7 +694,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// Read/evaluate the candidate before changing anything. Session::execute owns the single
|
||||
// undo step and restores both the document and evaluated state on any later failure.
|
||||
let (_, sf, faces, _, properties) = linked_bodies(&file, s.path.as_deref()).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let ((_, sf, faces, _, properties), read) = linked_bodies_read(&file, s.path.as_deref()).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let geometry_changed = match (s.doc.feature(link.feature).map(|f| &f.kind), &sf.kind) {
|
||||
(Some(FeatureKind::Import { step: old, .. }), FeatureKind::Import { step, .. }) => old != step,
|
||||
_ => true,
|
||||
@@ -677,7 +717,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
f.kind = sf.kind;
|
||||
f.link_faces = faces;
|
||||
}
|
||||
s.doc_mut().links.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), ..link.clone() });
|
||||
s.doc_mut().links.insert(comp, DesignLink { path: file.to_string_lossy().into_owned(), file_sha256: Some(read), ..link.clone() });
|
||||
s.refresh();
|
||||
if let Some(error) = s.model.result(link.feature).and_then(|r| r.error.clone()) {
|
||||
return Err(bad(cmd, error));
|
||||
@@ -717,8 +757,17 @@ fn list_links(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
// A refused path is not looked for at all: on Windows asking whether a file on
|
||||
// `\\host\share` exists already connects to the host.
|
||||
match link_refusal(s.path.as_deref(), &l.path) {
|
||||
None => json!({"component": c, "name": name, "path": path, "file": file,
|
||||
"found": solidcraft_io::vfs::design_path_is_file(&file, s.path.as_deref())}),
|
||||
None => {
|
||||
let found = solidcraft_io::vfs::design_path_is_file(&file, s.path.as_deref());
|
||||
// Whether the file is no longer what this design last read (#9739): `null`
|
||||
// when it isn't found, or the link keeps no digest (made before they were).
|
||||
let changed = l
|
||||
.file_sha256
|
||||
.as_ref()
|
||||
.filter(|_| found == Some(true))
|
||||
.and_then(|kept| file_sha256(Path::new(file.as_ref()), s.path.as_deref()).map(|now| now != *kept));
|
||||
json!({"component": c, "name": name, "path": path, "file": file, "found": found, "changed": changed})
|
||||
}
|
||||
Some(solidcraft_io::vfs::Remote::Network(host)) => json!({"component": c, "name": name, "path": path, "file": file,
|
||||
"found": null, "host": host, "reason": format!("on the network location \\\\{host}, which is not allowed")}),
|
||||
Some(solidcraft_io::vfs::Remote::Device) => json!({"component": c, "name": name, "path": path, "file": file,
|
||||
@@ -913,6 +962,41 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #9739: a link keeps what it read of its file, so an assembly opened again says which
|
||||
/// linked designs changed while it was closed; Update Links takes the change and the record.
|
||||
#[test]
|
||||
fn a_link_says_its_file_changed_while_the_design_was_closed() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-link-changed-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let (part, assembly) = (dir.join("p.sc"), dir.join("a.sc"));
|
||||
save_box(&part, 10.0);
|
||||
let mut a = Session::default();
|
||||
run(&mut a, "component.insert_design", json!({"path": part}));
|
||||
run(&mut a, "file.save", json!({"path": assembly}));
|
||||
let changed = |s: &mut Session| run(s, "component.links", json!({}))["links"][0]["changed"].clone();
|
||||
assert_eq!(changed(&mut a), json!(false));
|
||||
// The part is edited while the assembly is closed.
|
||||
save_box(&part, 30.0);
|
||||
let mut opened = Session::default();
|
||||
run(&mut opened, "doc.open", json!({"path": assembly}));
|
||||
assert_eq!(changed(&mut opened), json!(true));
|
||||
assert!((volume(&mut opened) - 2000.0).abs() < 1e-6, "{}", volume(&mut opened));
|
||||
assert_eq!(run(&mut opened, "component.update_links", json!({}))["updated"], json!(["p"]));
|
||||
assert_eq!(changed(&mut opened), json!(false));
|
||||
assert!((volume(&mut opened) - 6000.0).abs() < 1e-6, "{}", volume(&mut opened));
|
||||
// Saved, it opens up to date.
|
||||
run(&mut opened, "file.save", json!({}));
|
||||
let mut again = Session::default();
|
||||
run(&mut again, "doc.open", json!({"path": assembly}));
|
||||
assert_eq!(changed(&mut again), json!(false));
|
||||
// A link made before the record says nothing; Update Links gives it one.
|
||||
again.doc_mut().links.values_mut().for_each(|l| l.file_sha256 = None);
|
||||
assert_eq!(changed(&mut again), Value::Null);
|
||||
assert_eq!(run(&mut again, "component.update_links", json!({}))["unchanged"], json!(["p"]));
|
||||
assert_eq!(changed(&mut again), json!(false));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserting_a_missing_file_or_bad_parameters_changes_nothing() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -46,6 +46,13 @@ mod imp {
|
||||
touched();
|
||||
std::fs::metadata(path).map(|m| m.len())
|
||||
}
|
||||
/// The file's modification time and size (`None`: not there, or the platform keeps no times).
|
||||
pub fn stamp(path: &str) -> Option<(std::time::SystemTime, u64)> {
|
||||
check(path).ok()?;
|
||||
touched();
|
||||
let meta = std::fs::metadata(path).ok()?;
|
||||
Some((meta.modified().ok()?, meta.len()))
|
||||
}
|
||||
pub fn exists(path: &str) -> bool {
|
||||
check(path).is_ok() && {
|
||||
touched();
|
||||
@@ -108,6 +115,10 @@ mod imp {
|
||||
pub fn len(path: &str) -> Result<u64> {
|
||||
FILES.with(|f| f.borrow().get(path).map(|b| b.len() as u64)).ok_or_else(|| missing(path))
|
||||
}
|
||||
/// Browser storage keeps no modification times.
|
||||
pub fn stamp(_path: &str) -> Option<(std::time::SystemTime, u64)> {
|
||||
None
|
||||
}
|
||||
pub fn exists(path: &str) -> bool {
|
||||
FILES.with(|f| f.borrow().contains_key(path))
|
||||
}
|
||||
@@ -566,6 +577,12 @@ pub fn design_path_is_file(path: &str, design: Option<&str>) -> Option<bool> {
|
||||
design_path_refusal(path, design).is_none().then(|| is_file(path))
|
||||
}
|
||||
|
||||
/// [`stamp`] of a path a design names; `None` when [`check_design_path`] refuses it, which asks
|
||||
/// nothing of the disk or the network.
|
||||
pub fn design_path_stamp(path: &str, design: Option<&str>) -> Option<(std::time::SystemTime, u64)> {
|
||||
design_path_refusal(path, design).is_none().then(|| stamp(path)).flatten()
|
||||
}
|
||||
|
||||
/// The checks the layers below ask before reading a file a design names (sketch text's font
|
||||
/// files). Done once; every session and agent scope does it.
|
||||
pub fn install_checks() {
|
||||
|
||||
@@ -378,11 +378,13 @@ pub fn adopt(app: &mut SolveApp, session: Session) {
|
||||
app.session.replace_design(session);
|
||||
// The blank design's record of its links goes with it; the new one's start now.
|
||||
app.docs.link_stamps.clear();
|
||||
crate::linked_source::note_unread(app);
|
||||
crate::render_workspace::reset_document(app);
|
||||
app.session.invalidate_view(rev);
|
||||
app.cam = Camera::default();
|
||||
app.fit_view();
|
||||
// The linked designs that changed while it was closed are read again (#9739): last, as a
|
||||
// read is taken up only by the design revision it started from.
|
||||
crate::linked_source::update_changed(app);
|
||||
} else if let Err(e) = open_tab(app, session) {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
|
||||
@@ -156,26 +156,44 @@ pub(crate) fn record(stamps: &mut Vec<(u64, Stamp)>, component: u64, stamp: Stam
|
||||
}
|
||||
}
|
||||
|
||||
/// The shown design's linked components whose files are no longer what the design last read
|
||||
/// (`component.links` `changed`), as when one changed while the design was closed (#9739).
|
||||
fn changed_since_read(session: &mut solidcraft_engine::Session) -> Vec<u64> {
|
||||
let Ok(links) = session.execute("component.links", &json!({})) else { return Vec::new() };
|
||||
links["links"].as_array().into_iter().flatten().filter(|l| l["changed"].as_bool() == Some(true)).filter_map(|l| l["component"].as_u64()).collect()
|
||||
}
|
||||
|
||||
/// The links of the shown design it holds no record of yet (it was just opened, a link was just
|
||||
/// made) are taken as their files are now.
|
||||
/// made) are taken as their files are now, except a file that changed since the design last read
|
||||
/// it (#9739): that one counts as not read yet, so [`update_changed`] reads it.
|
||||
pub(crate) fn note_unread(app: &mut SolveApp) {
|
||||
if app.session.doc.links.keys().all(|c| app.docs.link_stamps.iter().any(|(k, _)| k == c)) {
|
||||
return;
|
||||
}
|
||||
for (c, s) in disk_stamps(app) {
|
||||
let now = disk_stamps(app);
|
||||
let stale = if now.is_empty() { Vec::new() } else { changed_since_read(&mut app.session) };
|
||||
for (c, s) in now {
|
||||
if !app.docs.link_stamps.iter().any(|(k, _)| *k == c) {
|
||||
app.docs.link_stamps.push((c, s));
|
||||
app.docs.link_stamps.push((c, if stale.contains(&c) { None } else { s }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The shown design came back to its tab: read again, off the window thread as Update Links
|
||||
/// does, the linked designs whose files changed since it last read them. Each is a run of its
|
||||
/// own, so one that cannot be read holds up no other; one that fails is tried again next time.
|
||||
/// The shown design came back to its tab, or was just opened: read again, off the window thread
|
||||
/// as Update Links does, the linked designs whose files changed since it last read them. Each is
|
||||
/// a run of its own, so one that cannot be read holds up no other; one that fails is tried again
|
||||
/// next time.
|
||||
pub(crate) fn update_changed(app: &mut SolveApp) {
|
||||
let now = disk_stamps(app);
|
||||
let changed: Vec<u64> =
|
||||
now.iter().filter(|(c, s)| s.is_some() && app.docs.link_stamps.iter().any(|(k, was)| k == c && was != s)).map(|(c, _)| *c).collect();
|
||||
// Links met here for the first time: those whose files changed since the design last read
|
||||
// them (#9739) are read again too.
|
||||
let first: Vec<u64> = now.iter().map(|(c, _)| *c).filter(|c| !app.docs.link_stamps.iter().any(|(k, _)| k == c)).collect();
|
||||
let stale = if first.is_empty() { Vec::new() } else { changed_since_read(&mut app.session) };
|
||||
let changed: Vec<u64> = now
|
||||
.iter()
|
||||
.filter(|(c, s)| s.is_some() && (app.docs.link_stamps.iter().any(|(k, was)| k == c && was != s) || (first.contains(c) && stale.contains(c))))
|
||||
.map(|(c, _)| *c)
|
||||
.collect();
|
||||
for (c, s) in now {
|
||||
if !app.docs.link_stamps.iter().any(|(k, _)| *k == c) {
|
||||
app.docs.link_stamps.push((c, s));
|
||||
@@ -601,6 +619,36 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(folder);
|
||||
}
|
||||
|
||||
/// #9739: a linked design changed while the assembly was closed is read when the assembly
|
||||
/// opens, in place of the blank design or in a tab of its own. Nothing changed, nothing is.
|
||||
#[test]
|
||||
fn a_design_changed_while_the_assembly_was_closed_is_read_when_it_opens() {
|
||||
let (app, folder, plate, _) = two_links("closed");
|
||||
drop(app);
|
||||
let case = folder.join("case.solidcraft");
|
||||
let open = |in_tab: bool| {
|
||||
let mut app = SolveApp::new(Session::default(), Default::default());
|
||||
if in_tab {
|
||||
app.run("solid.box", json!({"length": 1, "width": 1, "height": 1})).unwrap();
|
||||
}
|
||||
let mut opened = Session::default();
|
||||
opened.execute("doc.open", &json!({"path": case})).unwrap();
|
||||
crate::documents::adopt(&mut app, opened);
|
||||
assert_eq!(app.docs.count(), if in_tab { 2 } else { 1 });
|
||||
app
|
||||
};
|
||||
let app = open(false);
|
||||
assert!(app.file_load.idle(), "nothing changed: nothing to read");
|
||||
assert!((volume(&app) - 7000.0).abs() < 1e-6, "{}", volume(&app));
|
||||
save_part(&plate, [10.0, 20.0, 30.0], true);
|
||||
for in_tab in [false, true] {
|
||||
let mut app = open(in_tab);
|
||||
settle(&mut app);
|
||||
assert!((volume(&app) - 7250.0).abs() < 1e-6, "in a tab of its own: {in_tab}: {}", volume(&app));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_a_close_all_reads_what_the_tab_left_in_front_missed() {
|
||||
let (assembly, folder, plate, _) = two_links("cancelled");
|
||||
|
||||
@@ -385,7 +385,7 @@ Mesh** would. The part's solid bodies come
|
||||
in as they sit in that design, and they are kept in the assembly, so it opens even without the
|
||||
part file. The link is stored relative to the assembly's folder once the assembly is saved.
|
||||
After the part file changes, **Update Links** (`component.update_links`) reads it again.
|
||||
An assembly whose tab comes back to the front does that by itself for the parts whose files
|
||||
An assembly that opens, or whose tab comes back to the front, does that by itself for the parts whose files
|
||||
changed since it last read them, whether the part was saved in another tab, another window or by
|
||||
a checkout. A part that cannot be read is tried again the next time, and holds up no other.
|
||||
**Update changed linked designs when a tab comes back** (Preferences, Design,
|
||||
@@ -393,7 +393,8 @@ a checkout. A part that cannot be read is tried again the next time, and holds u
|
||||
To edit the source, right-click a linked component or occurrence in the Browser and choose
|
||||
**Open Linked Design**. It opens the part in another tab and reports if its file is missing.
|
||||
**Break Link** (`component.break_link`) keeps the bodies and forgets the file, and
|
||||
`component.links` lists the links and whether their files are found. A linked component holds
|
||||
`component.links` lists the links, whether their files are found, and whether they changed since
|
||||
the assembly last read them (`changed`). A linked component holds
|
||||
the part's bodies only: its timeline, parameters and sheet-metal data (flat pattern, bend
|
||||
table) stay in the part's own design.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user