diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-28 20:14:51 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-28 20:14:51 +0200 |
| commit | 32438ce25f69de99d2523b98237f62cfa90890d7 (patch) | |
| tree | 3a712c347dec0889bdea3328b4c817c2d9392b27 /rust/crates/mask-defs/tests/render.rs | |
| parent | f5a82152394fde4d8f740f664ab7737b82861618 (diff) | |
| download | opencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz | |
feat: port domain crates to Rust (session 1 complete)
All green: cargo test --workspace (~300 tests, 0 failures).
- timeline (45): placement, resize/ripple, split, snap, retime,
audio separation, scene helpers
- animation (73): bezier solver, channel sampling, keyframe ops,
split/clamp/clone, param definitions
- commands (9): Command trait, BatchCommand, history, 19 concrete
commands, freeze-frame port 1:1
- audio (22): audible collection, range mixing, mastering limiter,
rms/peak buckets
- text (19): cosmic-text layout/measure/raster, backgrounds,
decorations
- graphics (52): full CSS gradient parser, tiny-skia raster,
4 shape definitions
- effect-defs (16): blur definition, registry, pass resolution
- mask-defs (19): all 9 mask renderers via tiny-skia
- renderer (6): node model + scene builder port
- desktop: GPUI projects screen wired to ProjectStore
- scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps)
Diffstat (limited to 'rust/crates/mask-defs/tests/render.rs')
| -rw-r--r-- | rust/crates/mask-defs/tests/render.rs | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/rust/crates/mask-defs/tests/render.rs b/rust/crates/mask-defs/tests/render.rs new file mode 100644 index 00000000..ca8e843a --- /dev/null +++ b/rust/crates/mask-defs/tests/render.rs @@ -0,0 +1,372 @@ +use mask_defs::{feather_for_gpu, is_active, render_mask_body, render_mask_stroke}; +use scene::{ + FreeformPathMaskParams, FreeformPathPoint, Mask, RectangleMaskParams, SplitMaskParams, + StrokeAlign, TextDecoration, TextFontStyle, TextFontWeight, TextMaskParams, +}; +use tiny_skia::Pixmap; + +fn box_params() -> RectangleMaskParams { + RectangleMaskParams { + feather: 0.0, + inverted: false, + stroke_color: "#ffffff".to_string(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + center_x: 0.0, + center_y: 0.0, + width: 0.6, + height: 0.6, + rotation: 0.0, + scale: 1.0, + } +} + +fn split_params() -> SplitMaskParams { + SplitMaskParams { + feather: 0.0, + inverted: false, + stroke_color: "#ffffff".to_string(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + center_x: 0.0, + center_y: 0.0, + rotation: 0.0, + } +} + +fn text_params(content: &str) -> TextMaskParams { + TextMaskParams { + feather: 0.0, + inverted: false, + stroke_color: "#ffffff".to_string(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + content: content.to_string(), + font_size: 15.0, + font_family: "Arial".to_string(), + font_weight: TextFontWeight::Normal, + font_style: TextFontStyle::Normal, + text_decoration: TextDecoration::None, + letter_spacing: 0.0, + line_height: 1.2, + center_x: 0.0, + center_y: 0.0, + rotation: 0.0, + scale: 1.0, + } +} + +fn freeform_params(points: Vec<FreeformPathPoint>, closed: bool) -> FreeformPathMaskParams { + FreeformPathMaskParams { + feather: 0.0, + inverted: false, + stroke_color: "#ffffff".to_string(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + path: points, + closed, + center_x: 0.0, + center_y: 0.0, + rotation: 0.0, + scale: 1.0, + } +} + +fn point(id: &str, x: f64, y: f64) -> FreeformPathPoint { + FreeformPathPoint { + id: id.to_string(), + x, + y, + in_x: 0.0, + in_y: 0.0, + out_x: 0.0, + out_y: 0.0, + } +} + +fn alpha_at(pixmap: &Pixmap, x: u32, y: u32) -> u8 { + pixmap.pixel(x, y).map(|p| p.alpha()).unwrap_or(0) +} + +fn assert_opaque(pixmap: &Pixmap, x: u32, y: u32) { + assert!( + alpha_at(pixmap, x, y) > 250, + "expected opaque at ({x}, {y}), got {}", + alpha_at(pixmap, x, y) + ); +} + +fn assert_transparent(pixmap: &Pixmap, x: u32, y: u32) { + assert!( + alpha_at(pixmap, x, y) < 5, + "expected transparent at ({x}, {y}), got {}", + alpha_at(pixmap, x, y) + ); +} + +fn box_mask(kind: &str, params: RectangleMaskParams) -> Mask { + match kind { + "cinematic-bars" => Mask::CinematicBars { id: "m".into(), params }, + "rectangle" => Mask::Rectangle { id: "m".into(), params }, + "ellipse" => Mask::Ellipse { id: "m".into(), params }, + "heart" => Mask::Heart { id: "m".into(), params }, + "diamond" => Mask::Diamond { id: "m".into(), params }, + _ => Mask::Star { id: "m".into(), params }, + } +} + +#[test] +fn rectangle_body_center_opaque_corners_transparent() { + let mask = box_mask("rectangle", box_params()); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 100); + assert_opaque(&pixmap, 50, 50); + assert_transparent(&pixmap, 20, 20); + assert_transparent(&pixmap, 180, 180); +} + +#[test] +fn rectangle_body_rotated_90_swaps_extents() { + let mut params = box_params(); + params.width = 0.6; + params.height = 0.3; + params.rotation = 90.0; + let mask = box_mask("rectangle", params); + let pixmap = render_mask_body(&mask, 200, 100).unwrap(); + assert_opaque(&pixmap, 100, 50); + assert_opaque(&pixmap, 100, 10); + assert_transparent(&pixmap, 150, 50); +} + +#[test] +fn rectangle_stroke_ring_present_when_stroke_width_positive() { + let mut params = box_params(); + params.stroke_width = 10.0; + let mask = box_mask("rectangle", params); + let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap(); + assert_opaque(&stroke, 100, 40); + assert_transparent(&stroke, 100, 100); + assert_transparent(&stroke, 10, 10); +} + +#[test] +fn stroke_is_none_when_stroke_width_zero() { + let mask = box_mask("rectangle", box_params()); + assert!(render_mask_stroke(&mask, 200, 200).unwrap().is_none()); +} + +#[test] +fn ellipse_body_center_opaque_corners_transparent() { + let mask = box_mask("ellipse", box_params()); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 100); + assert_opaque(&pixmap, 130, 100); + assert_transparent(&pixmap, 45, 45); + assert_transparent(&pixmap, 10, 10); +} + +#[test] +fn heart_body_region_coverage() { + let mask = box_mask("heart", box_params()); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 110); + assert_opaque(&pixmap, 75, 80); + assert_transparent(&pixmap, 100, 60); + assert_transparent(&pixmap, 10, 10); +} + +#[test] +fn diamond_body_region_coverage() { + let mask = box_mask("diamond", box_params()); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 100); + assert_opaque(&pixmap, 130, 100); + assert_transparent(&pixmap, 145, 145); + assert_transparent(&pixmap, 10, 10); +} + +#[test] +fn star_body_region_coverage() { + let mask = box_mask("star", box_params()); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 100); + assert_opaque(&pixmap, 100, 52); + assert_transparent(&pixmap, 121, 71); + assert_transparent(&pixmap, 10, 10); +} + +#[test] +fn cinematic_bars_body_full_width_band() { + let mut params = box_params(); + params.width = 1.5; + params.height = 0.6; + let mask = box_mask("cinematic-bars", params); + let pixmap = render_mask_body(&mask, 200, 100).unwrap(); + assert_opaque(&pixmap, 100, 50); + assert_opaque(&pixmap, 5, 50); + assert_transparent(&pixmap, 100, 10); + assert_transparent(&pixmap, 100, 95); +} + +#[test] +fn split_body_zero_feather_hard_edge() { + let mask = Mask::Split { id: "m".into(), params: split_params() }; + let pixmap = render_mask_body(&mask, 200, 100).unwrap(); + assert_opaque(&pixmap, 150, 50); + assert_opaque(&pixmap, 101, 50); + assert_transparent(&pixmap, 99, 50); + assert_transparent(&pixmap, 50, 50); +} + +#[test] +fn split_body_feather_gradient_band() { + let mut params = split_params(); + params.feather = 40.0; + let mask = Mask::Split { id: "m".into(), params }; + let pixmap = render_mask_body(&mask, 200, 100).unwrap(); + assert_opaque(&pixmap, 150, 50); + assert_transparent(&pixmap, 50, 50); + let mid = alpha_at(&pixmap, 100, 50); + assert!( + (108..=148).contains(&mid), + "expected mid alpha ~128 at split line, got {mid}" + ); + let quarter = alpha_at(&pixmap, 110, 50) as u16; + assert!( + (170..=215).contains(&quarter), + "expected alpha ~191 at feather quarter, got {quarter}" + ); +} + +#[test] +fn split_stroke_segment_vertical_and_horizontal() { + let mut params = split_params(); + params.stroke_width = 4.0; + let mask = Mask::Split { id: "m".into(), params }; + let stroke = render_mask_stroke(&mask, 200, 100).unwrap().unwrap(); + assert_opaque(&stroke, 100, 50); + assert_transparent(&stroke, 106, 50); + + let mut rotated = split_params(); + rotated.rotation = 90.0; + rotated.stroke_width = 4.0; + let mask = Mask::Split { id: "m".into(), params: rotated }; + let stroke = render_mask_stroke(&mask, 200, 100).unwrap().unwrap(); + assert_opaque(&stroke, 50, 50); + assert_transparent(&stroke, 50, 56); +} + +#[test] +fn freeform_closed_triangle_region_coverage() { + let points = vec![ + point("a", -0.4, -0.4), + point("b", 0.4, -0.4), + point("c", 0.0, 0.4), + ]; + let mask = Mask::Freeform { id: "m".into(), params: freeform_params(points, true) }; + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + assert_opaque(&pixmap, 100, 110); + assert_transparent(&pixmap, 10, 10); + assert_transparent(&pixmap, 100, 180); +} + +#[test] +fn freeform_open_path_is_inactive_and_empty() { + let points = vec![ + point("a", -0.4, -0.4), + point("b", 0.4, -0.4), + point("c", 0.0, 0.4), + ]; + let mask = Mask::Freeform { id: "m".into(), params: freeform_params(points, false) }; + assert!(!is_active(&mask)); + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + for pixel in pixmap.pixels() { + assert_eq!(pixel.alpha(), 0); + } + let mut stroke_params = freeform_params( + vec![point("a", -0.4, -0.4), point("b", 0.4, -0.4), point("c", 0.0, 0.4)], + false, + ); + stroke_params.stroke_width = 5.0; + let mask = Mask::Freeform { id: "m".into(), params: stroke_params }; + let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap(); + for pixel in stroke.pixels() { + assert_eq!(pixel.alpha(), 0); + } +} + +#[test] +fn text_body_rasterizes_glyphs() { + let mask = Mask::Text { id: "m".into(), params: text_params("Hi") }; + let pixmap = render_mask_body(&mask, 200, 200).unwrap(); + let opaque_count = pixmap.pixels().iter().filter(|p| p.alpha() > 200).count(); + assert!(opaque_count > 20, "expected rasterized glyphs, got {opaque_count} opaque pixels"); + assert_transparent(&pixmap, 2, 2); + assert_transparent(&pixmap, 197, 197); +} + +#[test] +fn text_stroke_rasterizes_ring() { + let mut params = text_params("Hi"); + params.stroke_width = 3.0; + let mask = Mask::Text { id: "m".into(), params }; + let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap(); + let opaque_count = stroke.pixels().iter().filter(|p| p.alpha() > 200).count(); + assert!(opaque_count > 20, "expected stroked glyphs, got {opaque_count} opaque pixels"); +} + +#[test] +fn is_active_rules_match_web() { + let text = Mask::Text { id: "m".into(), params: text_params(" ") }; + assert!(!is_active(&text)); + let text = Mask::Text { id: "m".into(), params: text_params("Mask") }; + assert!(is_active(&text)); + assert!(is_active(&box_mask("rectangle", box_params()))); + assert!(is_active(&Mask::Split { id: "m".into(), params: split_params() })); +} + +#[test] +fn feather_for_gpu_zeroes_draw_with_feather_bodies() { + let mut split = split_params(); + split.feather = 40.0; + let mask = Mask::Split { id: "m".into(), params: split }; + assert_eq!(feather_for_gpu(&mask), 0.0); + + let mut rect = box_params(); + rect.feather = 40.0; + let mask = box_mask("rectangle", rect); + assert_eq!(feather_for_gpu(&mask), 40.0); + + let mut text = text_params("Hi"); + text.feather = 25.0; + let mask = Mask::Text { id: "m".into(), params: text }; + assert_eq!(feather_for_gpu(&mask), 25.0); +} + +#[test] +fn stroke_align_outside_keeps_outer_ring() { + let mut params = box_params(); + params.stroke_width = 10.0; + params.stroke_align = StrokeAlign::Outside; + let mask = Mask::Freeform { + id: "m".into(), + params: FreeformPathMaskParams { + stroke_width: 10.0, + stroke_align: StrokeAlign::Outside, + ..freeform_params( + vec![ + point("a", -0.3, -0.3), + point("b", 0.3, -0.3), + point("c", 0.3, 0.3), + point("d", -0.3, 0.3), + ], + true, + ) + }, + }; + let _ = params; + let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap(); + assert_opaque(&stroke, 100, 35); + assert_transparent(&stroke, 100, 100); + assert_transparent(&stroke, 100, 43); +} |
