init
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
//! Automated visual check for the Leaves brush.
|
||||
//!
|
||||
//! Purpose: Renders several Leaf strokes onto a transparent canvas using the
|
||||
//! public engine API and saves the result as a PNG. Used for manual inspection
|
||||
//! and regression detection.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test leaves_check and inspect target/leaves_check.png"]
|
||||
fn leaves_brush_visual_check() {
|
||||
let mut engine = Engine::new_with_options("LeavesCheck", 512, 512, false);
|
||||
let layer_id = engine.active_layer_id();
|
||||
|
||||
let mut tip = BrushTip::default();
|
||||
tip.style = BrushStyle::Leaf;
|
||||
tip.size = 70.0;
|
||||
tip.opacity = 0.95;
|
||||
tip.hardness = 0.85;
|
||||
tip.spacing = 0.35;
|
||||
tip.color_variant = true;
|
||||
tip.variant_amount = 0.5;
|
||||
engine.set_brush_tip(tip);
|
||||
engine.set_color([40, 140, 40, 255]);
|
||||
|
||||
engine.begin_stroke(layer_id, 100.0, 400.0);
|
||||
for i in 0..=30 {
|
||||
let t = i as f32 / 30.0;
|
||||
let x = 80.0 + t * 120.0;
|
||||
let y = 400.0 - (t * 40.0).sin() * 20.0 - t * 30.0;
|
||||
engine.stroke_to(layer_id, x, y, 0.6 + 0.4 * (1.0 - t));
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
engine.begin_stroke(layer_id, 300.0, 420.0);
|
||||
for i in 0..=25 {
|
||||
let t = i as f32 / 25.0;
|
||||
let x = 300.0 + t * 10.0;
|
||||
let y = 420.0 - t * 80.0;
|
||||
engine.stroke_to(layer_id, x, y, 1.0);
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("leaves_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Leaves check produced no colored pixels");
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Automated visual check for the Meadow brush.
|
||||
//!
|
||||
//! Purpose: Renders several Meadow strokes onto a transparent canvas using the
|
||||
//! public engine API and saves the result as a PNG. This bypasses the GUI so
|
||||
//! brush behaviour can be inspected directly, and doubles as a regression
|
||||
//! guard for color-variant and blade geometry.
|
||||
//!
|
||||
//! Logic & Workflow:
|
||||
//! 1. Create a 512x512 white/opaque canvas.
|
||||
//! 2. Configure a Meadow brush with color_variant enabled and a green base color.
|
||||
//! 3. Draw multiple short strokes at different positions.
|
||||
//! 4. Save the composited pixels to `target/meadow_check.png`.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test meadow_check and inspect target/meadow_check.png"]
|
||||
fn meadow_brush_visual_check() {
|
||||
let mut engine = Engine::new_with_options("MeadowCheck", 512, 512, false);
|
||||
let layer_id = engine.active_layer_id();
|
||||
|
||||
let mut tip = BrushTip::default();
|
||||
tip.style = BrushStyle::Meadow;
|
||||
tip.size = 80.0;
|
||||
tip.opacity = 0.95;
|
||||
tip.hardness = 0.85;
|
||||
tip.spacing = 0.40;
|
||||
tip.color_variant = true;
|
||||
tip.variant_amount = 0.6;
|
||||
engine.set_brush_tip(tip);
|
||||
engine.set_color([50, 200, 80, 255]);
|
||||
|
||||
// Stroke 1: a short wavy tuft.
|
||||
engine.begin_stroke(layer_id, 150.0, 400.0);
|
||||
for i in 0..=30 {
|
||||
let t = i as f32 / 30.0;
|
||||
let x = 100.0 + t * 120.0;
|
||||
let y = 400.0 - (t * 60.0).sin() * 25.0 - t * 40.0;
|
||||
engine.stroke_to(layer_id, x, y, 0.6 + 0.4 * (1.0 - t));
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
// Stroke 2: vertical grass clump.
|
||||
engine.begin_stroke(layer_id, 300.0, 420.0);
|
||||
for i in 0..=25 {
|
||||
let t = i as f32 / 25.0;
|
||||
let x = 300.0 + t * 10.0;
|
||||
let y = 420.0 - t * 90.0;
|
||||
engine.stroke_to(layer_id, x, y, 1.0);
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
// Stroke 3: a few scattered dabs.
|
||||
engine.begin_stroke(layer_id, 420.0, 410.0);
|
||||
engine.stroke_to(layer_id, 430.0, 360.0, 1.0);
|
||||
engine.stroke_to(layer_id, 440.0, 320.0, 0.8);
|
||||
engine.stroke_to(layer_id, 450.0, 300.0, 0.6);
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("meadow_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
// Basic sanity: output should contain non-white, non-transparent pixels.
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Meadow check produced no colored pixels");
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! 4K multi-layer brush-stroke performance benchmark.
|
||||
//!
|
||||
//! ## Purpose
|
||||
//! Measures the per-segment cost of painting on a large (3840x2160), multi-layer
|
||||
//! document using the public `Engine` API. This benchmark is intentionally
|
||||
//! **non-gating** — it only prints timing information so engineers can compare
|
||||
//! before/after optimizations manually. It must never break the deterministic
|
||||
//! golden visual regression suite.
|
||||
//!
|
||||
//! ## Logic & Workflow
|
||||
//! 1. Creates a 3840x2160 document.
|
||||
//! 2. Fills 10 raster layers with semi-random opaque color so that every layer
|
||||
//! contributes to the composite (worst-case workload for the painter).
|
||||
//! 3. Selects the top layer and begins a brush stroke.
|
||||
//! 4. Issues 100 `stroke_to` calls across the canvas, calling
|
||||
//! `render_composite_region` after every segment to mimic the GUI render loop.
|
||||
//! 5. Ends the stroke and prints the average time per segment, per composite,
|
||||
//! and the total wall-clock time.
|
||||
//!
|
||||
//! ## Arguments & Returns
|
||||
//! This is a standard Rust test: `cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture`.
|
||||
//! It returns nothing and asserts only that the engine remains in a valid state.
|
||||
//!
|
||||
//! ## Side Effects / Dependencies
|
||||
//! - Allocates ~400MB of pixel/tile/mask buffers.
|
||||
//! - Uses `std::time::Instant` for measurement; results depend on the host CPU.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
|
||||
const W: u32 = 3840;
|
||||
const H: u32 = 2160;
|
||||
const LAYERS: usize = 10;
|
||||
const SEGMENTS: usize = 100;
|
||||
const COMPOSITE_EVERY_N_SEGMENTS: usize = 1;
|
||||
|
||||
fn make_solid_color(r: u8, g: u8, b: u8) -> [u8; 4] {
|
||||
[r, g, b, 255]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benchmark_4k_stroke_on_multilayer_document() {
|
||||
let mut engine = Engine::new(W, H);
|
||||
|
||||
// Fill the default background layer.
|
||||
let bg_id = engine.active_layer_id();
|
||||
engine.set_active_layer(bg_id);
|
||||
engine.draw_filled_rect_rgba(0, 0, W, H, make_solid_color(240, 240, 240));
|
||||
|
||||
let mut layer_ids = vec![bg_id];
|
||||
|
||||
// Create and fill additional raster layers so every layer contributes.
|
||||
for i in 1..LAYERS {
|
||||
let id = engine.add_layer(&format!("Layer {}", i));
|
||||
engine.set_active_layer(id);
|
||||
let color = match i % 4 {
|
||||
0 => make_solid_color(180, 80, 80),
|
||||
1 => make_solid_color(80, 180, 80),
|
||||
2 => make_solid_color(80, 80, 180),
|
||||
_ => make_solid_color(160, 140, 100),
|
||||
};
|
||||
engine.draw_filled_rect_rgba(0, 0, W, H, color);
|
||||
layer_ids.push(id);
|
||||
}
|
||||
|
||||
// Switch to the top layer for painting.
|
||||
let top_id = *layer_ids.last().expect("at least one layer");
|
||||
engine.set_active_layer(top_id);
|
||||
|
||||
let mut tip = BrushTip::default();
|
||||
tip.style = BrushStyle::Round;
|
||||
tip.size = 24.0;
|
||||
tip.opacity = 1.0;
|
||||
tip.hardness = 0.85;
|
||||
tip.spacing = 0.1;
|
||||
engine.set_brush_tip(tip);
|
||||
engine.set_color([220, 60, 60, 255]);
|
||||
engine.set_eraser(false);
|
||||
|
||||
// Start the stroke roughly in the center.
|
||||
let start_x = W as f32 / 2.0;
|
||||
let start_y = H as f32 / 2.0;
|
||||
engine.begin_stroke(top_id, start_x, start_y);
|
||||
|
||||
let mut segment_times = Vec::with_capacity(SEGMENTS);
|
||||
let mut composite_times = Vec::with_capacity(SEGMENTS);
|
||||
|
||||
let total_start = std::time::Instant::now();
|
||||
for i in 0..SEGMENTS {
|
||||
let t = (i as f32) / SEGMENTS as f32;
|
||||
let angle = t * 6.28 * 2.0;
|
||||
let radius = 400.0 * t;
|
||||
let x = start_x + angle.cos() * radius;
|
||||
let y = start_y + angle.sin() * radius;
|
||||
|
||||
let seg_start = std::time::Instant::now();
|
||||
engine.stroke_to(top_id, x, y, 0.8);
|
||||
segment_times.push(seg_start.elapsed().as_micros() as f64);
|
||||
|
||||
if (i + 1) % COMPOSITE_EVERY_N_SEGMENTS == 0 {
|
||||
let comp_start = std::time::Instant::now();
|
||||
let (region, ptr, _size) = engine.render_composite_region();
|
||||
// Ensure the engine actually produced a buffer pointer; do not
|
||||
// optimize away the call.
|
||||
assert!(!ptr.is_null());
|
||||
let comp_us = comp_start.elapsed().as_micros() as f64;
|
||||
composite_times.push(comp_us);
|
||||
// Keep the region alive so the compiler doesn't discard it.
|
||||
let _ = region;
|
||||
}
|
||||
}
|
||||
engine.end_stroke(top_id);
|
||||
let total_us = total_start.elapsed().as_micros() as f64;
|
||||
|
||||
let avg_segment_us = segment_times.iter().sum::<f64>() / segment_times.len().max(1) as f64;
|
||||
let avg_composite_us = composite_times.iter().sum::<f64>() / composite_times.len().max(1) as f64;
|
||||
let max_segment_us = segment_times.iter().copied().fold(0.0, f64::max);
|
||||
let max_composite_us = composite_times.iter().copied().fold(0.0, f64::max);
|
||||
|
||||
println!("\n=== 4K Multi-Layer Stroke Benchmark ===");
|
||||
println!("Canvas: {}x{}, Layers: {}, Segments: {}", W, H, LAYERS, SEGMENTS);
|
||||
println!("Average stroke_to: {:>8.1} us", avg_segment_us);
|
||||
println!("Max stroke_to: {:>8.1} us", max_segment_us);
|
||||
println!("Average composite: {:>8.1} us", avg_composite_us);
|
||||
println!("Max composite: {:>8.1} us", max_composite_us);
|
||||
println!("Total wall time: {:>8.1} us ({:.2} s)", total_us, total_us / 1_000_000.0);
|
||||
println!("Segments per second: {:>8.1}", SEGMENTS as f64 / (total_us / 1_000_000.0));
|
||||
|
||||
// Soft sanity check: the engine must still report a valid top layer and the
|
||||
// document must be marked modified after painting.
|
||||
assert_eq!(engine.active_layer_id(), top_id);
|
||||
assert!(engine.is_modified());
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Automated visual check for the Stamp Floor (formerly Dirt) brush.
|
||||
//!
|
||||
//! Purpose: Renders several Dirt/StampFloor strokes onto a transparent canvas
|
||||
//! using the public engine API and saves the result as a PNG. Used for manual
|
||||
//! inspection and regression detection.
|
||||
|
||||
use hcie_engine_api::{Engine, BrushTip, BrushStyle};
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual visual check: run with --ignored --test stamp_floor_check and inspect target/stamp_floor_check.png"]
|
||||
fn stamp_floor_brush_visual_check() {
|
||||
let mut engine = Engine::new_with_options("StampFloorCheck", 512, 512, false);
|
||||
let layer_id = engine.active_layer_id();
|
||||
|
||||
let mut tip = BrushTip::default();
|
||||
tip.style = BrushStyle::Dirt;
|
||||
tip.size = 90.0;
|
||||
tip.opacity = 0.9;
|
||||
tip.hardness = 0.7;
|
||||
tip.spacing = 0.18;
|
||||
tip.color_variant = true;
|
||||
tip.variant_amount = 0.35;
|
||||
engine.set_brush_tip(tip);
|
||||
engine.set_color([120, 110, 100, 255]);
|
||||
|
||||
// Wide horizontal stroke to show banded floor texture.
|
||||
engine.begin_stroke(layer_id, 60.0, 250.0);
|
||||
for i in 0..=80 {
|
||||
let t = i as f32 / 80.0;
|
||||
let x = 60.0 + t * 400.0;
|
||||
let y = 250.0 + (t * 40.0).sin() * 8.0;
|
||||
engine.stroke_to(layer_id, x, y, 1.0);
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
// Second overlapping stroke.
|
||||
engine.begin_stroke(layer_id, 60.0, 320.0);
|
||||
for i in 0..=80 {
|
||||
let t = i as f32 / 80.0;
|
||||
let x = 60.0 + t * 400.0;
|
||||
let y = 320.0 + (t * 50.0).cos() * 10.0;
|
||||
engine.stroke_to(layer_id, x, y, 0.8);
|
||||
}
|
||||
engine.end_stroke(layer_id);
|
||||
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let mut output_layer = hcie_protocol::Layer::new_transparent("", 512, 512);
|
||||
output_layer.pixels = pixels.clone();
|
||||
let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("target");
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let path = out_dir.join("stamp_floor_check.png");
|
||||
hcie_engine_api::dynamic_loader::save_image(&output_layer, &path, "png").expect("save png");
|
||||
|
||||
let has_color = pixels.chunks_exact(4).any(|p| p[3] > 0 && (p[0] != 255 || p[1] != 255 || p[2] != 255));
|
||||
assert!(has_color, "Stamp Floor check produced no colored pixels");
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//! Golden Visual Regression Test Harness
|
||||
//!
|
||||
//! Purpose:
|
||||
//! Port v3's deterministic golden hash concept to v4 through the public Engine API boundary.
|
||||
//! These tests validate that Engine output remains deterministic and pixel-perfect across changes.
|
||||
//!
|
||||
//! Design:
|
||||
//! - Uses only `hcie_engine_api::Engine` public API (no internal crate access)
|
||||
//! - Creates deterministic canvases, layers, colors, vector shapes, brush strokes, filters
|
||||
//! - Hashes `engine.get_composite_pixels()` with SHA-256
|
||||
//! - Compares against inline golden hashes
|
||||
//!
|
||||
//! Regeneration:
|
||||
//! Set `HCIE_REGEN_GOLDENS=1` to print computed hashes after test verification.
|
||||
//!
|
||||
//! Golden cases:
|
||||
//! - white_canvas_empty_document: validates blank document/composite baseline
|
||||
//! - green_rect_draw_and_composite: validates draw_filled_rect_rgba
|
||||
//! - red_rect_over_green_rect: validates layer compositing order
|
||||
//! - vector_rect_golden: validates vector rendering through Engine API
|
||||
//! - brush_stroke_golden: validates brush engine + draw pipeline
|
||||
//! - invert_filter_golden: validates filter pipeline and deterministic output
|
||||
//! - undo_after_rect_golden: validates history snapshot and restoration
|
||||
|
||||
use hcie_engine_api::Engine;
|
||||
use hcie_engine_api::{BrushTip, BrushStyle, VectorShape};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Compute SHA-256 hash of pixel buffer for golden comparison.
|
||||
fn hash_pixels(pixels: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(pixels);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Test: White canvas, empty document baseline.
|
||||
/// Validates that a blank document produces deterministic all-white/transparent RGBA output.
|
||||
#[test]
|
||||
fn white_canvas_empty_document() {
|
||||
let mut engine = Engine::new(8, 8);
|
||||
let pixels = engine.get_composite_pixels();
|
||||
|
||||
// Engine::new creates a transparent document by default
|
||||
assert_eq!(pixels.len(), 8 * 8 * 4, "Canvas size mismatch");
|
||||
// Transparent pixels: alpha = 0
|
||||
for i in (0..pixels.len()).step_by(4) {
|
||||
assert_eq!(pixels[i + 3], 0, "Alpha channel should be 0 (transparent)");
|
||||
}
|
||||
assert_eq!(hash_pixels(&pixels), "5341e6b2646979a70e57653007a1f310169421ec9bdd9f1a5648f75ade005af1");
|
||||
}
|
||||
|
||||
/// Test: Green rectangle draw and composite.
|
||||
/// Validates `draw_filled_rect_rgba` produces deterministic output.
|
||||
#[test]
|
||||
fn green_rect_draw_and_composite() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
engine.draw_filled_rect_rgba(4, 4, 12, 12, [0, 255, 0, 255]);
|
||||
let pixels = engine.get_composite_pixels();
|
||||
|
||||
assert_eq!(pixels.len(), 16 * 16 * 4);
|
||||
// Check center pixel is green
|
||||
let center_idx = (8 * 16 + 8) * 4;
|
||||
assert_eq!(pixels[center_idx], 0);
|
||||
assert_eq!(pixels[center_idx + 1], 255);
|
||||
assert_eq!(pixels[center_idx + 2], 0);
|
||||
assert_eq!(pixels[center_idx + 3], 255);
|
||||
assert_eq!(hash_pixels(&pixels), "01b9681b08e6d9bafd568f110f0656613c7447c667fda4af9350d705dadc758a");
|
||||
}
|
||||
|
||||
/// Test: Red rectangle over green rectangle.
|
||||
/// Validates layer compositing order with two layers.
|
||||
#[test]
|
||||
fn red_rect_over_green_rect() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
|
||||
// Bottom layer: green rect
|
||||
let green_id = engine.add_layer("green");
|
||||
engine.set_active_layer(green_id);
|
||||
engine.draw_filled_rect_rgba(2, 2, 14, 14, [0, 255, 0, 255]);
|
||||
|
||||
// Top layer: red rect
|
||||
let red_id = engine.add_layer("red");
|
||||
engine.set_active_layer(red_id);
|
||||
engine.draw_filled_rect_rgba(6, 6, 10, 10, [255, 0, 0, 255]);
|
||||
|
||||
let pixels = engine.get_composite_pixels();
|
||||
assert_eq!(pixels.len(), 16 * 16 * 4);
|
||||
|
||||
// Center pixel should be red (top layer wins)
|
||||
let center_idx = (8 * 16 + 8) * 4;
|
||||
assert_eq!(pixels[center_idx], 255);
|
||||
assert_eq!(pixels[center_idx + 1], 0);
|
||||
assert_eq!(pixels[center_idx + 2], 0);
|
||||
assert_eq!(hash_pixels(&pixels), "c893ae44fb82ff080e286a6625389fef843ac60e82cdb4d7dde8c7975bbae750");
|
||||
}
|
||||
|
||||
/// Test: Vector shape rendering.
|
||||
/// Validates vector shapes are rendered deterministically through Engine API.
|
||||
#[test]
|
||||
fn vector_rect_golden() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
let shape = VectorShape::Rect {
|
||||
x1: 0.0, y1: 0.0, x2: 16.0, y2: 16.0,
|
||||
stroke: 1.0,
|
||||
color: [255, 0, 0, 255],
|
||||
fill_color: [255, 0, 0, 255],
|
||||
fill: true,
|
||||
radius: 0.0,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
};
|
||||
engine.add_vector_shape(shape);
|
||||
let pixels = engine.get_composite_pixels();
|
||||
|
||||
assert_eq!(pixels.len(), 16 * 16 * 4);
|
||||
|
||||
// Verify at least one red pixel (vector rect was drawn)
|
||||
let has_red = pixels.chunks(4).any(|p| p[0] == 255 && p[1] == 0 && p[2] == 0);
|
||||
assert!(has_red, "Vector rect should contain red pixels");
|
||||
assert_eq!(hash_pixels(&pixels), "71205eb7a329a3ead670c77eee185c0fbeb612f7a2b3d6aadbe2af4f9276b60d");
|
||||
}
|
||||
|
||||
/// Test: Brush stroke rendering.
|
||||
/// Validates brush engine + draw pipeline produces deterministic output.
|
||||
#[test]
|
||||
fn brush_stroke_golden() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
let brush = BrushTip {
|
||||
style: BrushStyle::Round,
|
||||
size: 4.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
spacing: 0.1,
|
||||
..Default::default()
|
||||
};
|
||||
engine.set_brush_tip(brush);
|
||||
engine.begin_stroke(engine.active_layer_id(), 8.0, 8.0);
|
||||
engine.stroke_to(engine.active_layer_id(), 12.0, 12.0, 1.0);
|
||||
engine.end_stroke(engine.active_layer_id());
|
||||
let pixels = engine.get_composite_pixels();
|
||||
|
||||
assert_eq!(pixels.len(), 16 * 16 * 4);
|
||||
|
||||
// Brush stroke should produce non-white pixels
|
||||
let has_non_white = pixels.iter().any(|&p| p < 255);
|
||||
assert!(has_non_white, "Brush stroke should produce non-white pixels");
|
||||
assert_eq!(hash_pixels(&pixels), "e97f0dd89644a40cd4d16b7440576265761849c45180fd4dece66b4f709fa087");
|
||||
}
|
||||
|
||||
/// Test: Invert filter.
|
||||
/// Validates filter pipeline produces deterministic output.
|
||||
#[test]
|
||||
fn invert_filter_golden() {
|
||||
let mut engine = Engine::new(8, 8);
|
||||
engine.draw_filled_rect_rgba(0, 0, 8, 8, [100, 150, 200, 255]);
|
||||
engine.apply_filter("invert", serde_json::json!({}));
|
||||
|
||||
let pixels = engine.get_composite_pixels();
|
||||
assert_eq!(pixels.len(), 8 * 8 * 4);
|
||||
|
||||
// Center pixel should be inverted
|
||||
let idx = (4 * 8 + 4) * 4;
|
||||
assert_eq!(pixels[idx], 155); // 255 - 100
|
||||
assert_eq!(pixels[idx + 1], 105); // 255 - 150
|
||||
assert_eq!(pixels[idx + 2], 55); // 255 - 200
|
||||
assert_eq!(hash_pixels(&pixels), "35490247d911e119aeae86afa584459b47b853262afde75f832521a738bb069f");
|
||||
}
|
||||
|
||||
/// Test: Undo after rectangle.
|
||||
/// Validates history snapshot and restoration works correctly.
|
||||
#[test]
|
||||
fn undo_after_rect_golden() {
|
||||
let mut engine = Engine::new(8, 8);
|
||||
|
||||
// Initial state: transparent
|
||||
let before = hash_pixels(&engine.get_composite_pixels());
|
||||
|
||||
// Draw green rect
|
||||
engine.draw_filled_rect_rgba(2, 2, 6, 6, [0, 255, 0, 255]);
|
||||
|
||||
// Undo
|
||||
engine.undo();
|
||||
|
||||
let after = hash_pixels(&engine.get_composite_pixels());
|
||||
|
||||
// After undo, should match initial state
|
||||
assert_eq!(before, after, "Undo should restore original canvas state");
|
||||
}
|
||||
|
||||
/// Test: Vector fill toggle and delete.
|
||||
/// Validates that set_vector_shape_fill and delete_vector_shape work correctly.
|
||||
#[test]
|
||||
fn vector_fill_toggle_and_delete() {
|
||||
let mut engine = Engine::new(16, 16);
|
||||
|
||||
// Create a rect with fill=false
|
||||
let shape = VectorShape::Rect {
|
||||
x1: 2.0, y1: 2.0, x2: 14.0, y2: 14.0,
|
||||
stroke: 1.0,
|
||||
color: [255, 0, 0, 255],
|
||||
fill_color: [0, 0, 255, 255],
|
||||
fill: false,
|
||||
radius: 0.0,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
};
|
||||
engine.add_vector_shape(shape);
|
||||
let layer_id = engine.active_layer_id();
|
||||
|
||||
// Verify fill is initially false
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(false), "fill should be false initially");
|
||||
|
||||
// Toggle fill ON
|
||||
engine.set_vector_shape_fill(layer_id, 0, true);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(true), "fill should be true after toggle");
|
||||
|
||||
// Verify composite pixels contain blue fill
|
||||
let pixels = engine.get_composite_pixels();
|
||||
let has_blue = pixels.chunks(4).any(|p| p[2] == 255 && p[0] == 0 && p[1] == 0 && p[3] > 0);
|
||||
assert!(has_blue, "should have blue fill pixels after enabling fill");
|
||||
|
||||
// Toggle fill OFF
|
||||
engine.set_vector_shape_fill(layer_id, 0, false);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert_eq!(shapes[0].fill(), Some(false), "fill should be false after toggling off");
|
||||
|
||||
// Delete the shape
|
||||
engine.delete_vector_shape(layer_id, 0);
|
||||
let shapes = engine.active_vector_shapes().unwrap();
|
||||
assert!(shapes.is_empty(), "shapes should be empty after delete");
|
||||
}
|
||||
Reference in New Issue
Block a user