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/src/split.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/src/split.rs')
| -rw-r--r-- | rust/crates/mask-defs/src/split.rs | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/rust/crates/mask-defs/src/split.rs b/rust/crates/mask-defs/src/split.rs new file mode 100644 index 00000000..d2b67f78 --- /dev/null +++ b/rust/crates/mask-defs/src/split.rs @@ -0,0 +1,135 @@ +use scene::SplitMaskParams; +use tiny_skia::{ + Color, GradientStop, LinearGradient, Paint, Path, PathBuilder, Pixmap, Point, SpreadMode, + Transform, +}; + +use crate::geometry::{ + INTERSECTION_EPSILON, MIN_POLYGON_AREA_PX, SplitLine, half_plane_sign, line_edge_intersection, + polygon_area, split_line_geometry, +}; + +fn canvas_edges(width: f64, height: f64) -> [(f64, f64, f64, f64); 4] { + [ + (0.0, 0.0, width, 0.0), + (width, 0.0, width, height), + (width, height, 0.0, height), + (0.0, height, 0.0, 0.0), + ] +} + +fn points_equal(a: (f64, f64), b: (f64, f64)) -> bool { + (a.0 - b.0).abs() <= INTERSECTION_EPSILON && (a.1 - b.1).abs() <= INTERSECTION_EPSILON +} + +fn split_line(params: &SplitMaskParams, width: f64, height: f64) -> SplitLine { + split_line_geometry( + params.center_x, + params.center_y, + params.rotation, + width, + height, + ) +} + +pub fn split_stroke_segment( + params: &SplitMaskParams, + width: f64, + height: f64, +) -> Option<[(f64, f64); 2]> { + let line = split_line(params, width, height); + let mut intersections: Vec<(f64, f64)> = Vec::new(); + for &(x1, y1, x2, y2) in &canvas_edges(width, height) { + let Some(hit) = line_edge_intersection(line, x1, y1, x2, y2) else { + continue; + }; + if intersections.iter().any(|&point| points_equal(point, hit)) { + continue; + } + intersections.push(hit); + } + if intersections.len() != 2 { + return None; + } + Some([intersections[0], intersections[1]]) +} + +pub fn split_stroke_path(params: &SplitMaskParams, width: f64, height: f64) -> Option<Path> { + let segment = split_stroke_segment(params, width, height)?; + let mut builder = PathBuilder::new(); + builder.move_to(segment[0].0 as f32, segment[0].1 as f32); + builder.line_to(segment[1].0 as f32, segment[1].1 as f32); + builder.finish() +} + +pub fn split_opaque_body_path(params: &SplitMaskParams, width: f64, height: f64) -> Option<Path> { + let line = split_line(params, width, height); + let is_inside = |x: f64, y: f64| half_plane_sign(line, x, y) >= 0.0; + + let mut vertices: Vec<(f64, f64)> = Vec::new(); + for &(x1, y1, x2, y2) in &canvas_edges(width, height) { + let vertex1_inside = is_inside(x1, y1); + let vertex2_inside = is_inside(x2, y2); + if vertex1_inside && vertex2_inside { + vertices.push((x2, y2)); + } else if vertex1_inside { + if let Some(hit) = line_edge_intersection(line, x1, y1, x2, y2) { + vertices.push(hit); + } + } else if vertex2_inside { + if let Some(hit) = line_edge_intersection(line, x1, y1, x2, y2) { + vertices.push(hit); + vertices.push((x2, y2)); + } + } + } + + if vertices.len() < 3 || polygon_area(&vertices) < MIN_POLYGON_AREA_PX { + return None; + } + + let mut builder = PathBuilder::new(); + builder.move_to(vertices[0].0 as f32, vertices[0].1 as f32); + for &(x, y) in &vertices[1..] { + builder.line_to(x as f32, y as f32); + } + builder.close(); + builder.finish() +} + +pub fn fill_split_feather_body( + params: &SplitMaskParams, + width: f64, + height: f64, + pixmap: &mut Pixmap, +) { + let line = split_line(params, width, height); + let feather_half = params.feather / 2.0; + let Some(shader) = LinearGradient::new( + Point::from_xy( + (line.line_x - line.normal_x * feather_half) as f32, + (line.line_y - line.normal_y * feather_half) as f32, + ), + Point::from_xy( + (line.line_x + line.normal_x * feather_half) as f32, + (line.line_y + line.normal_y * feather_half) as f32, + ), + vec![ + GradientStop::new(0.0, Color::from_rgba8(255, 255, 255, 0)), + GradientStop::new(1.0, Color::from_rgba8(255, 255, 255, 255)), + ], + SpreadMode::Pad, + Transform::default(), + ) else { + return; + }; + let paint = Paint { + shader, + anti_alias: true, + ..Paint::default() + }; + let Some(rect) = tiny_skia::Rect::from_xywh(0.0, 0.0, width as f32, height as f32) else { + return; + }; + pixmap.fill_rect(rect, &paint, Transform::default(), None); +} |
