forked from 1b-agent/fieldcraft-upstream
ui: Move Nodes prepares once the form settles and skips large regions
A changed form (Alt + scroll on the radius) is prepared 150 ms after it last changed, the sphere drawn meanwhile; a region of more than 1000 nodes is not previewed and moves on release. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
8a178879ee
commit
c1941ad349
3 files changed
+115
-20
No files matched your search
@@ -1614,7 +1614,7 @@ impl ModelApp {
|
||||
let cut = self.partition_preview();
|
||||
let merge = self.merge_preview();
|
||||
let relax_key = self.dialog.as_ref().filter(|d| d.command == "mesh.node.relax").map(|d| d.text.clone());
|
||||
let move_key = self.dialog.as_ref().filter(|d| d.command == node_move::COMMAND).map(|d| d.text.clone());
|
||||
let move_key = self.dialog.as_ref().filter(|d| d.command == node_move::COMMAND).map(|d| (d.text.clone(), self.node_move_pending()));
|
||||
let selected = hash(&|h| (scene, self.pick.generation, &set, &cut, &*merge, &relax_key, &move_key).hash(h));
|
||||
let key = hash(&|h| (selected, self.pick.hover_generation).hash(h));
|
||||
if self.pick.overlay.0 == Some(key) {
|
||||
@@ -2606,6 +2606,12 @@ mod tests {
|
||||
!overlay.fills.is_empty() && !overlay.lines.is_empty() && !overlay.on_top.is_empty(),
|
||||
"sphere, ghost and moved edges, the grabbed node"
|
||||
);
|
||||
// A changed radius waits out the debounce (the sphere drawn meanwhile), then prepares.
|
||||
assert!(app.node_move_scale_radius(1.5));
|
||||
let waiting = app.node_move_preview().unwrap();
|
||||
assert!(app.node_move_pending() && waiting.moved.is_none() && waiting.sphere.is_some());
|
||||
app.now += node_move::DEBOUNCE * 2.0;
|
||||
assert!(app.node_move_preview().unwrap().moved.is_some() && !app.node_move_pending());
|
||||
// Esc during a drag: nothing moves, the form stays.
|
||||
let start = app.session.model.clone();
|
||||
app.node_drag = Some(node_move::Drag { start: Vec3::new(2.0, 2.0, 2.0), normal: Vec3::Z });
|
||||
|
||||
+100
-19
@@ -15,7 +15,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_engine::cmd::{NodeMovePrepared, NodeMoved};
|
||||
use fieldcraft_engine::cmd::{NodeMoveCapped, NodeMovePrepared, NodeMoved};
|
||||
use fieldcraft_render::{Scene, SceneLine, Vec3};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -25,8 +25,25 @@ use crate::{ModelApp, scene};
|
||||
/// The command the tool runs.
|
||||
pub const COMMAND: &str = "mesh.node.move";
|
||||
|
||||
/// Most nodes a live preview moves: a larger region is moved on release only.
|
||||
pub const PREVIEW_NODES: usize = 20_000;
|
||||
/// Most nodes a live preview moves: a larger region is moved on release only. On the crank arm's
|
||||
/// hex20 mesh (8264 elements) a region of 492 nodes prepares in 20 ms (falloff) to 27 ms
|
||||
/// (harmonic) and draws a frame in 12 to 52 ms; one of 4051 nodes takes 0.8 s to prepare
|
||||
/// harmonically and 0.3 s a frame, too slow to follow the pointer.
|
||||
pub const PREVIEW_NODES: usize = 1_000;
|
||||
|
||||
/// How long the form must stay the same (seconds) before a changed move is prepared again: Alt +
|
||||
/// scroll changes the radius every frame, and preparing (the design's faces, the harmonic
|
||||
/// solve) takes longer than a frame on a big region.
|
||||
pub const DEBOUNCE: f64 = 0.15;
|
||||
|
||||
/// The prepared part of the move.
|
||||
#[derive(Clone)]
|
||||
enum Prep {
|
||||
Ready(Arc<NodeMovePrepared>),
|
||||
/// More than [`PREVIEW_NODES`] would follow: not prepared, applied on release only.
|
||||
TooLarge(usize),
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
/// A drag of the Move Nodes tool in progress.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -41,7 +58,10 @@ pub struct Drag {
|
||||
#[derive(Default)]
|
||||
pub struct Cache {
|
||||
/// The move up to its displacement, by model revision and form text without `by` and `to`.
|
||||
prepared: Option<((u64, String), Result<Arc<NodeMovePrepared>, String>)>,
|
||||
prepared: Option<((u64, String), Prep)>,
|
||||
/// The form (without the displacement) last seen and when (app time): a change waits
|
||||
/// [`DEBOUNCE`] before it is prepared again.
|
||||
seen: Option<((u64, String), f64)>,
|
||||
/// The move itself, by model revision and form text.
|
||||
preview: Option<((u64, String), Option<Arc<Preview>>)>,
|
||||
}
|
||||
@@ -98,18 +118,70 @@ impl ModelApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// The open form's move up to its displacement (cached by form and revision).
|
||||
fn move_prepared(&self, params: &Value) -> Result<Arc<NodeMovePrepared>, String> {
|
||||
/// The open form's move up to its displacement, cached by form and revision; `None` while a
|
||||
/// changed form waits out [`DEBOUNCE`] (the last one is kept meanwhile, never prepared on
|
||||
/// every frame of an Alt + scroll).
|
||||
fn move_prepared(&self, params: &Value) -> Option<Prep> {
|
||||
let key = (self.revision, without_displacement(params));
|
||||
let mut cache = self.node_move.borrow_mut();
|
||||
if let Some((k, p)) = &cache.prepared
|
||||
&& *k == key
|
||||
{
|
||||
return p.clone();
|
||||
return Some(p.clone());
|
||||
}
|
||||
let p = fieldcraft_engine::cmd::node_move_prepare(&self.session, params).map(Arc::new).map_err(|e| e.to_string());
|
||||
if cache.prepared.is_some() {
|
||||
match &cache.seen {
|
||||
Some((k, at)) if *k == key => {
|
||||
if self.now - at < DEBOUNCE {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
cache.seen = Some((key, self.now));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = match fieldcraft_engine::cmd::node_move_prepare_capped(&self.session, params, Some(PREVIEW_NODES)) {
|
||||
Ok(NodeMoveCapped::Ready(p)) => Prep::Ready(Arc::from(p)),
|
||||
Ok(NodeMoveCapped::TooLarge(n)) => Prep::TooLarge(n),
|
||||
Err(e) => Prep::Failed(e.to_string()),
|
||||
};
|
||||
cache.prepared = Some((key, p.clone()));
|
||||
p
|
||||
cache.seen = None;
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// Whether a changed Move Nodes form waits to be prepared (the view draws again after
|
||||
/// [`DEBOUNCE`]).
|
||||
pub fn node_move_pending(&self) -> bool {
|
||||
self.node_move.borrow().seen.is_some()
|
||||
}
|
||||
|
||||
/// The sphere the form describes, without preparing the move: its centre, or the listed
|
||||
/// nodes' centroid, and its radius.
|
||||
fn quick_sphere(&self, params: &Value) -> Option<([f64; 3], f64)> {
|
||||
let r = params.get("radius").and_then(Value::as_f64).filter(|r| *r > 0.0 && r.is_finite())?;
|
||||
let xyz = |v: &Value| -> Option<[f64; 3]> {
|
||||
let a = v.as_array()?;
|
||||
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?])
|
||||
};
|
||||
if let Some(c) = params.get("center").and_then(xyz) {
|
||||
return Some((c, r));
|
||||
}
|
||||
let pts: Vec<[f64; 3]> = params
|
||||
.get("nodes")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|n| n.as_u64().and_then(|n| u32::try_from(n).ok()))
|
||||
.filter_map(|n| self.session.model.node(n).map(|p| p.xyz))
|
||||
.collect();
|
||||
if pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let k = 1.0 / pts.len() as f64;
|
||||
Some((pts.iter().fold([0.0; 3], |a, p| [a[0] + p[0] * k, a[1] + p[1] * k, a[2] + p[2] * k]), r))
|
||||
}
|
||||
|
||||
/// What the open Move Nodes form would do, worked out once per form text and model revision.
|
||||
@@ -121,20 +193,29 @@ impl ModelApp {
|
||||
{
|
||||
return p.clone();
|
||||
}
|
||||
let params = serde_json::from_str::<Value>(&d.text).ok();
|
||||
let preview = params.and_then(|params| {
|
||||
let prep = self.move_prepared(¶ms).ok()?;
|
||||
let mut out = Preview { sphere: prep.sphere, nodes: prep.nodes(), moved: None, note: None };
|
||||
if prep.nodes() > PREVIEW_NODES {
|
||||
out.note = Some(format!("{} nodes follow: too many for a live preview; the move applies on release", prep.nodes()));
|
||||
} else {
|
||||
match prep.displacement(¶ms).and_then(|d| prep.preview(&self.session, d)) {
|
||||
let params = serde_json::from_str::<Value>(&d.text).ok()?;
|
||||
let Some(prep) = self.move_prepared(¶ms) else {
|
||||
// Waiting out the debounce: the sphere as the form has it, nothing cached.
|
||||
return Some(Arc::new(Preview { sphere: self.quick_sphere(¶ms), nodes: 0, moved: None, note: Some("Preview: updating…".into()) }));
|
||||
};
|
||||
let preview = match prep {
|
||||
Prep::Failed(e) => Some(Preview { sphere: self.quick_sphere(¶ms), nodes: 0, moved: None, note: Some(e) }),
|
||||
Prep::TooLarge(n) => Some(Preview {
|
||||
sphere: self.quick_sphere(¶ms),
|
||||
nodes: n,
|
||||
moved: None,
|
||||
note: Some(format!("{n} nodes follow: too many for a live preview (over {PREVIEW_NODES}); the move applies on release")),
|
||||
}),
|
||||
Prep::Ready(prep) => {
|
||||
let mut out = Preview { sphere: prep.sphere, nodes: prep.nodes(), moved: None, note: None };
|
||||
match prep.displacement(¶ms).and_then(|d| prep.preview_live(&self.session, d)) {
|
||||
Ok(m) => out.moved = Some(m),
|
||||
Err(e) => out.note = Some(e.to_string()),
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
Some(Arc::new(out))
|
||||
});
|
||||
}
|
||||
.map(Arc::new);
|
||||
self.node_move.borrow_mut().preview = Some((key, preview.clone()));
|
||||
preview
|
||||
}
|
||||
|
||||
@@ -184,6 +184,14 @@ fn node_drag(app: &mut ModelApp, ui: &egui::Ui, response: &egui::Response, boxin
|
||||
{
|
||||
app.node_move_grab(at);
|
||||
}
|
||||
// A changed form is prepared once it settles (here, before the overlay is drawn): draw
|
||||
// again then.
|
||||
if app.moving_nodes() {
|
||||
app.node_move_preview();
|
||||
}
|
||||
if app.node_move_pending() {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(crate::node_move::DEBOUNCE));
|
||||
}
|
||||
if app.node_drag.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user