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 datum::preview as datum_preview;
|
||||||
pub use fieldcraft_mesh::relax::{ElementChange, Moved as NodeMoved};
|
pub use fieldcraft_mesh::relax::{ElementChange, Moved as NodeMoved};
|
||||||
pub use mesh::analytic_surfaces;
|
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 nodes::merge_preview as node_merge_preview;
|
||||||
pub use plan::untagged_faces;
|
pub use plan::untagged_faces;
|
||||||
pub use relax::{Preview as RelaxPreview, preview as relax_preview, sphere as relax_sphere};
|
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)>,
|
pub sphere: Option<([f64; 3], f64)>,
|
||||||
guard: Guard,
|
guard: Guard,
|
||||||
geometry: Geometry,
|
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 {
|
impl Prepared {
|
||||||
/// Nodes that take a share of the displacement.
|
/// Nodes that take a share of the displacement.
|
||||||
pub fn nodes(&self) -> usize {
|
pub fn nodes(&self) -> usize {
|
||||||
@@ -85,6 +91,17 @@ impl Prepared {
|
|||||||
pub fn preview(&self, s: &Session, d: [f64; 3]) -> Result<Moved> {
|
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())?)
|
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.
|
/// 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.
|
/// Everything of `mesh.node.move` but its displacement.
|
||||||
pub fn prepare(s: &Session, p: &Value) -> Result<Prepared> {
|
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 model = &s.model;
|
||||||
let propagate = match super::opt::<String>(p, "propagate")? {
|
let propagate = match super::opt::<String>(p, "propagate")? {
|
||||||
None => Propagate::Falloff,
|
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 };
|
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 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).
|
// 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 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();
|
let mut nodes: BTreeSet<u32> = candidates.keys().copied().collect();
|
||||||
// Their elements' nodes too: the mid-side nodes that follow.
|
// Their elements' nodes too: the mid-side nodes that follow.
|
||||||
for e in model.elements().values() {
|
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()));
|
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)));
|
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.
|
/// 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));
|
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:?}");
|
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