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 }