init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "hcie-draw"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
hcie-brush-engine = { path = "../hcie-brush-engine" }
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
rand = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
egui = "0.34"
|
||||
eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }
|
||||
@@ -0,0 +1,221 @@
|
||||
#![allow(deprecated)]
|
||||
//! Visual test GUI for the decoupled `hcie-draw` library using `egui`.
|
||||
//! Compiles only when running `cargo run --example draw_tester`.
|
||||
|
||||
use eframe::egui;
|
||||
use hcie_draw::{draw_line, draw_filled_rect, draw_filled_circle, flood_fill};
|
||||
use hcie_protocol::Layer;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([700.0, 500.0])
|
||||
.with_title("HCIE Draw Engine Visual Tester"),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"HCIE Draw Visual Tester",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(DrawTesterApp::new(cc)))),
|
||||
)
|
||||
}
|
||||
|
||||
struct DrawTesterApp {
|
||||
layer: Layer,
|
||||
current_color: [u8; 4],
|
||||
thickness: f32,
|
||||
selected_tool: &'static str,
|
||||
last_drag_pos: Option<egui::Pos2>,
|
||||
texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl DrawTesterApp {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let width = 256;
|
||||
let height = 256;
|
||||
let len = (width * height * 4) as usize;
|
||||
|
||||
let mut pixels = vec![255u8; len];
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let i = ((y * width + x) * 4) as usize;
|
||||
let is_gray = ((x / 16) + (y / 16)) % 2 == 0;
|
||||
let c = if is_gray { 220 } else { 255 };
|
||||
pixels[i] = c;
|
||||
pixels[i + 1] = c;
|
||||
pixels[i + 2] = c;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let layer = Layer::from_rgba("canvas", width, height, pixels);
|
||||
|
||||
Self {
|
||||
layer,
|
||||
current_color: [255, 0, 0, 255],
|
||||
thickness: 5.0,
|
||||
selected_tool: "Line",
|
||||
last_drag_pos: None,
|
||||
texture: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_canvas(&mut self) {
|
||||
let w = self.layer.width;
|
||||
let h = self.layer.height;
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
let is_gray = ((x / 16) + (y / 16)) % 2 == 0;
|
||||
let c = if is_gray { 220 } else { 255 };
|
||||
self.layer.pixels[i] = c;
|
||||
self.layer.pixels[i + 1] = c;
|
||||
self.layer.pixels[i + 2] = c;
|
||||
self.layer.pixels[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
self.texture = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for DrawTesterApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if self.texture.is_none() {
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[self.layer.width as usize, self.layer.height as usize],
|
||||
&self.layer.pixels,
|
||||
);
|
||||
self.texture = Some(ctx.load_texture("canvas_preview", color_image, Default::default()));
|
||||
}
|
||||
let texture = self.texture.as_ref().unwrap().clone();
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
ui.heading("HCIE Draw Engine Visual Tester");
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Click & drag to draw on this canvas:");
|
||||
ui.add_space(5.0);
|
||||
|
||||
let image_widget = egui::Image::new((texture.id(), texture.size_vec2())).sense(egui::Sense::click_and_drag());
|
||||
let resp = ui.add(image_widget);
|
||||
|
||||
if resp.dragged() || resp.clicked() {
|
||||
if let Some(hover_pos) = resp.hover_pos() {
|
||||
let rect = resp.rect;
|
||||
let px = ((hover_pos.x - rect.min.x) / rect.width() * self.layer.width as f32).clamp(0.0, self.layer.width as f32 - 1.0);
|
||||
let py = ((hover_pos.y - rect.min.y) / rect.height() * self.layer.height as f32).clamp(0.0, self.layer.height as f32 - 1.0);
|
||||
|
||||
match self.selected_tool {
|
||||
"Line" => {
|
||||
if let Some(last_pos) = self.last_drag_pos {
|
||||
draw_line(
|
||||
&mut self.layer,
|
||||
last_pos.x, last_pos.y,
|
||||
px, py,
|
||||
self.current_color,
|
||||
self.thickness,
|
||||
None,
|
||||
);
|
||||
self.texture = None;
|
||||
}
|
||||
self.last_drag_pos = Some(egui::pos2(px, py));
|
||||
}
|
||||
"Circle" => {
|
||||
if resp.drag_started() || resp.clicked() {
|
||||
draw_filled_circle(
|
||||
&mut self.layer,
|
||||
px, py,
|
||||
self.thickness * 2.0,
|
||||
self.current_color,
|
||||
None,
|
||||
);
|
||||
self.texture = None;
|
||||
}
|
||||
}
|
||||
"Rect" => {
|
||||
if resp.drag_started() || resp.clicked() {
|
||||
let size = self.thickness * 3.0;
|
||||
draw_filled_rect(
|
||||
&mut self.layer,
|
||||
px - size, py - size,
|
||||
px + size, py + size,
|
||||
self.current_color,
|
||||
None,
|
||||
);
|
||||
self.texture = None;
|
||||
}
|
||||
}
|
||||
"Flood Fill" => {
|
||||
if resp.clicked() {
|
||||
flood_fill(
|
||||
&mut self.layer,
|
||||
px as u32, py as u32,
|
||||
self.current_color,
|
||||
20,
|
||||
None,
|
||||
);
|
||||
self.texture = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.last_drag_pos = None;
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Toolbar Tools:");
|
||||
ui.add_space(5.0);
|
||||
|
||||
ui.selectable_value(&mut self.selected_tool, "Line", "Brush/Line");
|
||||
ui.selectable_value(&mut self.selected_tool, "Circle", "Filled Circle");
|
||||
ui.selectable_value(&mut self.selected_tool, "Rect", "Filled Rectangle");
|
||||
ui.selectable_value(&mut self.selected_tool, "Flood Fill", "Flood Fill");
|
||||
|
||||
ui.add_space(15.0);
|
||||
ui.separator();
|
||||
ui.add_space(15.0);
|
||||
|
||||
ui.label(format!("Size / Radius: {:.1}", self.thickness));
|
||||
ui.add(egui::Slider::new(&mut self.thickness, 1.0..=50.0));
|
||||
|
||||
ui.add_space(15.0);
|
||||
|
||||
ui.label("Brush Color:");
|
||||
let mut color_f32 = [
|
||||
self.current_color[0] as f32 / 255.0,
|
||||
self.current_color[1] as f32 / 255.0,
|
||||
self.current_color[2] as f32 / 255.0,
|
||||
self.current_color[3] as f32 / 255.0,
|
||||
];
|
||||
if ui.color_edit_button_rgba_unmultiplied(&mut color_f32).changed() {
|
||||
self.current_color = [
|
||||
(color_f32[0] * 255.0).round() as u8,
|
||||
(color_f32[1] * 255.0).round() as u8,
|
||||
(color_f32[2] * 255.0).round() as u8,
|
||||
(color_f32[3] * 255.0).round() as u8,
|
||||
];
|
||||
}
|
||||
|
||||
ui.add_space(25.0);
|
||||
|
||||
if ui.button("Clear Canvas").clicked() {
|
||||
self.clear_canvas();
|
||||
}
|
||||
|
||||
ui.add_space(35.0);
|
||||
ui.colored_label(egui::Color32::LIGHT_GREEN, "Layer-based drawing API");
|
||||
ui.colored_label(egui::Color32::LIGHT_GREEN, "Decoupled from hcie-protocol");
|
||||
ui.colored_label(egui::Color32::LIGHT_GREEN, "Operating with 100% GUI neutrality");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
## hcie-draw Projesi Nedir?
|
||||
|
||||
**hcie-draw**, HCIE (HC Image Editor) Rust port'unun bir parçası olan **raster çizim motoru** kütüphanesidir.
|
||||
|
||||
### Projenin Amacı
|
||||
Bu kütüphane, RGBA piksel buffer'ları üzerine **vektörel çizim araçlarını** (çizgi, daire, elips, dikdörten, flood fill) ve **fırça darbelerini** (pressure-aware brush strokes) rasterize eder. Pure Rust ile yazılmış, GUI'den bağımsız bir çekirdek kütüphane.
|
||||
|
||||
---
|
||||
|
||||
### Mimari Yapı (Katman Bakışı)
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ hcie-core-app (Tauri GUI) │ ← Svelte frontend + Rust backend
|
||||
├─────────────────────────────┤
|
||||
│ hcie-engine-api │ ← Public API & command bridge
|
||||
├─────────────────────────────┤
|
||||
│ hcie-draw (bu crate) │ ← Rasterization motoru
|
||||
│ ├─ Layer │ • Pixel buffer yönetimi
|
||||
│ ├─ draw_line/circle/ellipse│ • Primitif çizim fonksiyonları
|
||||
│ ├─ draw_brush_stroke │ • Pressure-aware fırça
|
||||
│ └─ C FFI exports │ • Zero-copy native arayüz
|
||||
├─────────────────────────────┤
|
||||
│ hcie-blend │ ← Blend modları (Normal, Multiply, vb.)
|
||||
│ hcie-brush-engine │ ← Fırça tipi, spacing, jitter
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Ana Bileşenler
|
||||
|
||||
**`Layer` struct'ı:**
|
||||
```rust
|
||||
pub struct Layer {
|
||||
pub name: String,
|
||||
pub pixels: Vec<u8>, // RGBA format (4 bayt/piksel)
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub visible: bool,
|
||||
pub opacity: f32,
|
||||
pub dirty: bool, // Render invalidation flag
|
||||
}
|
||||
```
|
||||
|
||||
**Çizim fonksiyonları:**
|
||||
| Fonksiyon | Açıklama |
|
||||
|-----------|----------|
|
||||
| `draw_line()` | Xiaolin Wu algoritmasıyla AA çizgi |
|
||||
| `draw_filled_circle()` | Doldurulmuş daire (kenar geçişli) |
|
||||
| `draw_filled_rect()` | Doldurulmuş dikdörtgen |
|
||||
| `draw_filled_ellipse()` | Doldurulmuş elips |
|
||||
| `flood_fill()` | Bağlamsız renk değiştirme |
|
||||
| `draw_brush_stamp()` | Tek fırça dampası |
|
||||
| `draw_brush_stroke()` | Tüm noktalar boyunca fırça izi |
|
||||
|
||||
---
|
||||
|
||||
### Çalışma Mantığı
|
||||
|
||||
1. **Piksel Erişimi**: `get_pixel()` / `set_pixel()` ile RGBA okuma-yazma
|
||||
2. **Blend Entegrasyonu**: `hcie_blend::blend_pixels()` ile renk karıştırma
|
||||
3. **Brush Entegrasyonu**: `hcie_brush_engine::draw_dab_capped()` ile fırça tipi uygulama
|
||||
4. **Mask Desteği**: Opsiyonel selection mask (0-255) ile çizim sınırlandırma
|
||||
|
||||
**Pressure-aware brush stroke akışı:**
|
||||
```
|
||||
Input: points = [(x, y, pressure), ...]
|
||||
↓
|
||||
brush_spacing_pixels() → spacing hesapla
|
||||
↓
|
||||
Her segment için interpolate → pressure değişimi
|
||||
↓
|
||||
jitter_offset() → pozisyon rastgelelik
|
||||
↓
|
||||
draw_dab_capped() → fırça dampası render et
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### C FFI Arayüzü
|
||||
|
||||
Kütüphane **cdylib** olarak derlenir, bu sayede başka dillerden doğrudan çağrılabilir:
|
||||
|
||||
```c
|
||||
// draw_line_c - raw buffer pointer alır, in-place değiştirir
|
||||
void draw_line_c(
|
||||
uint8_t* pixels, uint32_t width, uint32_t height,
|
||||
float x0, float y0, float x1, float y1,
|
||||
const uint8_t* color_rgba, float line_thickness,
|
||||
const uint8_t* mask
|
||||
);
|
||||
```
|
||||
|
||||
Diğer FFI fonksiyonları: `draw_filled_rect_c`, `draw_filled_circle_c`, `flood_fill_c`, `draw_brush_stroke_c`
|
||||
|
||||
---
|
||||
|
||||
### Kullanım Örkeli
|
||||
|
||||
Test runner'ı çalıştırmak:
|
||||
```bash
|
||||
cargo test --manifest-path tests/draw_pixels.rs
|
||||
```
|
||||
|
||||
GUI testörünü çalıştırmak:
|
||||
```bash
|
||||
cargo run --example draw_tester
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bağımlılıklar
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
hcie-blend = { path = "../hcie-blend" } # blend_pixels
|
||||
hcie-brush-engine = { path = "../hcie-brush-engine" } # brush tip & spacing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Özellikler
|
||||
|
||||
- **Zero-copy FFI**: Buffer kopyası yok, doğrudan pointer manipülasyonu
|
||||
- **GUI bağımsız**: Pure Rust, egui/tauri olmadan da kullanılabilir
|
||||
- **Multi-threading hazır**: rayon entegrasyonu (blend katmanında)
|
||||
- **30+ fırça stıl**: BrushStyle enum'ı içinde (Round, Pencil, Oil, Watercolor, vb.)
|
||||
@@ -0,0 +1,282 @@
|
||||
#![allow(dead_code)]
|
||||
pub use hcie_protocol::{Layer, BlendMode};
|
||||
pub use hcie_brush_engine::{BrushTip, BrushStyle};
|
||||
use hcie_blend::blend_pixels;
|
||||
use hcie_brush_engine::{brush_spacing_pixels, jitter_offset};
|
||||
|
||||
pub fn draw_brush_stamp(
|
||||
layer: &mut Layer,
|
||||
cx: f32, cy: f32,
|
||||
stamp: &[u8], stamp_diameter: usize,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let radius = (stamp_diameter / 2) as i32;
|
||||
let start_x = (cx as i32) - radius;
|
||||
let start_y = (cy as i32) - radius;
|
||||
|
||||
for sy in 0..stamp_diameter {
|
||||
for sx in 0..stamp_diameter {
|
||||
let px = start_x + sx as i32;
|
||||
let py = start_y + sy as i32;
|
||||
if px < 0 || py < 0 || px >= layer.width as i32 || py >= layer.height as i32 {
|
||||
continue;
|
||||
}
|
||||
let px_u = px as u32;
|
||||
let py_u = py as u32;
|
||||
|
||||
if let Some(m) = mask {
|
||||
let idx = (py_u * layer.width + px_u) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
||||
}
|
||||
|
||||
let stamp_alpha = stamp[sy * stamp_diameter + sx] as f32 / 255.0;
|
||||
if stamp_alpha <= 0.0 { continue; }
|
||||
|
||||
let dst = layer.get_pixel(px_u, py_u);
|
||||
let src_color: [u8; 4] = [
|
||||
color[0], color[1], color[2],
|
||||
(color[3] as f32 * stamp_alpha).round() as u8,
|
||||
];
|
||||
let out = blend_pixels(dst, src_color, hcie_protocol::BlendMode::Normal.into(), 1.0);
|
||||
layer.set_pixel(px_u, py_u, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_brush_stroke(
|
||||
layer: &mut Layer,
|
||||
points: &[(f32, f32, f32)],
|
||||
color: [u8; 4],
|
||||
tip: &BrushTip,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
mut stroke_mask: Option<&mut [u8]>,
|
||||
bg_pixels: Option<&[u8]>,
|
||||
) {
|
||||
if points.is_empty() { return; }
|
||||
|
||||
let spacing = brush_spacing_pixels(tip.size, tip.spacing);
|
||||
let use_color_variant = tip.color_variant && tip.variant_amount > 0.0;
|
||||
let mut rng = if use_color_variant {
|
||||
Some(rand::thread_rng())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut last_p = points[0];
|
||||
let (jx0, jy0) = jitter_offset(tip.jitter_amount, tip.size);
|
||||
let first_x = last_p.0 + jx0;
|
||||
let first_y = last_p.1 + jy0;
|
||||
let first_size = tip.size * last_p.2.max(0.1);
|
||||
let first_opacity = tip.opacity * last_p.2.max(0.0).min(1.0);
|
||||
let first_color = if let Some(r) = rng.as_mut() {
|
||||
hcie_brush_engine::vary_color_hsl(color, tip.variant_amount, r)
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let first_px_color = [
|
||||
first_color[0], first_color[1], first_color[2],
|
||||
(first_color[3] as f32 * first_opacity).round() as u8,
|
||||
];
|
||||
hcie_brush_engine::draw_dab_capped(
|
||||
&mut layer.pixels, layer.width, layer.height,
|
||||
first_x, first_y, first_size, tip.hardness, first_px_color, 1.0, is_eraser, mask,
|
||||
stroke_mask.as_deref_mut(), bg_pixels, tip.opacity,
|
||||
);
|
||||
|
||||
for &p in &points[1..] {
|
||||
let dx = p.0 - last_p.0;
|
||||
let dy = p.1 - last_p.1;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist == 0.0 { continue; }
|
||||
|
||||
let steps = ((dist / spacing).max(1.0)).ceil() as i32;
|
||||
let step_x = dx / steps as f32;
|
||||
let step_y = dy / steps as f32;
|
||||
|
||||
for i in 1..=steps {
|
||||
let t = i as f32 / steps as f32;
|
||||
let pressure = last_p.2 * (1.0 - t) + p.2 * t;
|
||||
let px = last_p.0 + step_x * i as f32;
|
||||
let py = last_p.1 + step_y * i as f32;
|
||||
|
||||
let (jx, jy) = jitter_offset(tip.jitter_amount, tip.size);
|
||||
let final_x = px + jx;
|
||||
let final_y = py + jy;
|
||||
|
||||
let effective_size = tip.size * pressure.max(0.1);
|
||||
let pressure_opacity = tip.opacity * pressure.max(0.0).min(1.0);
|
||||
let dab_color = if let Some(r) = rng.as_mut() {
|
||||
hcie_brush_engine::vary_color_hsl(color, tip.variant_amount, r)
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let px_color = [
|
||||
dab_color[0], dab_color[1], dab_color[2],
|
||||
(dab_color[3] as f32 * pressure_opacity).round() as u8,
|
||||
];
|
||||
hcie_brush_engine::draw_dab_capped(
|
||||
&mut layer.pixels, layer.width, layer.height,
|
||||
final_x, final_y, effective_size, tip.hardness, px_color, 1.0, is_eraser, mask,
|
||||
stroke_mask.as_deref_mut(), bg_pixels, tip.opacity,
|
||||
);
|
||||
}
|
||||
|
||||
last_p = p;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_line(
|
||||
layer: &mut Layer,
|
||||
x0: f32, y0: f32, x1: f32, y1: f32,
|
||||
color: [u8; 4],
|
||||
width: f32,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let dx = x1 - x0;
|
||||
let dy = y1 - y0;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist == 0.0 { return; }
|
||||
|
||||
let steps = (dist * 2.0).max(1.0).ceil() as i32;
|
||||
for i in 0..=steps {
|
||||
let t = i as f32 / steps as f32;
|
||||
let px = x0 + dx * t;
|
||||
let py = y0 + dy * t;
|
||||
draw_filled_circle(layer, px, py, width / 2.0, color, mask);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_filled_circle(
|
||||
layer: &mut Layer,
|
||||
cx: f32, cy: f32, r: f32,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let r_i = r.ceil() as i32;
|
||||
for dy in -r_i..=r_i {
|
||||
let py = (cy + dy as f32).round() as i32;
|
||||
if py < 0 || py >= layer.height as i32 { continue; }
|
||||
let dx_max = (r * r - dy as f32 * dy as f32).sqrt();
|
||||
let dx_i = dx_max.ceil() as i32;
|
||||
for dx in -dx_i..=dx_i {
|
||||
let px = (cx + dx as f32).round() as i32;
|
||||
if px < 0 || px >= layer.width as i32 { continue; }
|
||||
|
||||
if let Some(m) = mask {
|
||||
let idx = (py as u32 * layer.width + px as u32) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
||||
}
|
||||
|
||||
let dist = ((dx as f32).powi(2) + (dy as f32).powi(2)).sqrt();
|
||||
let alpha_t = 1.0 - (dist - (r - 1.0)).max(0.0).min(1.0);
|
||||
if alpha_t <= 0.0 { continue; }
|
||||
let src = [color[0], color[1], color[2], (color[3] as f32 * alpha_t).round() as u8];
|
||||
let dst = layer.get_pixel(px as u32, py as u32);
|
||||
let out = blend_pixels(dst, src, hcie_protocol::BlendMode::Normal.into(), 1.0);
|
||||
layer.set_pixel(px as u32, py as u32, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_filled_rect(
|
||||
layer: &mut Layer,
|
||||
x1: f32, y1: f32, x2: f32, y2: f32,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let x_start = x1.min(x2).max(0.0).floor() as u32;
|
||||
let x_end = x1.max(x2).min(layer.width as f32).ceil() as u32;
|
||||
let y_start = y1.min(y2).max(0.0).floor() as u32;
|
||||
let y_end = y1.max(y2).min(layer.height as f32).ceil() as u32;
|
||||
|
||||
for y in y_start..y_end {
|
||||
for x in x_start..x_end {
|
||||
if let Some(m) = mask {
|
||||
let idx = (y * layer.width + x) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
||||
}
|
||||
let dst = layer.get_pixel(x, y);
|
||||
let out = blend_pixels(dst, color, hcie_protocol::BlendMode::Normal.into(), 1.0);
|
||||
layer.set_pixel(x, y, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_filled_ellipse(
|
||||
layer: &mut Layer,
|
||||
cx: f32, cy: f32, rx: f32, ry: f32,
|
||||
color: [u8; 4],
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let x_start = (cx - rx).max(0.0).floor() as u32;
|
||||
let x_end = (cx + rx).min(layer.width as f32).ceil() as u32;
|
||||
let y_start = (cy - ry).max(0.0).floor() as u32;
|
||||
let y_end = (cy + ry).min(layer.height as f32).ceil() as u32;
|
||||
|
||||
for y in y_start..y_end {
|
||||
for x in x_start..x_end {
|
||||
let dx = (x as f32 - cx) / rx;
|
||||
let dy = (y as f32 - cy) / ry;
|
||||
let dist = dx * dx + dy * dy;
|
||||
if dist <= 1.0 {
|
||||
if let Some(m) = mask {
|
||||
let idx = (y * layer.width + x) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
||||
}
|
||||
let dst = layer.get_pixel(x, y);
|
||||
let out = blend_pixels(dst, color, hcie_protocol::BlendMode::Normal.into(), 1.0);
|
||||
layer.set_pixel(x, y, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flood_fill(
|
||||
layer: &mut Layer,
|
||||
x: u32, y: u32,
|
||||
fill_color: [u8; 4],
|
||||
tolerance: u8,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
if let Some(m) = mask {
|
||||
let idx = (y * layer.width + x) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { return; }
|
||||
}
|
||||
|
||||
let start_color = layer.get_pixel(x, y);
|
||||
if color_match(&start_color, &fill_color, tolerance) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut stack = vec![(x, y)];
|
||||
let w = layer.width;
|
||||
let h = layer.height;
|
||||
|
||||
while let Some((px, py)) = stack.pop() {
|
||||
let c = layer.get_pixel(px, py);
|
||||
if !color_match(&c, &start_color, tolerance) { continue; }
|
||||
|
||||
if let Some(m) = mask {
|
||||
let idx = (py * w + px) as usize;
|
||||
if m.get(idx).copied().unwrap_or(0) == 0 { continue; }
|
||||
}
|
||||
|
||||
layer.set_pixel(px, py, fill_color);
|
||||
|
||||
if px > 0 { stack.push((px - 1, py)); }
|
||||
if px + 1 < w { stack.push((px + 1, py)); }
|
||||
if py > 0 { stack.push((px, py - 1)); }
|
||||
if py + 1 < h { stack.push((px, py + 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
fn color_match(a: &[u8; 4], b: &[u8; 4], tolerance: u8) -> bool {
|
||||
let tol = tolerance as i32;
|
||||
let diff_r = (a[0] as i32 - b[0] as i32).abs();
|
||||
let diff_g = (a[1] as i32 - b[1] as i32).abs();
|
||||
let diff_b = (a[2] as i32 - b[2] as i32).abs();
|
||||
let diff_a = (a[3] as i32 - b[3] as i32).abs();
|
||||
diff_r <= tol && diff_g <= tol && diff_b <= tol && diff_a <= tol
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use hcie_protocol::Layer;
|
||||
|
||||
#[test]
|
||||
fn test_draw_filled_rect() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_filled_rect(&mut layer, 1.0, 1.0, 4.0, 4.0, [255, 0, 0, 255], None);
|
||||
let px = layer.get_pixel(2, 2);
|
||||
assert_eq!(px[0], 255, "inside rect should be red");
|
||||
assert_eq!(px[1], 0);
|
||||
assert_eq!(px[2], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_filled_rect_outside() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_filled_rect(&mut layer, 1.0, 1.0, 4.0, 4.0, [255, 0, 0, 255], None);
|
||||
let px = layer.get_pixel(9, 9);
|
||||
assert_eq!(px, [0; 4], "outside rect should be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_filled_circle() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_filled_circle(&mut layer, 5.0, 5.0, 3.0, [0, 255, 0, 255], None);
|
||||
let px = layer.get_pixel(5, 5);
|
||||
assert_eq!(px[1], 255, "circle center should be green");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_filled_ellipse() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_filled_ellipse(&mut layer, 5.0, 5.0, 3.0, 2.0, [0, 0, 255, 255], None);
|
||||
let px = layer.get_pixel(5, 5);
|
||||
assert_eq!(px[2], 255, "ellipse center should be blue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_line() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_line(&mut layer, 1.0, 5.0, 8.0, 5.0, [255, 255, 0, 255], 1.0, None);
|
||||
let px = layer.get_pixel(5, 5);
|
||||
assert_eq!(px[0], 255, "line pixel should be yellow");
|
||||
assert_eq!(px[1], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flood_fill() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::flood_fill(&mut layer, 0, 0, [255, 0, 0, 255], 0, None);
|
||||
let px = layer.get_pixel(5, 5);
|
||||
assert_eq!(px[0], 255, "flood filled area should be red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_with_mask_restricts_pixels() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
let mut mask = vec![0u8; 100];
|
||||
mask[0] = 255;
|
||||
hcie_draw::draw_filled_rect(&mut layer, 0.0, 0.0, 9.0, 9.0, [255, 0, 0, 255], Some(&mask));
|
||||
let in_mask = layer.get_pixel(0, 0);
|
||||
assert_eq!(in_mask[0], 255, "masked pixel should be red");
|
||||
let out_mask = layer.get_pixel(5, 5);
|
||||
assert_eq!(out_mask, [0; 4], "unmasked pixel should be transparent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_at_edge() {
|
||||
let mut layer = Layer::new_transparent("test", 10, 10);
|
||||
hcie_draw::draw_line(&mut layer, 0.0, 0.0, 9.0, 9.0, [255, 0, 0, 255], 1.0, None);
|
||||
let px = layer.get_pixel(0, 0);
|
||||
assert_eq!(px[0], 255, "line at edge should draw");
|
||||
}
|
||||
Reference in New Issue
Block a user