git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/soft_transform_tool.rs (3.9K)

  1 //! The soft-transform viewer state: place the falloff centre and the offset by
  2 //! dragging, instead of typing six numbers.
  3 //!
  4 //! The second [`HandleSource`], and deliberately a different shape from the
  5 //! curve's — an abstraction with one implementation has not been shown to be
  6 //! one. Where a curve is an open-ended list of world positions stored as world
  7 //! positions, a soft transform has exactly TWO handles and only one of them is
  8 //! a position:
  9 //!
 10 //! - **Centre** — where the falloff is centred, a world position, stored as
 11 //!   one.
 12 //! - **Tip** — drawn at `Centre + Translation`, because a translation is not a
 13 //!   place: it is how far things move.
 14 //!
 15 //! So the pair reads as a vector with a base and a tip, and dragging EITHER
 16 //! end changes the offset between them — moving the centre keeps the tip where
 17 //! it is and shortens or lengthens the translation to match. That is the
 18 //! behaviour a two-handled gizmo has everywhere, and it is also the only one
 19 //! this trait can express honestly: `write` is handed a full set of handles
 20 //! with no word about which moved, and it has to mean the same thing when the
 21 //! set came from an undo snapshot as when it came from a drag. A rule like
 22 //! "keep the translation when the centre moves" would be right for the drag
 23 //! and would quietly discard half of every restored snapshot.
 24 //!
 25 //! The conversion between stored parameters and world handles is exactly what
 26 //! [`HandleSource`] exists to contain. The framework's drag maths only ever
 27 //! sees two world positions, and needs no idea that one of them is derived —
 28 //! which is the property that makes it a framework rather than the curve tool
 29 //! with the names changed.
 30 //!
 31 //! Handles are fixed, so the source is not extensible: a press on empty space
 32 //! grabs nothing and a third handle would mean nothing.
 33 
 34 use crate::app::FsNode;
 35 use crate::geometry::node_param_str;
 36 use crate::viewer_state::HandleSource;
 37 use glam::Vec3;
 38 
 39 pub struct SoftTransformHandles;
 40 
 41 /// Read a `x:y:z` (or whitespace/comma separated) triple parameter.
 42 ///
 43 /// Soft Transform stores Translation and Center as `text`, not `float3`, so
 44 /// the separator it was saved with depends on which pane wrote it last —
 45 /// accept any of them rather than silently reading a zero.
 46 fn triple(node: &FsNode, name: &str) -> Vec3 {
 47     let raw = node_param_str(node, name, "");
 48     let n: Vec<f32> = raw
 49         .split(|c: char| c == ':' || c == ',' || c.is_whitespace())
 50         .filter(|t| !t.is_empty())
 51         .filter_map(|t| t.parse::<f32>().ok())
 52         .collect();
 53     if n.len() == 3 && n.iter().all(|v| v.is_finite()) {
 54         Vec3::new(n[0], n[1], n[2])
 55     } else {
 56         Vec3::ZERO
 57     }
 58 }
 59 
 60 fn set_triple(node: &mut FsNode, name: &str, v: Vec3) {
 61     // Written back in the same `x:y:z` form the templates ship, so a value the
 62     // tool wrote and a value the pane wrote are indistinguishable.
 63     let formatted = format!("{:.2}:{:.2}:{:.2}", v.x, v.y, v.z);
 64     if let Some(p) = node.params.iter_mut().find(|p| p.name == name) {
 65         p.default = formatted;
 66     }
 67 }
 68 
 69 impl HandleSource for SoftTransformHandles {
 70     fn name(&self) -> &'static str {
 71         "Soft Transform"
 72     }
 73 
 74     fn accepts(&self, node_type: &str) -> bool {
 75         node_type.eq_ignore_ascii_case("soft_transform")
 76     }
 77 
 78     fn read(&self, node: &FsNode) -> Vec<Vec3> {
 79         let centre = triple(node, "Center");
 80         vec![centre, centre + triple(node, "Translation")]
 81     }
 82 
 83     fn write(&self, node: &mut FsNode, handles: &[Vec3]) {
 84         let [centre, offset] = handles else { return };
 85         set_triple(node, "Center", *centre);
 86         set_triple(node, "Translation", *offset - *centre);
 87     }
 88 
 89     fn extensible(&self) -> bool {
 90         false
 91     }
 92 
 93     fn hints(&self) -> &'static str {
 94         "drag the base to place the falloff, the tip to set the offset"
 95     }
 96 
 97     fn handle_label(&self, i: usize) -> String {
 98         if i == 0 { "centre".into() } else { "tip".into() }
 99     }
100 }