Files
llimphi/llimphi-compositor/examples/layout_builder_demo.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

244 lines
9.5 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 headless del **LayoutBuilder** (Bloque 9 de PARIDAD-FLUTTER): el MISMO
//! árbol declarativo, renderizado a dos anchos de viewport. Un panel central
//! usa `View::layout_builder`: si su slot es **angosto** apila las tarjetas en
//! **1 columna**; si es **ancho**, en **2 columnas**. La decisión depende del
//! tamaño del slot (no de la ventana), resuelto en dos pasadas — exactamente lo
//! que el runtime hace por frame.
//!
//! Emula el camino del runtime (`resolve_layout_builders`) con las funciones
//! puras del compositor: `has_layout_builder` → mount pasada 1 → compute →
//! `collect_builder_constraints` → `expand_layout_builders` → mount/paint.
//!
//! Vuelca dos PNGs (`<base>-angosto.png` y `<base>-ancho.png`).
//! `cargo run -p llimphi-compositor --example layout_builder_demo -- [base]`
use std::fs::File;
use std::io::BufWriter;
use llimphi_compositor::{
collect_builder_constraints, expand_layout_builders, has_layout_builder, mount, paint,
Constraints, View,
};
use llimphi_hal::{wgpu, Hal};
use llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
use llimphi_layout::taffy::{AlignItems, JustifyContent, LengthPercentage, Rect};
use llimphi_layout::LayoutTree;
use llimphi_raster::peniko::Color;
use llimphi_raster::{vello, Renderer};
use llimphi_text::{Alignment, Typesetter};
const H: u32 = 360;
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// Bajo este ancho de slot, el panel apila en 1 columna; por encima, 2.
const BREAKPOINT: f32 = 360.0;
fn rgb(r: u8, g: u8, b: u8) -> Color {
Color::from_rgba8(r, g, b, 255)
}
/// Una tarjeta de muestra.
fn card(label: &str) -> View<()> {
View::<()>::new(Style {
size: Size { width: percent(1.0), height: length(64.0) },
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
})
.fill(rgb(60, 72, 100))
.radius(10.0)
.children(vec![View::<()>::new(Style {
size: Size { width: percent(1.0), height: length(20.0) },
..Default::default()
})
.text_aligned(label.to_string(), 14.0, rgb(235, 238, 245), Alignment::Center)])
}
/// El subárbol que el builder produce según sus constraints: 1 columna si
/// angosto, 2 si ancho. Cada columna es un flex column con tarjetas.
fn responsive_panel(c: Constraints) -> View<()> {
let dos_columnas = c.max_width >= BREAKPOINT;
let etiqueta = if dos_columnas {
format!("slot {:.0}px = 2 columnas", c.max_width)
} else {
format!("slot {:.0}px = 1 columna", c.max_width)
};
let header = View::<()>::new(Style {
size: Size { width: percent(1.0), height: length(28.0) },
..Default::default()
})
.text_aligned(etiqueta, 13.0, rgb(150, 200, 160), Alignment::Center);
let col = |labels: &[&str]| {
View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
flex_direction: FlexDirection::Column,
gap: Size { width: length(0.0), height: length(10.0) },
..Default::default()
})
.children(labels.iter().map(|l| card(l)).collect())
};
let cuerpo = if dos_columnas {
View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
flex_direction: FlexDirection::Row,
gap: Size { width: length(12.0), height: length(0.0) },
..Default::default()
})
.children(vec![col(&["Uno", "Tres"]), col(&["Dos", "Cuatro"])])
} else {
col(&["Uno", "Dos", "Tres", "Cuatro"])
};
View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
flex_direction: FlexDirection::Column,
gap: Size { width: length(0.0), height: length(8.0) },
..Default::default()
})
.children(vec![header, cuerpo])
}
/// Árbol raíz: una sidebar fija + un panel central que es el `layout_builder`.
/// El ancho del slot del panel = viewport sidebar paddings, así cambia con
/// el viewport sin que el árbol "sepa" el tamaño al construirse.
fn root() -> View<()> {
let sidebar = View::<()>::new(Style {
size: Size { width: length(160.0), height: percent(1.0) },
..Default::default()
})
.fill(rgb(34, 40, 54))
.radius(12.0)
.children(vec![View::<()>::new(Style {
size: Size { width: percent(1.0), height: length(20.0) },
..Default::default()
})
.text_aligned("sidebar", 13.0, rgb(140, 150, 170), Alignment::Center)]);
let panel = View::<()>::new(Style {
flex_grow: 1.0,
size: Size { width: percent(0.0), height: percent(1.0) },
..Default::default()
})
.layout_builder(responsive_panel);
View::<()>::new(Style {
size: Size { width: percent(1.0), height: percent(1.0) },
flex_direction: FlexDirection::Row,
gap: Size { width: length(16.0), height: length(0.0) },
padding: Rect {
left: LengthPercentage::length(16.0),
right: LengthPercentage::length(16.0),
top: LengthPercentage::length(16.0),
bottom: LengthPercentage::length(16.0),
},
..Default::default()
})
.fill(rgb(24, 28, 38))
.children(vec![sidebar, panel])
}
/// Resuelve los builders (dos pasadas) y vuelca el árbol a un PNG a ese ancho.
fn render_a(ancho: u32, ts: &mut Typesetter, hal: &Hal, renderer: &mut Renderer, path: &str) {
let viewport = (ancho as f32, H as f32);
// Pasada 1: montar (builders como hojas) + computar.
let v1 = root();
assert!(has_layout_builder(&v1), "el demo debe tener un layout_builder");
let mut l1 = LayoutTree::new();
let m1 = mount(&mut l1, v1);
let c1 = l1.compute(m1.root, viewport).expect("layout p1");
let cons = collect_builder_constraints(&m1, &c1);
// Pasada 2: árbol fresco + expand con las constraints reales.
let resolved = expand_layout_builders(root(), &cons);
let mut l2 = LayoutTree::new();
let m2 = mount(&mut l2, resolved);
let c2 = l2.compute(m2.root, viewport).expect("layout p2");
let mut scene = vello::Scene::new();
paint(&mut scene, &m2, &c2, ts, None, None);
let target = hal.device.create_texture(&wgpu::TextureDescriptor {
label: Some("dump-lb"),
size: wgpu::Extent3d { width: ancho, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FMT,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
renderer
.render_to_view(hal, &scene, &view, ancho, H, rgb(244, 245, 248))
.expect("render_to_view");
write_png(hal, &target, ancho, path);
eprintln!("layout_builder_demo: escrito {path} ({ancho}x{H}) — slot panel {:.0}px", cons[0].max_width);
}
fn main() {
let base = std::env::args().nth(1).unwrap_or_else(|| "lb".to_string());
let hal = pollster::block_on(Hal::new(None)).expect("hal");
let mut renderer = Renderer::new(&hal).expect("renderer");
let mut ts = Typesetter::new();
// Angosto: viewport 460 → slot ~268px (<360) → 1 columna.
render_a(460, &mut ts, &hal, &mut renderer, &format!("{base}-angosto.png"));
// Ancho: viewport 760 → slot ~568px (≥360) → 2 columnas.
render_a(760, &mut ts, &hal, &mut renderer, &format!("{base}-ancho.png"));
}
fn write_png(hal: &Hal, target: &wgpu::Texture, w: u32, path: &str) {
let unpadded = (w * 4) as usize;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
let padded = unpadded.div_ceil(align) * align;
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: (padded * H as usize) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: Some(H),
},
},
wgpu::Extent3d { width: w, height: H, depth_or_array_layers: 1 },
);
hal.queue.submit(std::iter::once(enc.finish()));
let slice = buf.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
hal.device.poll(wgpu::PollType::wait_indefinitely());
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range();
let mut pixels = Vec::with_capacity((w * H * 4) as usize);
for row in 0..H as usize {
let s = row * padded;
pixels.extend_from_slice(&data[s..s + unpadded]);
}
drop(data);
buf.unmap();
let file = File::create(path).expect("png");
let mut enc = png::Encoder::new(BufWriter::new(file), w, H);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut wr = enc.write_header().unwrap();
wr.write_image_data(&pixels).unwrap();
}