Skip to main content

toggle/
toggle.rs

1//! A toggle switch centered in the viewport, as one numeric program, ported
2//! from handwired's template/toggle.ts without its spring animation. Like a
3//! native switch, it acts on release: a press on the track arms it, a release
4//! on the track while armed flips it, and a release off the track or a cancel
5//! disarms it. `ToggleReference` is the same switch in plain Rust, and
6//! tests/toggle.rs compares the two.
7//!
8//! Run with `cargo run --example toggle`.
9#![allow(dead_code)]
10
11use handwired_rs::{
12    Circuit, Relation, SourceTable, TensorProgram, Wire, emit_and, emit_compare, emit_not, emit_select, event, input,
13    state_bit,
14};
15
16pub const TRACK_WIDTH: f64 = 52.0;
17pub const TRACK_HEIGHT: f64 = 32.0;
18
19#[derive(Copy, Clone, Debug, PartialEq)]
20pub enum PointerKind {
21    Down,
22    Up,
23    Cancel,
24}
25
26/// A pointer event, with coordinates on the 1/64 px grid.
27#[derive(Copy, Clone, Debug, PartialEq)]
28pub struct PointerEvent {
29    pub kind: PointerKind,
30    pub x: f32,
31    pub y: f32,
32}
33
34#[derive(Copy, Clone, Debug, PartialEq)]
35pub struct Output {
36    pub track_left: f32,
37    pub track_top: f32,
38    pub on: f32,
39    pub armed: f32,
40}
41
42struct Pointer {
43    active: Wire,
44    x: Wire,
45    y: Wire,
46}
47
48pub struct Toggle {
49    table: SourceTable,
50    circuit: Circuit,
51    program: TensorProgram,
52    source: Vec<f32>,
53    viewport: (Wire, Wire),
54    down: Pointer,
55    up: Pointer,
56    cancel: Wire,
57    outputs: [Wire; 4],
58}
59
60impl Default for Toggle {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl Toggle {
67    pub fn new() -> Toggle {
68        // Every number the program reads, or keeps from one evaluation to the next.
69        let mut table = SourceTable::new();
70        let coordinate = || input(-16_384.0, 16_384.0).grid(1.0 / 64.0);
71        let viewport_width = table.declare("viewport.width", input(0.0, 16_384.0).grid(1.0).initial(1280.0));
72        let viewport_height = table.declare("viewport.height", input(0.0, 16_384.0).grid(1.0).initial(800.0));
73        let mut pointer = |name: &str| Pointer {
74            active: table.declare(format!("{name}.active"), event()),
75            x: table.declare(format!("{name}.x"), coordinate()),
76            y: table.declare(format!("{name}.y"), coordinate()),
77        };
78        let down = pointer("down");
79        let up = pointer("up");
80        let cancel = table.declare("cancel", event());
81        let on_cell = table.declare("on", state_bit());
82        let armed_cell = table.declare("armed", state_bit());
83        let mut circuit = table.create_circuit();
84        let c = &mut circuit;
85
86        // Layout. trackLeft = viewportWidth / 2 - trackWidth / 2
87        let track_left = c.affine(viewport_width * 0.5 - TRACK_WIDTH / 2.0);
88        let track_top = c.affine(viewport_height * 0.5 - TRACK_HEIGHT / 2.0);
89        let track_right = c.affine(track_left + TRACK_WIDTH);
90        let track_bottom = c.affine(track_top + TRACK_HEIGHT);
91        // x >= trackLeft && x < trackLeft + trackWidth && y >= trackTop && y < trackTop + trackHeight
92        let inside_track = |c: &mut Circuit, point: &Pointer| {
93            let bits = [
94                emit_compare(c, point.x, Relation::AtLeast, track_left),
95                emit_compare(c, point.x, Relation::Less, track_right),
96                emit_compare(c, point.y, Relation::AtLeast, track_top),
97                emit_compare(c, point.y, Relation::Less, track_bottom),
98            ];
99            emit_and(c, &bits)
100        };
101
102        // Handle inputs in a fixed order: down, then up, then cancel.
103        // if (down) armed = insideTrack(down)
104        let down_inside = inside_track(c, &down);
105        let armed_after_down = emit_select(c, down.active, armed_cell, down_inside);
106        // if (up && armed && insideTrack(up)) on = !on
107        let up_inside = inside_track(c, &up);
108        let flips = emit_and(c, &[up.active, armed_after_down, up_inside]);
109        let flipped = emit_not(c, on_cell);
110        let on = emit_select(c, flips, on_cell, flipped);
111        // if (up || cancel) armed = false
112        let no_up = emit_not(c, up.active);
113        let no_cancel = emit_not(c, cancel);
114        let armed = emit_and(c, &[armed_after_down, no_up, no_cancel]);
115
116        // Commit state: each state cell gets exactly one next value.
117        table.next(on_cell, on);
118        table.next(armed_cell, armed);
119        table.finish();
120        let outputs = [track_left, track_top, on, armed];
121        let mut roots = outputs.to_vec();
122        roots.extend(table.next_wires());
123        let program = circuit.compile(Some(&roots));
124        let source = table.initial();
125        Toggle {
126            table,
127            circuit,
128            program,
129            source,
130            viewport: (viewport_width, viewport_height),
131            down,
132            up,
133            cancel,
134            outputs,
135        }
136    }
137
138    pub fn circuit(&self) -> &Circuit {
139        &self.circuit
140    }
141
142    pub fn program(&self) -> &TensorProgram {
143        &self.program
144    }
145
146    /// One evaluation: write the viewport and this frame's events into the
147    /// table, run the compiled program, and keep the next state.
148    pub fn step(&mut self, width: f32, height: f32, events: &[PointerEvent]) -> Result<Output, String> {
149        let mut values = self.source.clone();
150        values[self.viewport.0.index()] = width;
151        values[self.viewport.1.index()] = height;
152        for event in events {
153            let pointer = match event.kind {
154                PointerKind::Down => &self.down,
155                PointerKind::Up => &self.up,
156                PointerKind::Cancel => {
157                    values[self.cancel.index()] = 1.0;
158                    continue;
159                }
160            };
161            values[pointer.active.index()] = 1.0;
162            values[pointer.x.index()] = event.x;
163            values[pointer.y.index()] = event.y;
164        }
165        self.table.validate(&values)?;
166        let result = self.circuit.evaluate_tensor(&values, &self.program).map_err(|error| error.to_string())?;
167        self.source = self.table.advance(&values, &result);
168        let [track_left, track_top, on, armed] = self.outputs.map(|wire| result[wire.index()]);
169        Ok(Output { track_left, track_top, on, armed })
170    }
171}
172
173/// The same switch in plain Rust, in f64.
174#[derive(Default)]
175pub struct ToggleReference {
176    on: bool,
177    armed: bool,
178}
179
180impl ToggleReference {
181    pub fn step(&mut self, width: f64, height: f64, events: &[PointerEvent]) -> Output {
182        let track_left = width / 2.0 - TRACK_WIDTH / 2.0;
183        let track_top = height / 2.0 - TRACK_HEIGHT / 2.0;
184        let inside = |event: &PointerEvent| {
185            let (x, y) = (event.x as f64, event.y as f64);
186            x >= track_left && x < track_left + TRACK_WIDTH && y >= track_top && y < track_top + TRACK_HEIGHT
187        };
188        let last = |kind| events.iter().rev().find(|event| event.kind == kind);
189        if let Some(down) = last(PointerKind::Down) {
190            self.armed = inside(down);
191        }
192        let up = last(PointerKind::Up);
193        if let Some(up) = up
194            && self.armed
195            && inside(up)
196        {
197            self.on = !self.on;
198        }
199        if up.is_some() || last(PointerKind::Cancel).is_some() {
200            self.armed = false;
201        }
202        let bit = |value: bool| if value { 1.0 } else { 0.0 };
203        Output { track_left: track_left as f32, track_top: track_top as f32, on: bit(self.on), armed: bit(self.armed) }
204    }
205}
206
207fn main() {
208    let mut toggle = Toggle::new();
209    let program = toggle.program();
210    println!(
211        "toggle: {} source cells, {} built operations, {} live in {} layers, {} stored weights",
212        toggle.circuit().source_size(),
213        toggle.circuit().operations().len(),
214        program.live_operation_count(),
215        program.layers.len(),
216        program.stored_weight_count(),
217    );
218    let press = |kind, x, y| [PointerEvent { kind, x, y }];
219    let frames: [(&str, Vec<PointerEvent>); 4] = [
220        ("press on the track", press(PointerKind::Down, 640.0, 400.0).to_vec()),
221        ("release on the track", press(PointerKind::Up, 640.0, 400.0).to_vec()),
222        ("press 1/64 px left of the track", press(PointerKind::Down, 613.984_4, 400.0).to_vec()),
223        ("release on the track", press(PointerKind::Up, 640.0, 400.0).to_vec()),
224    ];
225    for (label, events) in frames {
226        let output = toggle.step(1280.0, 800.0, &events).unwrap();
227        println!("{label:32} -> on {} armed {}", output.on, output.armed);
228    }
229}