feat: dominium standalone — simulador de campo medio sobre Llimphi
Front-door publicable de dominium: los 9 crates propios como path members; Llimphi, app-bus, rimay-localize, wawa-config y pluma-notebook por git-dep al monorepo tawasuyu.git (branch=main). cargo check --workspace --all-targets pasa exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "dominium-canvas-llimphi"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "dominium-canvas-llimphi — backend Llimphi del simulador. Recibe un `RenderPlan` (cadena `core → physics → iso → render-plan` agnóstica intacta) y devuelve un `View<Msg>` con `paint_with` que pinta los quads centrados en sus bounds usando vello."
|
||||
|
||||
[dependencies]
|
||||
dominium-render-plan = { path = "../dominium-render-plan" }
|
||||
llimphi-ui = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Cadena completa para el example: arma un World + un IsoProjector y
|
||||
# llama a `build_plan` para que el canvas reciba un plan realista.
|
||||
dominium-core = { path = "../dominium-core" }
|
||||
dominium-iso = { path = "../dominium-iso" }
|
||||
|
||||
[[example]]
|
||||
name = "canvas_demo"
|
||||
path = "examples/canvas_demo.rs"
|
||||
@@ -0,0 +1,10 @@
|
||||
# dominium-canvas-llimphi
|
||||
|
||||
> Backend Llimphi (vello) para [dominium](../README.md).
|
||||
|
||||
Convierte el `Vec<Quad>` que produce [`dominium-render-plan`](../dominium-render-plan/README.md) en operaciones `vello::Scene` adentro de un `View::paint_with(...)` de Llimphi. Single-pass; cero allocs por frame (re-usa el buffer de quads).
|
||||
|
||||
## Deps
|
||||
|
||||
- [`dominium-render-plan`](../dominium-render-plan/README.md)
|
||||
- [`llimphi-ui`](../../../02_ruway/llimphi/) (vello)
|
||||
@@ -0,0 +1,10 @@
|
||||
# dominium-canvas-llimphi
|
||||
|
||||
> Llimphi (vello) backend for [dominium](../README.md).
|
||||
|
||||
Converts the `Vec<Quad>` from [`dominium-render-plan`](../dominium-render-plan/README.md) into `vello::Scene` operations inside a Llimphi `View::paint_with(...)`. Single-pass; zero allocations per frame (reuses the quad buffer).
|
||||
|
||||
## Deps
|
||||
|
||||
- [`dominium-render-plan`](../dominium-render-plan/README.md)
|
||||
- [`llimphi-ui`](../../../02_ruway/llimphi/) (vello)
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Showcase de `dominium-canvas-llimphi`: arma un mundo pequeño con
|
||||
//! patrones manuales (vetas de oro, parches de materia, niebla de
|
||||
//! psique), construye el `RenderPlan` con `build_plan` y lo pinta
|
||||
//! centrado en la ventana.
|
||||
//!
|
||||
//! Sin loop de simulación — la app Llimphi completa con tick vivo
|
||||
//! va en `dominium-app-llimphi` (próximo bloque).
|
||||
//!
|
||||
//! Corré con: `cargo run -p dominium-canvas-llimphi --example canvas_demo --release`.
|
||||
|
||||
use dominium_canvas_llimphi::canvas_view;
|
||||
use dominium_core::World;
|
||||
use dominium_iso::{IsoProjector, ZWeights};
|
||||
use dominium_render_plan::{build_plan, PlanConfig};
|
||||
use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
use llimphi_ui::{App, Handle, View};
|
||||
|
||||
const GRID: usize = 32;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Msg {}
|
||||
|
||||
struct Model {
|
||||
world: World,
|
||||
}
|
||||
|
||||
struct Showcase;
|
||||
|
||||
impl App for Showcase {
|
||||
type Model = Model;
|
||||
type Msg = Msg;
|
||||
|
||||
fn title() -> &'static str {
|
||||
"dominium · canvas showcase"
|
||||
}
|
||||
|
||||
fn initial_size() -> (u32, u32) {
|
||||
(1000, 720)
|
||||
}
|
||||
|
||||
fn init(_: &Handle<Msg>) -> Model {
|
||||
Model { world: seed() }
|
||||
}
|
||||
|
||||
fn update(model: Model, _: Msg, _: &Handle<Msg>) -> Model {
|
||||
model
|
||||
}
|
||||
|
||||
fn view(model: &Model) -> View<Msg> {
|
||||
let iso = IsoProjector::new(1.0, 4.0);
|
||||
let weights = ZWeights::default();
|
||||
let cfg = PlanConfig::default();
|
||||
let plan = build_plan(&model.world, &iso, &weights, &cfg);
|
||||
|
||||
let canvas = canvas_view::<Msg>(plan, Some(Color::from_rgba8(14, 16, 22, 255)));
|
||||
|
||||
View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![canvas])
|
||||
}
|
||||
}
|
||||
|
||||
/// Mundo sembrado a mano: continentes de materia en el centro, vetas
|
||||
/// de oro en una diagonal y un parche de psique en una esquina. Sin
|
||||
/// PRNG — siempre la misma escena entre runs.
|
||||
fn seed() -> World {
|
||||
let mut w = World::new(GRID, GRID);
|
||||
for cy in 0..GRID {
|
||||
for cx in 0..GRID {
|
||||
let idx = w.grid.idx(cx, cy);
|
||||
// Continente: gauss centrado.
|
||||
let dx = cx as f32 - (GRID as f32 * 0.5);
|
||||
let dy = cy as f32 - (GRID as f32 * 0.5);
|
||||
let d2 = dx * dx + dy * dy;
|
||||
let materia = (40.0 - d2 * 0.15).max(0.0);
|
||||
w.grid.materia[idx] = materia;
|
||||
|
||||
// Veta de oro en la diagonal cx == cy.
|
||||
if cx == cy && cx > 4 && cx < GRID - 4 {
|
||||
w.grid.oro[idx] = 35.0;
|
||||
}
|
||||
|
||||
// Psique en el cuadrante inferior derecho.
|
||||
if cx > GRID * 2 / 3 && cy > GRID * 2 / 3 {
|
||||
w.grid.psique[idx] = 18.0;
|
||||
}
|
||||
|
||||
// Borde de degradación.
|
||||
if cx == 0 || cy == 0 || cx == GRID - 1 || cy == GRID - 1 {
|
||||
w.grid.degradacion[idx] = 25.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
fn main() {
|
||||
llimphi_ui::run::<Showcase>();
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! `dominium-canvas-llimphi` — el único crate de dominium que importa
|
||||
//! `llimphi-ui`.
|
||||
//!
|
||||
//! Toda la cadena `dominium-core → physics → iso → render-plan` es
|
||||
//! agnóstica de backend. Este crate cierra el circuito: una función
|
||||
//! [`canvas_view`] que recibe un [`RenderPlan`] ya resuelto y devuelve
|
||||
//! un `View<Msg>` con `paint_with` que pinta los quads vía vello,
|
||||
//! centrando la maqueta en los bounds asignados por taffy.
|
||||
//!
|
||||
//! Reemplazo Llimphi del `dominium-canvas-gpui`. Igual contrato:
|
||||
//! el `Element` (acá `View`) no guarda estado entre frames — el host
|
||||
//! reconstruye el View con el `RenderPlan` del frame actual.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use dominium_render_plan::{Color as PlanColor, RenderPlan, SpritePrim};
|
||||
use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
|
||||
use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Circle, Point, Rect as KurboRect, Stroke};
|
||||
use llimphi_ui::llimphi_raster::peniko::{Color, Fill};
|
||||
use llimphi_ui::llimphi_text::{draw_block, TextBlock};
|
||||
use llimphi_ui::{PaintRect, View};
|
||||
|
||||
/// Convierte el RGBA lineal del plan (`[f32;4]` en [0,1]) al `Color`
|
||||
/// de peniko. Mantiene la convención sin gamma del backend GPUI.
|
||||
fn plan_color(c: PlanColor) -> Color {
|
||||
let to_byte = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
Color::from_rgba8(to_byte(c[0]), to_byte(c[1]), to_byte(c[2]), to_byte(c[3]))
|
||||
}
|
||||
|
||||
/// Construye un View que pinta `plan` en su rect. Si `background` está
|
||||
/// presente, se pinta como fondo sólido antes de los quads (el `fill`
|
||||
/// del View ya lo cubriría — pero esta API mantiene el shape del
|
||||
/// `DominiumCanvas::background` del backend GPUI).
|
||||
pub fn canvas_view<Msg>(plan: RenderPlan, background: Option<Color>) -> View<Msg>
|
||||
where
|
||||
Msg: Clone + 'static,
|
||||
{
|
||||
// El plan es Send + Sync (Vec<Quad> con Copy). Lo movemos a la
|
||||
// closure de paint; el runtime la invoca por frame.
|
||||
let view = View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let view = if let Some(bg) = background {
|
||||
view.fill(bg)
|
||||
} else {
|
||||
view
|
||||
};
|
||||
view.paint_with(move |scene, ts, rect: PaintRect| {
|
||||
if plan.quads.is_empty()
|
||||
&& plan.polygons.is_empty()
|
||||
&& plan.glyphs.is_empty()
|
||||
&& plan.sprites.is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Centra la maqueta: el centro de la caja envolvente del plan
|
||||
// se alinea con el centro del rect del nodo.
|
||||
let plan_cx = (plan.min_x + plan.max_x) * 0.5;
|
||||
let plan_cy = (plan.min_y + plan.max_y) * 0.5;
|
||||
let off_x = (rect.x + rect.w * 0.5 - plan_cx) as f64;
|
||||
let off_y = (rect.y + rect.h * 0.5 - plan_cy) as f64;
|
||||
|
||||
// Intercala quads + polygons por depth, atrás → adelante. Cada
|
||||
// input ya está ordenado por su propio depth, así que un merge
|
||||
// lineal alcanza — sin re-ordenar.
|
||||
let mut qi = 0usize;
|
||||
let mut pi = 0usize;
|
||||
while qi < plan.quads.len() || pi < plan.polygons.len() {
|
||||
let q_d = plan.quads.get(qi).map(|q| q.depth);
|
||||
let p_d = plan.polygons.get(pi).map(|p| p.depth);
|
||||
let take_quad = match (q_d, p_d) {
|
||||
(Some(q), Some(p)) => q <= p,
|
||||
(Some(_), None) => true,
|
||||
(None, Some(_)) => false,
|
||||
(None, None) => break,
|
||||
};
|
||||
if take_quad {
|
||||
let q = &plan.quads[qi];
|
||||
let x0 = q.x as f64 + off_x;
|
||||
let y0 = q.y as f64 + off_y;
|
||||
let x1 = x0 + q.w as f64;
|
||||
let y1 = y0 + q.h as f64;
|
||||
let r = KurboRect::new(x0, y0, x1, y1);
|
||||
scene.fill(
|
||||
Fill::NonZero,
|
||||
Affine::IDENTITY,
|
||||
plan_color(q.color),
|
||||
None,
|
||||
&r,
|
||||
);
|
||||
qi += 1;
|
||||
} else {
|
||||
let p = &plan.polygons[pi];
|
||||
let mut path = BezPath::new();
|
||||
let v = &p.vertices;
|
||||
path.move_to(Point::new(v[0].0 as f64 + off_x, v[0].1 as f64 + off_y));
|
||||
path.line_to(Point::new(v[1].0 as f64 + off_x, v[1].1 as f64 + off_y));
|
||||
path.line_to(Point::new(v[2].0 as f64 + off_x, v[2].1 as f64 + off_y));
|
||||
path.line_to(Point::new(v[3].0 as f64 + off_x, v[3].1 as f64 + off_y));
|
||||
path.close_path();
|
||||
scene.fill(
|
||||
Fill::NonZero,
|
||||
Affine::IDENTITY,
|
||||
plan_color(p.color),
|
||||
None,
|
||||
&path,
|
||||
);
|
||||
pi += 1;
|
||||
}
|
||||
}
|
||||
// Sprites vectoriales de los Conceptos, por encima de los quads.
|
||||
// Cada primitiva es relleno (polígono cerrado), trazo (polilínea
|
||||
// con grosor) o disco. Coordenadas ya en pantalla → sólo offset.
|
||||
for prim in &plan.sprites {
|
||||
match prim {
|
||||
SpritePrim::Fill { points, color } => {
|
||||
if points.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(Point::new(points[0].0 as f64 + off_x, points[0].1 as f64 + off_y));
|
||||
for pt in &points[1..] {
|
||||
path.line_to(Point::new(pt.0 as f64 + off_x, pt.1 as f64 + off_y));
|
||||
}
|
||||
path.close_path();
|
||||
scene.fill(Fill::NonZero, Affine::IDENTITY, plan_color(*color), None, &path);
|
||||
}
|
||||
SpritePrim::Stroke { points, width, color } => {
|
||||
if points.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(Point::new(points[0].0 as f64 + off_x, points[0].1 as f64 + off_y));
|
||||
for pt in &points[1..] {
|
||||
path.line_to(Point::new(pt.0 as f64 + off_x, pt.1 as f64 + off_y));
|
||||
}
|
||||
scene.stroke(
|
||||
&Stroke::new(*width as f64),
|
||||
Affine::IDENTITY,
|
||||
plan_color(*color),
|
||||
None,
|
||||
&path,
|
||||
);
|
||||
}
|
||||
SpritePrim::Disc { cx, cy, r, color } => {
|
||||
let circle =
|
||||
Circle::new(Point::new(*cx as f64 + off_x, *cy as f64 + off_y), *r as f64);
|
||||
scene.fill(Fill::NonZero, Affine::IDENTITY, plan_color(*color), None, &circle);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Glifos por encima de todo, sin re-shaping cacheado.
|
||||
for gl in &plan.glyphs {
|
||||
let s = gl.ch.to_string();
|
||||
let block = TextBlock::simple(
|
||||
&s,
|
||||
gl.size_px,
|
||||
plan_color(gl.color),
|
||||
(gl.x as f64 + off_x, gl.y as f64 + off_y),
|
||||
);
|
||||
draw_block(scene, ts, &block);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pure_red_round_trips() {
|
||||
let c = plan_color([1.0, 0.0, 0.0, 1.0]).to_rgba8();
|
||||
assert_eq!((c.r, c.g, c.b, c.a), (255, 0, 0, 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_passes_through() {
|
||||
let c = plan_color([0.0, 0.0, 1.0, 0.25]).to_rgba8();
|
||||
assert_eq!(c.b, 255);
|
||||
assert_eq!(c.a, 64); // 0.25 * 255 = 63.75 ~> 64
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_clamps() {
|
||||
let c = plan_color([1.5, -0.2, 0.5, 1.0]).to_rgba8();
|
||||
assert_eq!((c.r, c.g, c.b), (255, 0, 128));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user