mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0a7c74ce5 | |||
| bd81c4622e | |||
| 95731a804a |
@@ -1406,6 +1406,156 @@ impl Model {
|
||||
self.last_recomputed = recomputed;
|
||||
self.last_restored = restored;
|
||||
}
|
||||
|
||||
/// The model after body `old` was renamed `new`, `before` and `after` being the document
|
||||
/// either side of the rename, without building anything again (#3044): a rename changes no
|
||||
/// geometry, but it changes the features that name the body, and so every fingerprint from
|
||||
/// the body's feature on, and `evaluate` would rebuild all of them. Here each result keeps its
|
||||
/// geometry and takes the new name (its bodies, threads and sheets, and its sketches' links
|
||||
/// where the rename changed them), and its fingerprint is worked out on `after`, so the next
|
||||
/// `evaluate` reuses it.
|
||||
///
|
||||
/// `false`, with the model as it was, where a full evaluation might name something else
|
||||
/// differently: a body whose name was made from `old` at evaluation (`old (2)` or `old'`, a
|
||||
/// piece split off it) and isn't kept in its feature's names, a body already called `new`
|
||||
/// at some step, a component's placed body, a message that names `old`, or results that
|
||||
/// don't match the timeline. The caller then evaluates.
|
||||
pub fn rename_body(&mut self, before: &Document, after: &Document, old: &str, new: &str) -> bool {
|
||||
if self.no_cache || old == new || self.results.len() != after.features.len() || before.features.len() != after.features.len() {
|
||||
return false;
|
||||
}
|
||||
let made_from_old = |name: &str| name.strip_prefix(old).is_some_and(|rest| rest.starts_with(" (") || rest.starts_with('\''));
|
||||
let kept = |b: &ModelBody| after.feature(b.feature).is_some_and(|f| f.body_names.contains(&b.name));
|
||||
let names_old = |m: &Option<String>| m.as_deref().is_some_and(|m| m.contains(old));
|
||||
for (r, f) in self.results.iter().zip(&after.features) {
|
||||
if r.id != f.id || r.stale || names_old(&r.error) || names_old(&r.warning) {
|
||||
return false;
|
||||
}
|
||||
if r.state.bodies.iter().any(|b| b.name == new || b.source_name.is_some() || (made_from_old(&b.name) && !kept(b))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// The links the rename changed, by sketch feature and link id: their new source and names.
|
||||
let mut links: std::collections::HashMap<(u64, &str), &solidcraft_sketch::Link> = std::collections::HashMap::new();
|
||||
for (fb, fa) in before.features.iter().zip(&after.features) {
|
||||
if let (FeatureKind::Sketch { sketch: sb, .. }, FeatureKind::Sketch { sketch: sa, .. }) = (&fb.kind, &fa.kind) {
|
||||
for (lb, la) in sb.links.iter().zip(&sa.links) {
|
||||
if lb.id == la.id && (lb.source != la.source || lb.face_name != la.face_name || lb.edge_name != la.edge_name) {
|
||||
links.insert((fa.id, la.id.as_str()), la);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rename = |st: &ModelState| -> Option<ModelState> {
|
||||
let sketch_changed = |sk: &SolvedSketch| sk.sketch.links.iter().any(|l| links.contains_key(&(sk.feature, l.id.as_str())));
|
||||
let touched = st.bodies.iter().any(|b| b.name == old)
|
||||
|| st.threads.iter().any(|t| t.body == old)
|
||||
|| st.sheets.iter().any(|sh| sh.body == old)
|
||||
|| st.sketches.iter().any(|sk| sketch_changed(sk));
|
||||
if !touched {
|
||||
return None;
|
||||
}
|
||||
let mut st = st.clone();
|
||||
for b in st.bodies.iter_mut().filter(|b| b.name == old) {
|
||||
b.name = new.to_string();
|
||||
}
|
||||
for t in st.threads.iter_mut().filter(|t| t.body == old) {
|
||||
t.body = new.to_string();
|
||||
}
|
||||
for sh in st.sheets.iter_mut().filter(|sh| sh.body == old) {
|
||||
sh.body = new.to_string();
|
||||
}
|
||||
for sk in &mut st.sketches {
|
||||
if sketch_changed(sk) {
|
||||
let solved = Arc::make_mut(sk);
|
||||
for l in &mut solved.sketch.links {
|
||||
if let Some(la) = links.get(&(solved.feature, l.id.as_str())) {
|
||||
l.source = la.source.clone();
|
||||
l.face_name.clone_from(&la.face_name);
|
||||
l.edge_name.clone_from(&la.edge_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(st)
|
||||
};
|
||||
self.renamed(after, rename);
|
||||
true
|
||||
}
|
||||
|
||||
/// The model after feature `id` was renamed `old` to `new` in `after`, without building
|
||||
/// anything again (#3044), as [`Model::rename_body`]: the feature's result, its solved
|
||||
/// sketch and its construction geometry take the new name. `false`, with the model as it
|
||||
/// was, where an evaluation might name something else differently: a body, thread or sheet
|
||||
/// whose name holds `old`, a message that names it, or results that don't match the timeline.
|
||||
pub fn rename_feature(&mut self, after: &Document, id: u64, old: &str, new: &str) -> bool {
|
||||
if self.no_cache || old == new || old.is_empty() || self.results.len() != after.features.len() {
|
||||
return false;
|
||||
}
|
||||
let names_old = |m: &Option<String>| m.as_deref().is_some_and(|m| m.contains(old));
|
||||
for (r, f) in self.results.iter().zip(&after.features) {
|
||||
if r.id != f.id || r.stale || names_old(&r.error) || names_old(&r.warning) {
|
||||
return false;
|
||||
}
|
||||
let st = &r.state;
|
||||
if st.bodies.iter().any(|b| b.name.contains(old) || b.source_name.as_deref().is_some_and(|n| n.contains(old)))
|
||||
|| st.threads.iter().any(|t| t.body.contains(old))
|
||||
|| st.sheets.iter().any(|sh| sh.body.contains(old))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let rename = |st: &ModelState| -> Option<ModelState> {
|
||||
let touched =
|
||||
st.sketches.iter().any(|sk| sk.feature == id && sk.name == old) || st.construct.iter().any(|c| c.feature == id && c.name == old);
|
||||
if !touched {
|
||||
return None;
|
||||
}
|
||||
let mut st = st.clone();
|
||||
for sk in st.sketches.iter_mut().filter(|sk| sk.feature == id && sk.name == old) {
|
||||
new.clone_into(&mut Arc::make_mut(sk).name);
|
||||
}
|
||||
for c in st.construct.iter_mut().filter(|c| c.feature == id && c.name == old) {
|
||||
new.clone_into(&mut c.name);
|
||||
}
|
||||
Some(st)
|
||||
};
|
||||
self.renamed(after, rename);
|
||||
for r in self.results.iter_mut().filter(|r| r.id == id) {
|
||||
new.clone_into(&mut r.name);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Each result with its state as `rename` makes it (`None`: as it was) and its fingerprint
|
||||
/// worked out on `after`, so the next `evaluate` reuses it; nothing is built.
|
||||
fn renamed(&mut self, after: &Document, rename: impl Fn(&ModelState) -> Option<ModelState>) {
|
||||
let (vals, _) = evaluation_values(after);
|
||||
let steps = StepDigests::of(after, &self.step_digests);
|
||||
let marker = after.marker.unwrap_or(usize::MAX);
|
||||
// Results share their states (a feature that changes nothing passes its input on): each
|
||||
// state is renamed once.
|
||||
let mut done: std::collections::HashMap<*const ModelState, Arc<ModelState>> = std::collections::HashMap::new();
|
||||
let mut prev_fp = 0u64;
|
||||
let mut out = Vec::with_capacity(self.results.len());
|
||||
for (i, (r, f)) in self.results.iter().zip(&after.features).enumerate() {
|
||||
let fp = chain_fingerprint(after, &vals, f, prev_fp, i >= marker, &steps);
|
||||
prev_fp = fp;
|
||||
let state = match done.get(&Arc::as_ptr(&r.state)) {
|
||||
Some(s) => s.clone(),
|
||||
None => {
|
||||
let s = rename(&r.state).map_or_else(|| r.state.clone(), Arc::new);
|
||||
done.insert(Arc::as_ptr(&r.state), s.clone());
|
||||
s
|
||||
}
|
||||
};
|
||||
out.push(FeatureResult { fingerprint: fp, state, ..r.clone() });
|
||||
}
|
||||
self.step_digests = steps;
|
||||
self.results = out;
|
||||
self.last_recomputed = 0;
|
||||
self.last_restored = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Key of the design's length unit (mm per unit) among the values (no parameter can be named
|
||||
|
||||
@@ -1182,3 +1182,173 @@ mod tests {
|
||||
assert_eq!(s.visibility.hidden_folders, vec!["o:4/sketches".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
/// #3044: a rename builds nothing again. The model the rename leaves is the one a full
|
||||
/// evaluation of the renamed design gives, whether it took the new name in place or, where it
|
||||
/// can't tell, evaluated.
|
||||
#[cfg(test)]
|
||||
mod rename_tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{id} {p}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a later feature or the view reads from a state: its bodies (name, feature, volume),
|
||||
/// threads, sheets and sketch link sources.
|
||||
fn summary(st: &solidcraft_doc::ModelState) -> String {
|
||||
let bodies: Vec<String> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.map(|b| format!("{} f{} {:.6}", b.name, b.feature, solidcraft_kernel::measure(&b.body).map(|m| m.volume).unwrap_or(f64::NAN)))
|
||||
.collect();
|
||||
let threads: Vec<&str> = st.threads.iter().map(|t| t.body.as_str()).collect();
|
||||
let sheets: Vec<&str> = st.sheets.iter().map(|t| t.body.as_str()).collect();
|
||||
let links: Vec<String> = st
|
||||
.sketches
|
||||
.iter()
|
||||
.flat_map(|sk| sk.sketch.links.iter().map(move |l| format!("{}:{} {:?} {:?} {:?}", sk.feature, l.id, l.source, l.face_name, l.edge_name)))
|
||||
.collect();
|
||||
let sketches: Vec<String> = st.sketches.iter().map(|sk| format!("{} {}", sk.feature, sk.name)).collect();
|
||||
let construct: Vec<String> = st.construct.iter().map(|c| format!("{} {}", c.feature, c.name)).collect();
|
||||
format!("{bodies:?} {threads:?} {sheets:?} {links:?} {sketches:?} {construct:?}")
|
||||
}
|
||||
|
||||
/// Rename body `old` to `new`; whether that built nothing again. The model then is the one a
|
||||
/// full evaluation (no cache) of the renamed design gives, feature by feature.
|
||||
fn rename(s: &mut Session, old: &str, new: &str) -> bool {
|
||||
renamed_like_evaluated(s, "body.rename", json!({"body": old, "name": new}))
|
||||
}
|
||||
|
||||
/// Rename feature `old` to `new`, as `rename` does a body.
|
||||
fn rename_feature(s: &mut Session, old: &str, new: &str) -> bool {
|
||||
renamed_like_evaluated(s, "timeline.rename", json!({"feature": old, "name": new}))
|
||||
}
|
||||
|
||||
fn renamed_like_evaluated(s: &mut Session, id: &str, p: Value) -> bool {
|
||||
let evaluations = s.evaluations;
|
||||
run(s, id, p);
|
||||
let in_place = s.evaluations == evaluations;
|
||||
let mut fresh = Session::new((*s.doc).clone());
|
||||
fresh.model.no_cache = true;
|
||||
fresh.refresh();
|
||||
assert_eq!(s.model.results.len(), fresh.model.results.len());
|
||||
for (r, f) in s.model.results.iter().zip(&fresh.model.results) {
|
||||
assert_eq!((&r.name, &r.error, &r.warning, r.skipped), (&f.name, &f.error, &f.warning, f.skipped), "feature {}", r.name);
|
||||
assert_eq!(summary(&r.state), summary(&f.state), "after feature {}", r.name);
|
||||
}
|
||||
// And the next evaluation reuses every result.
|
||||
s.refresh();
|
||||
assert_eq!(s.model.last_recomputed, 0, "a rename leaves results the next evaluation rebuilds");
|
||||
in_place
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renaming_the_sensor_enclosures_bodies_builds_nothing() {
|
||||
let mut s = Session::default();
|
||||
let script: Value = serde_json::from_str(include_str!("../../../../examples/acceptance/sensor_enclosure.json")).unwrap();
|
||||
s.run_script(&script).unwrap();
|
||||
assert!(rename(&mut s, "Case", "Shell"));
|
||||
assert!(rename(&mut s, "Lid", "Cover"));
|
||||
// Undo puts the old names back, as it puts back any edit.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.model.state().body("Lid").is_some());
|
||||
}
|
||||
|
||||
/// A body a later feature names, and a sketch on its face whose links name it; the copies a
|
||||
/// pattern made of another body, renamed one by one.
|
||||
#[test]
|
||||
fn a_rename_follows_users_face_sketches_and_copies_without_building() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "operation": "new_body", "body_name": "X"}));
|
||||
run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["X"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20}));
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [0, 30, 0], "operation": "new_body", "body_name": "Y"}));
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Y"], "translate": [0, 0, 5]}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"face": [5, 35, 15]}}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [5, 35], "radius": 2}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": -3, "operation": "cut", "profiles": [{"point": [5, 35]}]}));
|
||||
assert!(rename(&mut s, "Y", "Stand"));
|
||||
let st = s.model.state();
|
||||
let named = st.sketches.iter().flat_map(|sk| &sk.sketch.links).filter(|l| format!("{:?}", l.source).contains("\"Stand\"")).count();
|
||||
assert!(named > 0, "the face sketch's links name the renamed body");
|
||||
assert!(rename(&mut s, "X (3)", "Last"));
|
||||
assert!(rename(&mut s, "X (2)", "Middle"));
|
||||
}
|
||||
|
||||
/// The copies a pattern makes are named after their body when the pattern is evaluated:
|
||||
/// renaming the body renames them, so it evaluates, and gives what an evaluation gives.
|
||||
#[test]
|
||||
fn a_patterned_body_is_renamed_by_evaluating() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "operation": "new_body", "body_name": "X"}));
|
||||
run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["X"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20}));
|
||||
assert!(!rename(&mut s, "X", "Base"));
|
||||
let names: Vec<String> = s.model.state().bodies.iter().map(|b| b.name.clone()).collect();
|
||||
assert_eq!(names, ["Base", "Base (2)", "Base (3)"]);
|
||||
}
|
||||
|
||||
/// Features renamed: a sketch later features use, and a construction plane a sketch is on (by
|
||||
/// name). Their results, solved sketches and planes take the new name; nothing is built.
|
||||
#[test]
|
||||
fn renaming_features_builds_nothing() {
|
||||
let mut s = Session::default();
|
||||
let script: Value = serde_json::from_str(include_str!("../../../../examples/acceptance/sensor_enclosure.json")).unwrap();
|
||||
s.run_script(&script).unwrap();
|
||||
let sketch = s.doc.features.iter().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Sketch { .. })).unwrap().name.clone();
|
||||
assert!(rename_feature(&mut s, &sketch, "Outline"));
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 5, "name": "Shelf"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "Shelf"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 4}));
|
||||
assert!(rename_feature(&mut s, "Shelf", "Ledge"));
|
||||
}
|
||||
|
||||
/// Folders and components renamed: what they name isn't geometry, and the evaluation that
|
||||
/// follows reuses every result.
|
||||
#[test]
|
||||
fn renaming_folders_and_components_builds_nothing() {
|
||||
let mut s = Session::default();
|
||||
let part = run(&mut s, "component.create", json!({"name": "Part"}))["component"].as_u64().unwrap();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "name": "First", "body_name": "A"}));
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "name": "Second", "body_name": "B"}));
|
||||
let folder = run(&mut s, "browser.group", json!({"folder": "bodies", "items": ["A"]}))["group"].as_u64().unwrap();
|
||||
let features = run(&mut s, "timeline.group", json!({"name": "Pair", "features": ["First", "Second"]}))["group"].as_u64().unwrap();
|
||||
for (id, p) in [
|
||||
("browser.rename_group", json!({"group": folder, "name": "Fasteners"})),
|
||||
("timeline.rename_group", json!({"group": features, "name": "Both"})),
|
||||
("component.rename", json!({"component": part, "name": "Bracket"})),
|
||||
] {
|
||||
run(&mut s, id, p);
|
||||
assert_eq!(s.model.last_recomputed, 0, "{id} built something again");
|
||||
}
|
||||
}
|
||||
|
||||
/// A body named like a feature: an evaluation may name it after the feature, so renaming the
|
||||
/// feature evaluates, and gives what an evaluation gives.
|
||||
#[test]
|
||||
fn a_feature_a_body_is_named_like_is_renamed_by_evaluating() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "name": "Block", "body_name": "Block"}));
|
||||
assert!(!rename_feature(&mut s, "Block", "Brick"));
|
||||
}
|
||||
|
||||
/// A body split later: the pieces are named after it when the split is evaluated, so the
|
||||
/// rename evaluates, and gives what an evaluation gives.
|
||||
#[test]
|
||||
fn a_body_split_later_is_renamed_by_evaluating() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "operation": "new_body", "body_name": "Block"}));
|
||||
run(&mut s, "solid.split_body", json!({"body": "Block", "plane": {"origin": [10, 0, 0], "normal": [1, 0, 0]}}));
|
||||
let names: Vec<String> = s.model.state().bodies.iter().map(|b| b.name.clone()).collect();
|
||||
assert!(names.iter().any(|n| n.starts_with("Block (")), "{names:?}");
|
||||
assert!(!rename(&mut s, "Block", "Beam"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +344,26 @@ impl Default for Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a rename command renamed (from its result and the document before it), so the model
|
||||
/// can take the new name without building anything again (#3044).
|
||||
enum Renamed {
|
||||
Body { old: String, new: String },
|
||||
Feature { id: u64, old: String, new: String },
|
||||
}
|
||||
|
||||
impl Renamed {
|
||||
fn of(command: &str, result: &Value, before: &Document) -> Option<Renamed> {
|
||||
let new = result.get("name")?.as_str()?.to_string();
|
||||
match command {
|
||||
"body.rename" => Some(Renamed::Body { old: result.get("body")?.as_str()?.to_string(), new }),
|
||||
"timeline.rename" => {
|
||||
let id = result.get("feature")?.as_u64()?;
|
||||
Some(Renamed::Feature { id, old: before.feature(id)?.name.clone(), new })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Session {
|
||||
pub fn new(mut doc: Document) -> Self {
|
||||
// The session keeps what is hidden (not an undo step); the design file carries it.
|
||||
@@ -576,6 +596,26 @@ impl Session {
|
||||
{
|
||||
self.evaluations += 1;
|
||||
}
|
||||
self.after_evaluation();
|
||||
}
|
||||
|
||||
/// A body or feature renamed (`previous` the document before): the model takes the new
|
||||
/// name without building anything again, or is evaluated where that might not give what an
|
||||
/// evaluation gives (see `Model::rename_body` and `Model::rename_feature`, #3044).
|
||||
fn refresh_renamed(&mut self, previous: &Arc<Document>, renamed: &Renamed) {
|
||||
let in_place = match renamed {
|
||||
Renamed::Body { old, new } => self.model.rename_body(previous, &self.doc, old, new),
|
||||
Renamed::Feature { id, old, new } => self.model.rename_feature(&self.doc, *id, old, new),
|
||||
};
|
||||
if in_place {
|
||||
self.after_evaluation();
|
||||
} else {
|
||||
self.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// What follows a new model: a new revision, and what depends on the model brought up to it.
|
||||
fn after_evaluation(&mut self) {
|
||||
self.revision += 1;
|
||||
self.prune_occurrence_state();
|
||||
// Joints follow the geometry their origins snap to.
|
||||
@@ -718,6 +758,7 @@ impl Session {
|
||||
let changed = !Arc::ptr_eq(&self.doc, &before.doc);
|
||||
match r {
|
||||
Ok((v, live_edit)) => {
|
||||
let renamed = Renamed::of(spec.id, &v, &before.doc);
|
||||
let v = if changed { self.with_created(spec.id, params, &before, v) } else { v };
|
||||
if changed {
|
||||
let render_only = self.render_only_change(&before);
|
||||
@@ -729,10 +770,11 @@ impl Session {
|
||||
}
|
||||
self.redo.clear();
|
||||
}
|
||||
match live_edit {
|
||||
Some(edit) => self.refresh_live(edit),
|
||||
None if render_only => self.refresh_render(&previous_doc),
|
||||
None => self.refresh(),
|
||||
match (live_edit, renamed) {
|
||||
(Some(edit), _) => self.refresh_live(edit),
|
||||
(None, _) if render_only => self.refresh_render(&previous_doc),
|
||||
(None, Some(renamed)) => self.refresh_renamed(&previous_doc, &renamed),
|
||||
(None, None) => self.refresh(),
|
||||
}
|
||||
}
|
||||
Ok(v)
|
||||
|
||||
Reference in New Issue
Block a user