Live operation previews: open feature dialogs (new or Edit Feature) evaluate on a scratch session off the UI thread (Session::preview, no document or undo changes) and draw the result in place: new surface tinted, removed material as a translucent ghost; errors show in the dialog and keep the last good preview; a failing join/cut still shows its tool body
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
b8d0fcc8e2
commit
d44083d1cc
8 files changed
+548
-23
No files matched your search
@@ -17,7 +17,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use solvecraft_doc::{Document, Model};
|
||||
use solvecraft_doc::{Document, Model, ModelState};
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
|
||||
@@ -122,6 +122,16 @@ pub struct Snapshot {
|
||||
pub active_sketch: Option<u64>,
|
||||
}
|
||||
|
||||
/// What some commands would make, evaluated on a scratch copy of a session
|
||||
/// ([`Session::preview`]). The session itself is not changed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Preview {
|
||||
/// The model now.
|
||||
pub before: Arc<ModelState>,
|
||||
/// The model after the commands.
|
||||
pub after: Arc<ModelState>,
|
||||
}
|
||||
|
||||
/// An editing session on one design.
|
||||
pub struct Session {
|
||||
pub doc: Arc<Document>,
|
||||
@@ -240,6 +250,52 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// A scratch copy of the session (same design and model, no history or log) to try
|
||||
/// commands on without touching this one.
|
||||
pub fn scratch(&self) -> Session {
|
||||
Session {
|
||||
doc: self.doc.clone(),
|
||||
model: self.model.clone(),
|
||||
undo: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
active_sketch: self.active_sketch,
|
||||
selection: Vec::new(),
|
||||
path: None,
|
||||
saved: self.saved.clone(),
|
||||
log: Vec::new(),
|
||||
revision: self.revision,
|
||||
}
|
||||
}
|
||||
|
||||
/// Live preview: run `commands` (id, parameters) on a scratch copy and return the model
|
||||
/// before and after. Nothing in this session changes (document, undo history, selection).
|
||||
/// A `timeline.redefine` is shown even while the timeline is rolled back to the feature
|
||||
/// being edited.
|
||||
pub fn preview(&self, commands: &[(String, Value)]) -> Result<Preview> {
|
||||
self.scratch().preview_in_place(commands).map(|after| Preview { before: self.model.state(), after })
|
||||
}
|
||||
|
||||
fn preview_in_place(mut self, commands: &[(String, Value)]) -> Result<Arc<ModelState>> {
|
||||
if commands.is_empty() {
|
||||
return Err(EngineError::Other("nothing to preview".into()));
|
||||
}
|
||||
for (id, params) in commands {
|
||||
let r = self.execute(id, params)?;
|
||||
if id == "timeline.redefine"
|
||||
&& let Some(fid) = r.get("feature").and_then(Value::as_u64)
|
||||
&& let Some(idx) = self.doc.feature_index(fid)
|
||||
&& self.doc.marker.is_some_and(|m| m <= idx)
|
||||
{
|
||||
self.doc_mut().marker = Some(idx + 1);
|
||||
self.refresh();
|
||||
if let Some(e) = self.model.result(fid).and_then(|r| r.error.clone()) {
|
||||
return Err(EngineError::Other(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.model.state())
|
||||
}
|
||||
|
||||
/// Run a script: a JSON array of `{"command": id, "params": {...}}` (or an object with a
|
||||
/// `commands` array). Stops at the first error. Returns each command's result.
|
||||
pub fn run_script(&mut self, script: &Value) -> Result<Vec<Value>> {
|
||||
|
||||
@@ -473,3 +473,55 @@ fn step_insert_shell_and_errors() {
|
||||
assert!(d.execute("FusionImportCommandFromToolbar", &json!({"path": p.with_extension("stl").to_string_lossy()})).is_err());
|
||||
assert_eq!(*d.doc, *before);
|
||||
}
|
||||
|
||||
fn body_volume(st: &solvecraft_doc::ModelState) -> f64 {
|
||||
st.bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum()
|
||||
}
|
||||
|
||||
/// Previews evaluate on a scratch copy: the document, undo history and model are untouched.
|
||||
#[test]
|
||||
fn preview_has_no_side_effects() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
let doc = s.doc.clone();
|
||||
let (undo, rev) = (s.undo.len(), s.revision);
|
||||
let p = s.preview(&[("Extrude".into(), json!({"distance": 15}))]).unwrap();
|
||||
assert!(p.before.bodies.is_empty());
|
||||
assert!(rel(body_volume(&p.after), 40.0 * 30.0 * 15.0) < 1e-9);
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!((s.undo.len(), s.revision), (undo, rev));
|
||||
assert!(s.model.state().bodies.is_empty());
|
||||
// Errors come back as errors and change nothing either.
|
||||
assert!(s.preview(&[("Extrude".into(), json!({"distance": "nonsense"}))]).is_err());
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
|
||||
// Fillet preview on a real body.
|
||||
run(&mut s, "Extrude", json!({"distance": 20}));
|
||||
let (doc, undo) = (s.doc.clone(), s.undo.len());
|
||||
let p = s.preview(&[("FusionFilletEdgesCommand".into(), json!({"edges": [[0, 0, 10]], "radius": 3}))]).unwrap();
|
||||
let filleted = 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0;
|
||||
assert!(rel(body_volume(&p.after), filleted) < 2e-4);
|
||||
assert!(rel(body_volume(&p.before), 24000.0) < 1e-9);
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
}
|
||||
|
||||
/// Previewing an edit shows the rebuilt feature although the timeline is rolled back to it.
|
||||
#[test]
|
||||
fn preview_of_an_edit() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
let r = run(&mut s, "Extrude", json!({"distance": 20}));
|
||||
let fid = r["feature"].as_u64().unwrap();
|
||||
let idx = s.doc.feature_index(fid).unwrap();
|
||||
run(&mut s, "timeline.rollTo", json!({"position": idx}));
|
||||
assert!(s.model.state().bodies.is_empty());
|
||||
let doc = s.doc.clone();
|
||||
let p = s.preview(&[("timeline.redefine".into(), json!({"feature": fid, "command": "Extrude", "params": {"distance": 10}}))]).unwrap();
|
||||
assert!(rel(body_volume(&p.after), 12000.0) < 1e-9);
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
}
|
||||
Reference in new issue
Block a user