Files
llimphi/llimphi-ui/examples/gestos.rs
T
Sergio ccab39f140 refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la
estructura vieja de eventloop) y suma el 3D:

- bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 /
  accesskit_winit 0.33 / vello_hybrid 0.0.9.
- nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido,
  montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel
  (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox).
- README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif).
- excluido modules/allichay (arrastra deps fuera del alcance del front-door).
- cargo check --workspace: verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:40:00 +00:00

184 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Demo de la **arena de gestos** de Llimphi (Tier 4 de PARIDAD-FLUTTER).
//!
//! Un canvas pannable + zoomable que ejercita los tres gestos nuevos:
//!
//! - **Ctrl + rueda** → `on_scale`: zoom hacia el cursor (camino universal de
//! desktop; en macOS también responde al pinch del trackpad).
//! - **Arrastrar** (botón izquierdo) → `draggable`: paneo. Mover cancela un
//! long-press en curso — esa desambiguación es la "arena".
//! - **Doble-click** → `on_double_tap`: resetea zoom y paneo.
//! - **Mantener apretado ~500 ms quieto** → `on_long_press_at`: deja una marca
//! en el punto (coordenadas de mundo, así sigue al zoom/paneo).
//!
//! La barra inferior muestra el zoom, la cantidad de marcas y el último gesto.
//!
//! Corre con: `cargo run -p llimphi-ui --example gestos --release`.
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, Dimension, FlexDirection, Size, Style},
AlignItems, JustifyContent,
};
use llimphi_ui::llimphi_raster::kurbo::{Affine, Circle, Line, Stroke};
use llimphi_ui::llimphi_raster::peniko::{Color, Fill};
use llimphi_ui::{App, DragPhase, GesturePhase, Handle, View};
#[derive(Clone)]
enum Msg {
/// Zoom incremental con factor multiplicativo + punto focal (local al canvas).
Zoom { factor: f32, fx: f32, fy: f32 },
/// Paneo por delta de arrastre.
Pan { dx: f32, dy: f32 },
/// Doble-tap: resetear la vista.
Reset,
/// Long-press: dejar una marca en el punto (local al canvas).
Mark { lx: f32, ly: f32 },
}
struct Model {
zoom: f32,
pan: (f32, f32),
/// Marcas en coordenadas de **mundo** (independientes del zoom/paneo).
marks: Vec<(f32, f32)>,
last: String,
}
struct Gestos;
impl App for Gestos {
type Model = Model;
type Msg = Msg;
fn title() -> &'static str {
"llimphi · gestos (pinch-zoom · double-tap · long-press)"
}
fn initial_size() -> (u32, u32) {
(900, 640)
}
fn init(_: &Handle<Self::Msg>) -> Self::Model {
Model {
zoom: 1.0,
pan: (0.0, 0.0),
marks: Vec::new(),
last: "probá: Ctrl+rueda (zoom) · arrastrar (paneo) · doble-click (reset) · mantener (marca)".into(),
}
}
fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
match msg {
Msg::Zoom { factor, fx, fy } => {
// Zoom hacia el cursor: mantené fijo el punto de mundo bajo
// (fx, fy) reajustando el paneo. new_pan = focal - rf·(focal - pan).
let new_zoom = (model.zoom * factor).clamp(0.15, 12.0);
let rf = new_zoom / model.zoom; // factor real tras el clamp
model.pan.0 = fx - rf * (fx - model.pan.0);
model.pan.1 = fy - rf * (fy - model.pan.1);
model.zoom = new_zoom;
model.last = format!("zoom ×{:.2}", model.zoom);
}
Msg::Pan { dx, dy } => {
model.pan.0 += dx;
model.pan.1 += dy;
model.last = "paneo".into();
}
Msg::Reset => {
model.zoom = 1.0;
model.pan = (0.0, 0.0);
model.last = "doble-tap → reset".into();
}
Msg::Mark { lx, ly } => {
// Local del canvas → mundo: (local - pan) / zoom.
let wx = (lx - model.pan.0) / model.zoom;
let wy = (ly - model.pan.1) / model.zoom;
model.marks.push((wx, wy));
model.last = format!("long-press → marca #{} @ ({wx:.0}, {wy:.0})", model.marks.len());
}
}
model
}
fn view(model: &Self::Model) -> View<Self::Msg> {
let zoom = model.zoom;
let pan = model.pan;
let marks = model.marks.clone();
let canvas = View::new(Style {
size: Size { width: percent(1.0_f32), height: Dimension::auto() },
flex_grow: 1.0,
..Default::default()
})
.fill(Color::from_rgba8(16, 18, 26, 255))
.clip(true)
.paint_with(move |scene, _ts, rect| {
// Grilla de mundo paso 40px, escalada por zoom y desplazada por pan.
let step = 40.0 * zoom as f64;
if step >= 4.0 {
let thin = Stroke::new(1.0);
let grid = Color::from_rgba8(40, 46, 60, 255);
// Offset del primer línea visible (pan módulo step).
let ox = (rect.x as f64) + (pan.0 as f64).rem_euclid(step);
let mut x = ox;
while x < (rect.x + rect.w) as f64 {
scene.stroke(&thin, Affine::IDENTITY, grid, None,
&Line::new((x, rect.y as f64), (x, (rect.y + rect.h) as f64)));
x += step;
}
let oy = (rect.y as f64) + (pan.1 as f64).rem_euclid(step);
let mut y = oy;
while y < (rect.y + rect.h) as f64 {
scene.stroke(&thin, Affine::IDENTITY, grid, None,
&Line::new((rect.x as f64, y), ((rect.x + rect.w) as f64, y)));
y += step;
}
}
// Marcas (coords de mundo → pantalla): pan + world·zoom.
let dot = Color::from_rgba8(90, 220, 150, 255);
let r = (6.0 * zoom as f64).clamp(3.0, 24.0);
for (wx, wy) in &marks {
let sx = rect.x as f64 + pan.0 as f64 + (*wx as f64) * zoom as f64;
let sy = rect.y as f64 + pan.1 as f64 + (*wy as f64) * zoom as f64;
scene.fill(Fill::NonZero, Affine::IDENTITY, dot, None, &Circle::new((sx, sy), r));
}
})
// Pinch-to-zoom (Ctrl+rueda / trackpad). El focal viene local al canvas.
.on_scale(|phase, factor, fx, fy| match phase {
GesturePhase::Update => Some(Msg::Zoom { factor, fx, fy }),
_ => None,
})
// Paneo por arrastre. El movimiento cancela un long-press en curso.
.draggable(|phase, dx, dy| match phase {
DragPhase::Move => Some(Msg::Pan { dx, dy }),
DragPhase::End => None,
})
// Doble-tap: reset. Long-press: marca en el punto.
.on_double_tap(Msg::Reset)
.on_long_press_at(|lx, ly, _w, _h| Some(Msg::Mark { lx, ly }));
let status = View::new(Style {
size: Size { width: percent(1.0_f32), height: length(40.0_f32) },
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
})
.fill(Color::from_rgba8(28, 32, 42, 255))
.text(
format!("{} · ×{:.2} · {} marcas", model.last, model.zoom, model.marks.len()),
18.0,
Color::from_rgba8(210, 220, 235, 255),
);
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
..Default::default()
})
.fill(Color::from_rgba8(16, 18, 26, 255))
.children(vec![canvas, status])
}
}
fn main() {
llimphi_ui::run::<Gestos>();
}