~raph/interp-toy

3ff2c40ecf16568a43dab1ffec512bcde20a2c7f — Raph Levien 2 years ago 0f8cb8e
Various refinements

Now a fake radio button choice for interpolation type (actually made
from checkbox). Progress towards better logic for editing point
positions at masters.

Also snap to latest druid (needed to get checkboxes).
7 files changed, 167 insertions(+), 29 deletions(-)

M Cargo.lock
M Cargo.toml
M src/app_state.rs
M src/interp_pane.rs
M src/main.rs
M src/master.rs
A src/radio.rs
M Cargo.lock => Cargo.lock +6 -6
@@ 183,9 183,9 @@ dependencies = [
[[package]]
name = "druid"
version = "0.3.0"
source = "git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34#6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34"
source = "git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814#88d18be0155791cac0d910f22d0552dd44ec3814"
dependencies = [
 "druid-shell 0.3.0 (git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34)",
 "druid-shell 0.3.0 (git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814)",
 "fluent-bundle 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "fluent-locale 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "fluent-syntax 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",


@@ 197,7 197,7 @@ dependencies = [
[[package]]
name = "druid-shell"
version = "0.3.0"
source = "git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34#6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34"
source = "git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814#88d18be0155791cac0d910f22d0552dd44ec3814"
dependencies = [
 "cairo-rs 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "cocoa 0.18.4 (registry+https://github.com/rust-lang/crates.io-index)",


@@ 315,7 315,7 @@ dependencies = [
name = "interp-toy"
version = "0.1.0"
dependencies = [
 "druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34)",
 "druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814)",
 "nalgebra 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "rbf-interp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]


@@ 1048,8 1048,8 @@ dependencies = [
"checksum direct2d 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7fa6ff10857eb253d1ae16987ebfd27372f4129b0c7a3fa41466fbdf7e453e75"
"checksum direct3d11 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "315aa929e68ba066cb6fb86f1b22af24f517e02fd9b5734c4d07e42cb9f4aefa"
"checksum directwrite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cdcd739e9351c411b8caf5cab32a27c818cfe06260595da121382ecdd22083d"
"checksum druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34)" = "<none>"
"checksum druid-shell 0.3.0 (git+https://github.com/xi-editor/druid?rev=6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34)" = "<none>"
"checksum druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814)" = "<none>"
"checksum druid-shell 0.3.0 (git+https://github.com/xi-editor/druid?rev=88d18be0155791cac0d910f22d0552dd44ec3814)" = "<none>"
"checksum dxgi 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1639bbfd6765e92a40267d217a7acbac5b49320b68013f39a8e4376aa8c1e091"
"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"

M Cargo.toml => Cargo.toml +1 -1
@@ 7,6 7,6 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
druid = {git = "https://github.com/xi-editor/druid", rev = "6fc687e39f01a4f0aeda8cbbd919ccd28a9e9f34" }
druid = {git = "https://github.com/xi-editor/druid", rev = "88d18be0155791cac0d910f22d0552dd44ec3814" }
rbf-interp = "0.1.3"
nalgebra = "0.18"

M src/app_state.rs => src/app_state.rs +59 -3
@@ 15,17 15,39 @@ pub struct AppState {
    // this needs to be a variable number, but we're trying to keep
    // complexity down for now.
    pub shared: Shared,
    pub sel: Option<usize>,

    pub pts: Arc<Vec<InterpPt>>,

    pub masters: Arc<Vec<Master>>,
    pub interp_type: InterpType,
}

#[derive(Clone, Copy, PartialEq)]
pub enum InterpType {
    ThinPlate,
    Gaussian,
}

impl Default for InterpType {
    fn default() -> Self {
        InterpType::ThinPlate
    }
}

impl Data for AppState {
    fn same(&self, other: &AppState) -> bool {
        self.shared.same(&other.shared)
            && self.sel.same(&other.sel)
            && self.pts.same(&other.pts)
            && self.masters.same(&other.masters)
            && self.interp_type.same(&other.interp_type)
    }
}

impl Data for InterpType {
    fn same(&self, other: &InterpType) -> bool {
        self == other
    }
}



@@ 49,6 71,7 @@ pub struct Shared {

#[derive(Clone)]
pub struct Master {
    pub width: f64,
    pub weight: f64,
}



@@ 61,7 84,7 @@ pub struct InterpSample {

impl Data for Master {
    fn same(&self, other: &Self) -> bool {
        self.weight.same(&other.weight)
        self.width.same(&other.width) && self.weight.same(&other.weight)
    }
}



@@ 78,6 101,7 @@ pub mod lenses {
        pub struct Weight;
        pub struct Shared;
        pub struct Masters;
        pub struct InterpType;

        // Note: this lens isn't quite right.
        impl Lens<AppState, f64> for Width {


@@ 114,6 138,20 @@ pub mod lenses {
            }
        }

        impl Lens<AppState, super::super::InterpType> for InterpType {
            fn get<'a>(&self, data: &'a AppState) -> &'a super::super::InterpType {
                &data.interp_type
            }

            fn with_mut<V, F: FnOnce(&mut super::super::InterpType) -> V>(
                &self,
                data: &mut AppState,
                f: F,
            ) -> V {
                f(&mut data.interp_type)
            }
        }

        impl Lens2<AppState, super::super::Shared> for Shared {
            fn get<V, F: FnOnce(&super::super::Shared) -> V>(&self, data: &AppState, f: F) -> V {
                f(&data.shared)


@@ 146,13 184,26 @@ pub mod lenses {

impl AppState {
    pub fn add_new_master(&mut self) {
        if self.is_at_master() {
            // TODO: provide feedback, or, better yet, set enable state of
            // button (see druid#143).
            println!("master already exists");
            return;
        }
        let mut masters = self.masters.deref().to_owned();
        masters.push(Master {
            width: self.shared.width,
            weight: self.shared.weight,
        });
        self.masters = masters.into();
        println!("adding new master");
    }

    pub fn is_at_master(&self) -> bool {
        self.masters
            .iter()
            .any(|master| self.shared.width == master.width && self.shared.weight == master.weight)
    }
}

impl InterpPt {


@@ 163,7 214,7 @@ impl InterpPt {
        }
    }

    pub fn eval(&self, width: f64, weight: f64) -> Point {
    pub fn eval(&self, width: f64, weight: f64, interp_type: InterpType) -> Point {
        let len = self.samples.len();
        let mut centers = Vec::with_capacity(len);
        let mut vals = Vec::with_capacity(len);


@@ 171,7 222,12 @@ impl InterpPt {
            centers.push(DVector::from_vec(vec![sample.width, sample.weight]));
            vals.push(DVector::from_vec(vec![sample.pt.x, sample.pt.y]));
        }
        let scatter = Scatter::create(centers, vals, Basis::PolyHarmonic(2), 2);
        let basis = match interp_type {
            InterpType::ThinPlate => Basis::PolyHarmonic(2),
            // TODO: control over radius
            InterpType::Gaussian => Basis::Gaussian(1.0),
        };
        let scatter = Scatter::create(centers, vals, basis, 2);
        let params = DVector::from_vec(vec![width, weight]);
        let interp = scatter.eval(params);
        Point::new(interp[0], interp[1])

M src/interp_pane.rs => src/interp_pane.rs +31 -5
@@ 16,6 16,15 @@ pub struct InterpPane {
    drag_ix: Option<usize>,
}

enum PtState {
    /// Point is interpolated and can't be dragged.
    Interpolated,
    /// Point doesn't have a master at the current params, but params are at a master.
    CanAddMaster,
    /// Point has a master at the current params.
    IsMaster,
}

impl Widget<AppState> for InterpPane {
    fn paint(
        &mut self,


@@ 26,10 35,25 @@ impl Widget<AppState> for InterpPane {
    ) {
        let width = data.shared.width;
        let weight = data.shared.weight;
        for pt in data.pts.deref() {
            let interp = pt.eval(width, weight);
            let circle = Circle::new(interp, 5.0);
            let fg_color = Color::WHITE;
        let pt_state = if data.is_at_master() {
            PtState::CanAddMaster
        } else {
            PtState::Interpolated
        };
        for (i, pt) in data.pts.iter().enumerate() {
            let fg_color = match pt_state {
                PtState::CanAddMaster => Color::WHITE,
                PtState::Interpolated => Color::WHITE.with_alpha(0.5),
                _ => Color::rgb(0xff, 0, 0),
            };
            let is_selected = data.sel == Some(i);
            let interp = pt.eval(width, weight, data.interp_type);
            let radius = if is_selected {
                6.0
            } else {
                5.0
            };
            let circle = Circle::new(interp, radius);
            paint_ctx.render_ctx.fill(circle, &fg_color);
        }
    }


@@ 59,13 83,15 @@ impl Widget<AppState> for InterpPane {
                let pos = e.pos;
                let mut pts = data.pts.deref().clone();
                for (i, pt) in pts.iter().enumerate() {
                    let interp = pt.eval(width, weight);
                    let interp = pt.eval(width, weight, data.interp_type);
                    if interp.distance(pos) < 5.0 {
                        self.drag_ix = Some(i);
                        data.sel = Some(i);
                        return None;
                    }
                }
                self.drag_ix = Some(pts.len());
                data.sel = self.drag_ix;
                let pt = InterpPt::new(pos, width, weight);
                pts.push(pt);
                data.pts = Arc::new(pts);

M src/main.rs => src/main.rs +19 -13
@@ 1,18 1,19 @@
use druid::shell::{runloop, WindowBuilder};
use druid::widget::{ActionWrapper, Button, Column, DynLabel, Padding, Row, Scroll, Slider};
use druid::{LensWrap, UiMain, UiState, Widget};
use druid::{AppLauncher, LensWrap, LocalizedString, Widget, WindowDesc};

mod app_state;
mod interp_pane;
mod lens2;
mod list;
mod master;
mod radio;

use app_state::{lenses, AppState, InterpPt};
use app_state::{lenses, AppState, InterpPt, InterpType};
use interp_pane::InterpPane;
use lens2::{Lens2Wrap, Pair};
use list::List;
use master::MasterItem;
use radio::radio;

fn build_ui() -> impl Widget<AppState> {
    let pane = InterpPane::default();


@@ 30,6 31,16 @@ fn build_ui() -> impl Widget<AppState> {
    let label_wdth =
        DynLabel::new(|data: &AppState, _env| format!("width: {:.2}", data.shared.width));
    col.add_child(Padding::uniform(5.0, label_wdth), 0.0);
    col.add_child(
        LensWrap::new(
            radio(vec![
                (InterpType::ThinPlate, LocalizedString::new("Thin plate")),
                (InterpType::Gaussian, LocalizedString::new("Gaussian")),
            ]),
            lenses::app_state::InterpType,
        ),
        0.0,
    );
    let new_master_button = Button::new("New Master");
    let new_master_button = ActionWrapper::new(new_master_button, |data: &mut AppState, _env| {
        data.add_new_master()


@@ 53,15 64,10 @@ fn build_ui() -> impl Widget<AppState> {
fn main() {
    druid::shell::init();

    let mut run_loop = runloop::RunLoop::new();
    let mut builder = WindowBuilder::new();
    let root = build_ui();

    let app_state = AppState::default();
    let state = UiState::new(root, app_state);
    builder.set_title("Interpolation toy");
    builder.set_handler(Box::new(UiMain::new(state)));
    let window = builder.build().unwrap();
    window.show();
    run_loop.run();
    let title = LocalizedString::new("Interpolation toy");
    let window = WindowDesc::new(build_ui).title(title);
    AppLauncher::with_window(window)
        .launch(app_state)
        .expect("launch failed");
}

M src/master.rs => src/master.rs +4 -1
@@ 20,7 20,9 @@ impl MasterItem {
        // Discussion: we might want spacing as a separate param for the list widget.
        let child = Padding::uniform(
            3.0,
            DynLabel::new(|data: &(Shared, Master), _env| format!("weight {:.2}", data.1.weight)),
            DynLabel::new(|data: &(Shared, Master), _env| {
                format!("weight {:.2} width {:.2}", data.1.weight, data.1.width)
            }),
        );
        MasterItem {
            child: WidgetPod::new(child).boxed(),


@@ 62,6 64,7 @@ impl Widget<(Shared, Master)> for MasterItem {
        match event {
            Event::MouseDown(_) => {
                data.0.weight = data.1.weight;
                data.0.width = data.1.width;
                return None;
            }
            _ => (),

A src/radio.rs => src/radio.rs +47 -0
@@ 0,0 1,47 @@
//! A somewhat hacked-up version of a radio button using lensing.

use druid::widget::{Checkbox, Column, Label, Padding, Row};
use druid::{Data, Lens, LensWrap, LocalizedString, Widget};

struct EnumLens<T: Data> {
    variant: T,
}

impl<T: Data> EnumLens<T> {
    pub fn new(variant: T) -> Self {
        EnumLens { variant }
    }
}

impl<T: Data> Lens<T, bool> for EnumLens<T> {
    fn get<'a>(&self, data: &'a T) -> &'a bool {
        if data.same(&self.variant) {
            &true
        } else {
            &false
        }
    }

    fn with_mut<V, F: FnOnce(&mut bool) -> V>(&self, data: &mut T, f: F) -> V {
        let mut is_set = data.same(&self.variant);
        let val = f(&mut is_set);
        if is_set {
            *data = self.variant.clone()
        }
        val
    }
}

pub fn radio<T: Data + 'static>(
    variants: impl IntoIterator<Item = (T, LocalizedString<bool>)>,
) -> impl Widget<T> {
    let mut col = Column::new();
    for (variant, label) in variants.into_iter() {
        let mut row = Row::new();
        row.add_child(Checkbox::new(), 0.0);
        row.add_child(Label::new(label), 1.0);
        let lensed = LensWrap::new(row, EnumLens::new(variant));
        col.add_child(Padding::uniform(3.0, lensed), 0.0);
    }
    col
}