#![allow(deprecated)] use eframe::egui; use eframe::egui::Color32; use egui::ColorImage; use hcie_brush_engine::{presets::BrushPreset, BrushStyle}; // ── tunables ───────────────────────────────────────────────────────────────── const TILE_SIZE: u32 = 512; struct SliderCaps { hardness: bool, opacity: bool, spacing: bool, flow: bool, jitter: bool, scatter: bool, angle: bool, roundness: bool, spray_particle_size: bool, spray_density: bool, } fn slider_caps(style: BrushStyle) -> SliderCaps { match style { BrushStyle::Calligraphy => SliderCaps { angle: true, roundness: true, jitter: false, scatter: false, spray_particle_size: false, spray_density: false, ..SliderCaps::common() }, BrushStyle::Pencil | BrushStyle::InkPen => SliderCaps { jitter: true, ..SliderCaps::common() }, BrushStyle::Spray | BrushStyle::Airbrush => SliderCaps { jitter: true, scatter: true, spray_particle_size: true, spray_density: true, ..SliderCaps::common() }, BrushStyle::Charcoal => SliderCaps { jitter: true, scatter: true, ..SliderCaps::common() }, BrushStyle::Crayon => SliderCaps { jitter: true, scatter: true, ..SliderCaps::common() }, BrushStyle::Tree => SliderCaps { jitter: true, angle: true, ..SliderCaps::common() }, BrushStyle::Meadow | BrushStyle::Rock | BrushStyle::Dirt => SliderCaps { jitter: true, ..SliderCaps::common() }, BrushStyle::Bristle => SliderCaps { roundness: true, ..SliderCaps::common() }, BrushStyle::HardRound | BrushStyle::SoftRound => SliderCaps { hardness: false, ..SliderCaps::common() }, BrushStyle::Mixer | BrushStyle::Blender => SliderCaps { flow: false, ..SliderCaps::common() }, _ => SliderCaps::common(), } } impl SliderCaps { fn common() -> Self { Self { hardness: true, opacity: true, spacing: true, flow: true, jitter: false, scatter: false, angle: false, roundness: false, spray_particle_size: false, spray_density: false, } } } #[derive(Debug, Clone, Copy, PartialEq)] enum CanvasPreset { S512, S1024, S2048, S4096, } impl CanvasPreset { fn size(self) -> (u32, u32) { match self { Self::S512 => (512, 512), Self::S1024 => (1024, 1024), Self::S2048 => (2048, 2048), Self::S4096 => (4096, 4096), } } fn name(self) -> &'static str { match self { Self::S512 => "512", Self::S1024 => "1024", Self::S2048 => "2048", Self::S4096 => "4096", } } } #[derive(Debug, Clone, Copy, PartialEq)] enum FpsCap { Unlimited, Fps30, Fps60, Fps165, } impl FpsCap { fn value(self) -> Option { match self { Self::Fps30 => Some(30.0), Self::Fps60 => Some(60.0), Self::Fps165 => Some(165.0), Self::Unlimited => None, } } fn name(self) -> &'static str { match self { Self::Fps30 => "30", Self::Fps60 => "60", Self::Fps165 => "165", Self::Unlimited => "∞", } } } // ── tiled canvas ───────────────────────────────────────────────────────────── struct Tile { texture: Option, dirty: bool, } struct TiledCanvas { pixels: Vec, tiles: Vec, w: u32, h: u32, cols: u32, dirty_count: usize, } impl TiledCanvas { fn new(w: u32, h: u32) -> Self { let cols = w.div_ceil(TILE_SIZE); let rows = h.div_ceil(TILE_SIZE); let total = (cols * rows) as usize; Self { pixels: vec![255; (w * h * 4) as usize], tiles: (0..total) .map(|_| Tile { texture: None, dirty: true, }) .collect(), w, h, cols, dirty_count: total, } } fn mark_dirty_rect(&mut self, cx: f32, cy: f32, radius: f32) { let tx0 = ((cx - radius).max(0.0) / TILE_SIZE as f32) as u32; let tx1 = (((cx + radius).min(self.w as f32 - 1.0) / TILE_SIZE as f32) as u32) .min(self.cols.saturating_sub(1)); let ty0 = ((cy - radius).max(0.0) / TILE_SIZE as f32) as u32; let ty1 = (((cy + radius).min(self.h as f32 - 1.0) / TILE_SIZE as f32) as u32) .min(self.h.div_ceil(TILE_SIZE).saturating_sub(1)); for ty in ty0..=ty1 { for tx in tx0..=tx1 { let idx = (ty * self.cols + tx) as usize; if !self.tiles[idx].dirty { self.tiles[idx].dirty = true; self.dirty_count += 1; } } } } fn mark_line_dirty(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, radius: f32) { let mut x0 = x0; let mut y0 = y0; let x1 = x1; let y1 = y1; let dx = (x1 - x0).abs(); let dy = (y1 - y0).abs(); let sx = if x0 < x1 { 1.0 } else { -1.0 }; let sy = if y0 < y1 { 1.0 } else { -1.0 }; let mut err = dx - dy; let max_steps = (dx + dy) as u32 + 1; for _ in 0..max_steps { self.mark_dirty_rect(x0, y0, radius); if (x0 - x1).abs() < f32::EPSILON && (y0 - y1).abs() < f32::EPSILON { break; } let e2 = 2.0 * err; if e2 > -dy { err -= dy; x0 += sx; } if e2 < dx { err += dx; y0 += sy; } } } fn mark_all_dirty(&mut self) { for t in &mut self.tiles { t.dirty = true; } self.dirty_count = self.tiles.len(); } fn upload_dirty(&mut self, ctx: &egui::Context) { if self.dirty_count == 0 { return; } let rows = self.h.div_ceil(TILE_SIZE); for ty in 0..rows { let tile_h = if ty == rows - 1 { self.h - ty * TILE_SIZE } else { TILE_SIZE } as usize; for tx in 0..self.cols { let idx = (ty * self.cols + tx) as usize; if !self.tiles[idx].dirty { continue; } self.tiles[idx].dirty = false; self.dirty_count = self.dirty_count.saturating_sub(1); let tile_w = if tx == self.cols - 1 { self.w - tx * TILE_SIZE } else { TILE_SIZE } as usize; let px = (tx * TILE_SIZE) as usize; let py = (ty * TILE_SIZE) as usize; let cap = tile_w * tile_h * 4; let mut data = vec![0u8; cap]; for row in 0..tile_h { let src = ((py + row) * self.w as usize + px) * 4; let dst = row * tile_w * 4; data[dst..dst + tile_w * 4] .copy_from_slice(&self.pixels[src..src + tile_w * 4]); } let img = ColorImage::from_rgba_unmultiplied([tile_w, tile_h], &data); let key = format!("t{}_{}", tx, ty); if let Some(tex) = &mut self.tiles[idx].texture { tex.set(img, egui::TextureOptions::NEAREST); } else { self.tiles[idx].texture = Some(ctx.load_texture(key, img, egui::TextureOptions::NEAREST)); } } } } } /// ── app state ──────────────────────────────────────────────────────────────── struct App { canvas: TiledCanvas, canvas_preset: CanvasPreset, preset: BrushPreset, color: [u8; 4], last_pt: Option<(f32, f32, f32)>, drawing: bool, last_frame: std::time::Instant, fps: f32, stroke_count: u64, fps_cap: FpsCap, } impl App { fn new(_cc: &eframe::CreationContext<'_>) -> Self { let preset = CanvasPreset::S1024; let (w, h) = preset.size(); let now = std::time::Instant::now(); Self { canvas: TiledCanvas::new(w, h), canvas_preset: preset, preset: BrushPreset::basic_round(), color: [0, 0, 0, 255], last_pt: None, drawing: false, last_frame: now, fps: 0.0, stroke_count: 0, fps_cap: FpsCap::Fps30, } } fn set_canvas_size(&mut self, p: CanvasPreset) { if self.canvas_preset == p { return; } self.canvas_preset = p; let (w, h) = p.size(); self.canvas = TiledCanvas::new(w, h); } } impl eframe::App for App { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { let ctx = ui.ctx().clone(); let now = std::time::Instant::now(); // real fps throttle — sleep remaining frame time if let Some(limit) = self.fps_cap.value() { let interval = std::time::Duration::from_secs_f32(1.0 / limit); let elapsed = now - self.last_frame; if elapsed < interval { std::thread::sleep(interval - elapsed); } } // fps counter — measure after sleep for accurate reading let after_sleep = std::time::Instant::now(); let dt = (after_sleep - self.last_frame).as_secs_f32(); self.last_frame = after_sleep; if dt > 0.0 { self.fps = self.fps * 0.9 + (1.0 / dt) * 0.1; } self.canvas.upload_dirty(&ctx); // ── left panel ─────────────────────────────────────────────────── egui::Panel::left("panel").show(&ctx, |ui| { ui.heading("Brush Test"); let limit_label = match self.fps_cap { FpsCap::Fps30 => "30", FpsCap::Fps60 => "60", FpsCap::Fps165 => "165", FpsCap::Unlimited => "∞", }; ui.label(format!( "FPS: {:.0}/{} │ dirty: {} │ strokes: {}", self.fps, limit_label, self.canvas.dirty_count, self.stroke_count )); ui.separator(); ui.label("Canvas"); egui::ComboBox::from_id_salt("canvas_preset") .selected_text(self.canvas_preset.name()) .show_ui(ui, |ui| { for p in &[ CanvasPreset::S512, CanvasPreset::S1024, CanvasPreset::S2048, CanvasPreset::S4096, ] { if ui .selectable_label(self.canvas_preset == *p, p.name()) .clicked() { self.set_canvas_size(*p); } } }); ui.label("FPS Cap"); egui::ComboBox::from_id_salt("fps_cap") .selected_text(self.fps_cap.name()) .show_ui(ui, |ui| { for c in &[ FpsCap::Fps30, FpsCap::Fps60, FpsCap::Fps165, FpsCap::Unlimited, ] { if ui.selectable_label(self.fps_cap == *c, c.name()).clicked() { self.fps_cap = *c; } } }); ui.separator(); ui.label("Brush"); egui::ComboBox::from_id_salt("brush_preset") .selected_text(&self.preset.name) .show_ui(ui, |ui| { for p in BrushPreset::all_defaults() { ui.selectable_value(&mut self.preset, p.clone(), &p.name); } }); ui.add(egui::Slider::new(&mut self.preset.tip.size, 1.0..=500.0).text("Size")); let caps = slider_caps(self.preset.style); ui.add_enabled( caps.hardness, egui::Slider::new(&mut self.preset.tip.hardness, 0.0..=1.0).text("Hardness"), ); ui.add_enabled( caps.opacity, egui::Slider::new(&mut self.preset.tip.opacity, 0.0..=1.0).text("Opacity"), ); ui.add_enabled( caps.spacing, egui::Slider::new(&mut self.preset.tip.spacing, 0.01..=1.0).text("Spacing"), ); ui.add_enabled( caps.flow, egui::Slider::new(&mut self.preset.tip.flow, 0.01..=1.0).text("Flow"), ); ui.add_enabled( caps.jitter, egui::Slider::new(&mut self.preset.tip.jitter_amount, 0.0..=5.0).text("Jitter"), ); ui.add_enabled( caps.scatter, egui::Slider::new(&mut self.preset.tip.scatter_amount, 0.0..=1.0).text("Scatter"), ); ui.add_enabled( caps.angle, egui::Slider::new(&mut self.preset.tip.angle, 0.0..=360.0).text("Angle"), ); ui.add_enabled( caps.roundness, egui::Slider::new(&mut self.preset.tip.roundness, 0.0..=1.0).text("Roundness"), ); ui.add_enabled( caps.spray_particle_size, egui::Slider::new(&mut self.preset.tip.spray_particle_size, 0.5..=20.0) .text("Spray Size"), ); ui.add_enabled( caps.spray_density, egui::Slider::new(&mut self.preset.tip.spray_density, 1..=500) .text("Spray Density"), ); let mut rgba = [ self.color[0] as f32 / 255., self.color[1] as f32 / 255., self.color[2] as f32 / 255., self.color[3] as f32 / 255., ]; if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() { self.color = [ (rgba[0] * 255.) as u8, (rgba[1] * 255.) as u8, (rgba[2] * 255.) as u8, (rgba[3] * 255.) as u8, ]; } if ui.button("Clear").clicked() { self.canvas.pixels.fill(255); self.canvas.mark_all_dirty(); self.stroke_count = 0; } }); // ── central canvas ──────────────────────────────────────────────── egui::CentralPanel::default().show(&ctx, |ui| { let available = ui.available_size_before_wrap(); let view_w = available.x.max(64.0); let view_h = available.y.max(64.0); let scale = (view_w / self.canvas.w as f32).min(view_h / self.canvas.h as f32); let disp_w = self.canvas.w as f32 * scale; let disp_h = self.canvas.h as f32 * scale; let (rect, resp) = ui.allocate_exact_size(egui::vec2(disp_w, disp_h), egui::Sense::drag()); // paint tiles if self .canvas .tiles .first() .and_then(|t| t.texture.as_ref()) .is_some() { let rows = self.canvas.h.div_ceil(TILE_SIZE); for ty in 0..rows { for tx in 0..self.canvas.cols { let idx = (ty * self.canvas.cols + tx) as usize; if let Some(tex) = &self.canvas.tiles[idx].texture { let tile_w = if tx == self.canvas.cols - 1 { self.canvas.w - tx * TILE_SIZE } else { TILE_SIZE } as f32; let tile_h = if ty == rows - 1 { self.canvas.h - ty * TILE_SIZE } else { TILE_SIZE } as f32; let src_x = (tx * TILE_SIZE) as f32 * scale; let src_y = (ty * TILE_SIZE) as f32 * scale; let r = egui::Rect::from_min_size( rect.min + egui::vec2(src_x, src_y), egui::vec2(tile_w * scale, tile_h * scale), ); ui.painter().image( tex.id(), r, egui::Rect::from_min_max(egui::pos2(0., 0.), egui::pos2(1., 1.)), Color32::WHITE, ); } } } } // input if resp.dragged() { self.drawing = true; if let Some(pos) = resp.interact_pointer_pos() { let cx = (pos.x - rect.min.x) / scale; let cy = (pos.y - rect.min.y) / scale; if cx >= 0.0 && cy >= 0.0 && cx < self.canvas.w as f32 && cy < self.canvas.h as f32 { let pt = (cx, cy, 1.0); let seg = if let Some(l) = self.last_pt { vec![l, pt] } else { vec![pt] }; self.last_pt = Some(pt); self.canvas.mark_line_dirty( seg[0].0, seg[0].1, seg[seg.len() - 1].0, seg[seg.len() - 1].1, self.preset.tip.size, ); hcie_brush_engine::draw_specialized_stroke( &mut self.canvas.pixels, self.canvas.w, self.canvas.h, &seg, self.preset.style, self.preset.tip.size, self.preset.tip.hardness, self.color, self.preset.tip.opacity, self.preset.tip.spacing, false, None, self.preset.tip.spray_particle_size, self.preset.tip.spray_density, None, None, None, self.preset.tip.color_variant, self.preset.tip.variant_amount, self.preset.tip.density, self.preset.tip.jitter_amount, self.preset.tip.scatter_amount, self.preset.tip.angle, self.preset.tip.roundness, self.preset.tip.rotation_random, self.preset.tip.drawing_angle, true, ); self.stroke_count += 1; // force upload now so stroke appears immediately self.canvas.upload_dirty(&ctx); } } } else if self.drawing { self.drawing = false; self.last_pt = None; } }); // keep requesting repaints so egui keeps calling update ctx.request_repaint(); } } fn main() -> eframe::Result<()> { eframe::run_native( "HCIE Brush Engine — Tiled GPU Test", eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([1400., 900.]), vsync: true, ..Default::default() }, Box::new(|cc| Ok(Box::new(App::new(cc)))), ) }