forked from 1b-agent/fieldcraft-upstream
engine: mesh.node.move's live preview, capped before its costly part
prepare_capped counts the nodes that would follow and stops before the design's faces and the harmonic solve when they are more than a cap; preview_live reuses the region's baseline and guards more coarsely. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
1dd2afe525
commit
8a178879ee
3 files changed
+62
-4
No files matched your search
@@ -20,7 +20,10 @@ pub use datum::edit_params as datum_edit_params;
|
||||
pub use datum::preview as datum_preview;
|
||||
pub use fieldcraft_mesh::relax::{ElementChange, Moved as NodeMoved};
|
||||
pub use mesh::analytic_surfaces;
|
||||
pub use node_move::{Prepared as NodeMovePrepared, nearest_node as node_move_nearest, prepare as node_move_prepare};
|
||||
pub use node_move::{
|
||||
Capped as NodeMoveCapped, Prepared as NodeMovePrepared, nearest_node as node_move_nearest, prepare as node_move_prepare,
|
||||
prepare_capped as node_move_prepare_capped,
|
||||
};
|
||||
pub use nodes::merge_preview as node_merge_preview;
|
||||
pub use plan::untagged_faces;
|
||||
pub use relax::{Preview as RelaxPreview, preview as relax_preview, sphere as relax_sphere};
|
||||
|
||||
@@ -55,8 +55,14 @@ pub struct Prepared {
|
||||
pub sphere: Option<([f64; 3], f64)>,
|
||||
guard: Guard,
|
||||
geometry: Geometry,
|
||||
/// The region's elements' quality before the move, measured by the first live preview.
|
||||
baseline: std::sync::OnceLock<Vec<(f64, bool)>>,
|
||||
}
|
||||
|
||||
/// Halvings of the guard's bisection in a live preview (the scale to within 2⁻⁶; the command
|
||||
/// itself finds it to 2⁻²⁰).
|
||||
const PREVIEW_GUARD_STEPS: usize = 6;
|
||||
|
||||
impl Prepared {
|
||||
/// Nodes that take a share of the displacement.
|
||||
pub fn nodes(&self) -> usize {
|
||||
@@ -85,6 +91,17 @@ impl Prepared {
|
||||
pub fn preview(&self, s: &Session, d: [f64; 3]) -> Result<Moved> {
|
||||
Ok(relax::move_nodes(&s.model, &self.weights, d, self.guard, self.geometry.constraints())?)
|
||||
}
|
||||
|
||||
/// [`Prepared::preview`] for a drag frame: the region's quality before the move is measured
|
||||
/// once and reused, and the guard's scale is found more coarsely.
|
||||
pub fn preview_live(&self, s: &Session, d: [f64; 3]) -> Result<Moved> {
|
||||
let run = relax::MoveRun { baseline: self.baseline.get().cloned(), guard_steps: PREVIEW_GUARD_STEPS };
|
||||
let m = relax::move_nodes_with(&s.model, &self.weights, d, self.guard, self.geometry.constraints(), &run)?;
|
||||
if self.baseline.get().is_none() {
|
||||
let _ = self.baseline.set(m.elements.iter().map(|c| (c.before, c.valid_before)).collect());
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
|
||||
/// The corner node nearest `p`: the node a drag in the view grabs.
|
||||
@@ -105,6 +122,21 @@ fn positive(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||
|
||||
/// Everything of `mesh.node.move` but its displacement.
|
||||
pub fn prepare(s: &Session, p: &Value) -> Result<Prepared> {
|
||||
match prepare_capped(s, p, None)? {
|
||||
Capped::Ready(prep) => Ok(*prep),
|
||||
Capped::TooLarge(n) => Err(EngineError::Params(format!("{n} nodes to move"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`prepare`], or the count of the nodes that would follow when it is more than `cap`: the
|
||||
/// costly part (the design's faces, the harmonic solve) is then not done.
|
||||
pub enum Capped {
|
||||
Ready(Box<Prepared>),
|
||||
TooLarge(usize),
|
||||
}
|
||||
|
||||
/// [`prepare`] that stops before the costly part when more than `cap` nodes would follow.
|
||||
pub fn prepare_capped(s: &Session, p: &Value, cap: Option<usize>) -> Result<Capped> {
|
||||
let model = &s.model;
|
||||
let propagate = match super::opt::<String>(p, "propagate")? {
|
||||
None => Propagate::Falloff,
|
||||
@@ -166,9 +198,14 @@ pub fn prepare(s: &Session, p: &Value) -> Result<Prepared> {
|
||||
let influence = Influence { centre: if propagate == Propagate::Falloff { centre } else { None }, radius, propagate, strength, falloff };
|
||||
// The constraints of every node that might take a share: the grabbed ones and those within
|
||||
// the radius (all of the mesh's corner nodes when the region is not known yet).
|
||||
let candidates =
|
||||
relax::move_weights(model, &handles, &Influence { propagate: Propagate::Falloff, falloff: Falloff::None, ..influence }, &Unconstrained)?;
|
||||
if let Some(cap) = cap
|
||||
&& candidates.len() > cap
|
||||
{
|
||||
return Ok(Capped::TooLarge(candidates.len()));
|
||||
}
|
||||
let geometry = if on_geometry {
|
||||
let candidates =
|
||||
relax::move_weights(model, &handles, &Influence { propagate: Propagate::Falloff, falloff: Falloff::None, ..influence }, &Unconstrained)?;
|
||||
let mut nodes: BTreeSet<u32> = candidates.keys().copied().collect();
|
||||
// Their elements' nodes too: the mid-side nodes that follow.
|
||||
for e in model.elements().values() {
|
||||
@@ -185,7 +222,7 @@ pub fn prepare(s: &Session, p: &Value) -> Result<Prepared> {
|
||||
return Err(EngineError::Params("no mesh nodes to move: the nodes given are no element's corners, or none lies in the sphere".into()));
|
||||
}
|
||||
let sphere = radius.and_then(|r| centre.or(origin).map(|c| (c, r)));
|
||||
Ok(Prepared { weights, handles, origin, sphere, guard, geometry })
|
||||
Ok(Capped::Ready(Box::new(Prepared { weights, handles, origin, sphere, guard, geometry, baseline: std::sync::OnceLock::new() })))
|
||||
}
|
||||
|
||||
/// The quality of the region as JSON, with its mean scaled Jacobian.
|
||||
|
||||
@@ -254,3 +254,21 @@ fn a_move_of_nothing_records_no_undo_step_and_min_quality_is_adjustable() {
|
||||
let (loose, strict) = (scale(&mut s, 0.0), scale(&mut s, 0.5));
|
||||
assert!(strict.0 < loose.0 && strict.1 >= 0.5 - 1e-9, "{loose:?} {strict:?}");
|
||||
}
|
||||
|
||||
/// The live preview's cap: more followers than it allows are counted, not prepared; the live
|
||||
/// preview is the command's move.
|
||||
#[test]
|
||||
fn a_capped_prepare_counts_a_large_region_and_the_live_preview_matches() {
|
||||
let s = plate();
|
||||
let n = interior(&s.model);
|
||||
let p = json!({"nodes": [n], "by": [0.2, 0.1, 0.0], "radius": 4.0, "propagate": "harmonic"});
|
||||
assert!(matches!(prepare_capped(&s, &p, Some(3)).unwrap(), Capped::TooLarge(k) if k > 3));
|
||||
let Capped::Ready(prep) = prepare_capped(&s, &p, None).unwrap() else { panic!("not prepared") };
|
||||
let (exact, first, again) = (
|
||||
prep.preview(&s, [0.2, 0.1, 0.0]).unwrap(),
|
||||
prep.preview_live(&s, [0.2, 0.1, 0.0]).unwrap(),
|
||||
prep.preview_live(&s, [0.2, 0.1, 0.0]).unwrap(),
|
||||
);
|
||||
assert_eq!(exact.positions, first.positions);
|
||||
assert_eq!(first, again, "the reused baseline gives the same move");
|
||||
}
|
||||
Reference in new issue
Block a user