tiny-skia 0.8.2

A tiny Skia subset ported to Rust.
Documentation
// Copyright 2011 Google Inc.
// Copyright 2020 Yevhenii Reizner
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use crate::{Point, Rect};

use tiny_skia_path::Scalar;

pub const MAX_POINTS: usize = 4;

/// Clip the line pts[0]...pts[1] against clip, ignoring segments that
/// lie completely above or below the clip. For portions to the left or
/// right, turn those into vertical line segments that are aligned to the
/// edge of the clip.
///
/// Return the number of line segments that result, and store the end-points
/// of those segments sequentially in lines as follows:
///
/// 1st segment: lines[0]..lines[1]
/// 2nd segment: lines[1]..lines[2]
/// 3rd segment: lines[2]..lines[3]
pub fn clip<'a>(
    src: &[Point; 2],
    clip: &Rect,
    can_cull_to_the_right: bool,
    points: &'a mut [Point; MAX_POINTS],
) -> &'a [Point] {
    let (mut index0, mut index1) = if src[0].y < src[1].y { (0, 1) } else { (1, 0) };

    // Check if we're completely clipped out in Y (above or below)

    if src[index1].y <= clip.top() {
        // we're above the clip
        return &[];
    }

    if src[index0].y >= clip.bottom() {
        // we're below the clip
        return &[];
    }

    // Chop in Y to produce a single segment, stored in tmp[0..1]

    let mut tmp = *src;

    // now compute intersections
    if src[index0].y < clip.top() {
        tmp[index0] = Point::from_xy(sect_with_horizontal(src, clip.top()), clip.top());
        debug_assert!(is_between_unsorted(tmp[index0].x, src[0].x, src[1].x));
    }

    if tmp[index1].y > clip.bottom() {
        tmp[index1] = Point::from_xy(sect_with_horizontal(src, clip.bottom()), clip.bottom());
        debug_assert!(is_between_unsorted(tmp[index1].x, src[0].x, src[1].x));
    }

    // Chop it into 1..3 segments that are wholly within the clip in X.

    // temp storage for up to 3 segments
    let mut result_storage = [Point::zero(); MAX_POINTS];
    let mut line_count = 1;
    let mut reverse;

    if src[0].x < src[1].x {
        index0 = 0;
        index1 = 1;
        reverse = false;
    } else {
        index0 = 1;
        index1 = 0;
        reverse = true;
    }

    let result: &[Point] = if tmp[index1].x <= clip.left() {
        // wholly to the left
        tmp[0].x = clip.left();
        tmp[1].x = clip.left();
        reverse = false;
        &tmp
    } else if tmp[index0].x >= clip.right() {
        // wholly to the right
        if can_cull_to_the_right {
            return &[];
        }

        tmp[0].x = clip.right();
        tmp[1].x = clip.right();
        reverse = false;
        &tmp
    } else {
        let mut offset = 0;

        if tmp[index0].x < clip.left() {
            result_storage[offset] = Point::from_xy(clip.left(), tmp[index0].y);
            offset += 1;
            result_storage[offset] =
                Point::from_xy(clip.left(), sect_clamp_with_vertical(&tmp, clip.left()));
            debug_assert!(is_between_unsorted(
                result_storage[offset].y,
                tmp[0].y,
                tmp[1].y
            ));
        } else {
            result_storage[offset] = tmp[index0];
        }
        offset += 1;

        if tmp[index1].x > clip.right() {
            result_storage[offset] =
                Point::from_xy(clip.right(), sect_clamp_with_vertical(&tmp, clip.right()));
            debug_assert!(is_between_unsorted(
                result_storage[offset].y,
                tmp[0].y,
                tmp[1].y
            ));
            offset += 1;
            result_storage[offset] = Point::from_xy(clip.right(), tmp[index1].y);
        } else {
            result_storage[offset] = tmp[index1];
        }

        line_count = offset;
        &result_storage
    };

    // Now copy the results into the caller's lines[] parameter
    if reverse {
        // copy the pts in reverse order to maintain winding order
        for i in 0..=line_count {
            points[line_count - i] = result[i];
        }
    } else {
        let len = line_count + 1;
        points[0..len].copy_from_slice(&result[0..len]);
    }

    &points[0..line_count + 1]
}

/// Returns X coordinate of intersection with horizontal line at Y.
fn sect_with_horizontal(src: &[Point; 2], y: f32) -> f32 {
    let dy = src[1].y - src[0].y;
    if dy.is_nearly_zero() {
        src[0].x.ave(src[1].x)
    } else {
        // need the extra precision so we don't compute a value that exceeds
        // our original limits
        let x0 = f64::from(src[0].x);
        let y0 = f64::from(src[0].y);
        let x1 = f64::from(src[1].x);
        let y1 = f64::from(src[1].y);
        let result = x0 + (f64::from(y) - y0) * (x1 - x0) / (y1 - y0);

        // The computed X value might still exceed [X0..X1] due to quantum flux
        // when the doubles were added and subtracted, so we have to pin the
        // answer :(
        pin_unsorted_f64(result, x0, x1) as f32
    }
}

/// Returns value between the two limits, where the limits are either ascending or descending.
fn is_between_unsorted(value: f32, limit0: f32, limit1: f32) -> bool {
    if limit0 < limit1 {
        limit0 <= value && value <= limit1
    } else {
        limit1 <= value && value <= limit0
    }
}

fn sect_clamp_with_vertical(src: &[Point; 2], x: f32) -> f32 {
    let y = sect_with_vertical(src, x);
    // Our caller expects y to be between src[0].y and src[1].y (unsorted), but due to the
    // numerics of floats/doubles, we might have computed a value slightly outside of that,
    // so we have to manually clamp afterwards.
    // See skbug.com/7491
    pin_unsorted_f32(y, src[0].y, src[1].y)
}

/// Returns Y coordinate of intersection with vertical line at X.
fn sect_with_vertical(src: &[Point; 2], x: f32) -> f32 {
    let dx = src[1].x - src[0].x;
    if dx.is_nearly_zero() {
        src[0].y.ave(src[1].y)
    } else {
        // need the extra precision so we don't compute a value that exceeds
        // our original limits
        let x0 = f64::from(src[0].x);
        let y0 = f64::from(src[0].y);
        let x1 = f64::from(src[1].x);
        let y1 = f64::from(src[1].y);
        let result = y0 + (f64::from(x) - x0) * (y1 - y0) / (x1 - x0);
        result as f32
    }
}

fn pin_unsorted_f32(value: f32, mut limit0: f32, mut limit1: f32) -> f32 {
    if limit1 < limit0 {
        core::mem::swap(&mut limit0, &mut limit1);
    }
    // now the limits are sorted
    debug_assert!(limit0 <= limit1);

    if value < limit0 {
        limit0
    } else if value > limit1 {
        limit1
    } else {
        value
    }
}

fn pin_unsorted_f64(value: f64, mut limit0: f64, mut limit1: f64) -> f64 {
    if limit1 < limit0 {
        core::mem::swap(&mut limit0, &mut limit1);
    }
    // now the limits are sorted
    debug_assert!(limit0 <= limit1);

    if value < limit0 {
        limit0
    } else if value > limit1 {
        limit1
    } else {
        value
    }
}

/// Intersect the line segment against the rect. If there is a non-empty
/// resulting segment, return true and set dst[] to that segment. If not,
/// return false and ignore dst[].
///
/// `clip` is specialized for scan-conversion, as it adds vertical
/// segments on the sides to show where the line extended beyond the
/// left or right sides. `intersect` does not.
pub fn intersect(src: &[Point; 2], clip: &Rect, dst: &mut [Point; 2]) -> bool {
    let bounds = Rect::from_ltrb(
        src[0].x.min(src[1].x),
        src[0].y.min(src[1].y),
        src[0].x.max(src[1].x),
        src[0].y.max(src[1].y),
    );

    if let Some(bounds) = bounds {
        if contains_no_empty_check(clip, &bounds) {
            dst.copy_from_slice(src);
            return true;
        }

        // check for no overlap, and only permit coincident edges if the line
        // and the edge are colinear
        if nested_lt(bounds.right(), clip.left(), bounds.width())
            || nested_lt(clip.right(), bounds.left(), bounds.width())
            || nested_lt(bounds.bottom(), clip.top(), bounds.height())
            || nested_lt(clip.bottom(), bounds.top(), bounds.height())
        {
            return false;
        }
    }

    let (index0, index1) = if src[0].y < src[1].y { (0, 1) } else { (1, 0) };

    let mut tmp = src.clone();

    // now compute Y intersections
    if tmp[index0].y < clip.top() {
        tmp[index0] = Point::from_xy(sect_with_horizontal(src, clip.top()), clip.top());
    }

    if tmp[index1].y > clip.bottom() {
        tmp[index1] = Point::from_xy(sect_with_horizontal(src, clip.bottom()), clip.bottom());
    }

    let (index0, index1) = if tmp[0].x < tmp[1].x { (0, 1) } else { (1, 0) };

    // check for quick-reject in X again, now that we may have been chopped
    if tmp[index1].x <= clip.left() || tmp[index0].x >= clip.right() {
        // usually we will return false, but we don't if the line is vertical and coincident
        // with the clip.
        if tmp[0].x != tmp[1].x || tmp[0].x < clip.left() || tmp[0].x > clip.right() {
            return false;
        }
    }

    if tmp[index0].x < clip.left() {
        tmp[index0] = Point::from_xy(clip.left(), sect_with_vertical(src, clip.left()));
    }

    if tmp[index1].x > clip.right() {
        tmp[index1] = Point::from_xy(clip.right(), sect_with_vertical(src, clip.right()));
    }

    dst.copy_from_slice(&tmp);
    true
}

fn nested_lt(a: f32, b: f32, dim: f32) -> bool {
    a <= b && (a < b || dim > 0.0)
}

// returns true if outer contains inner, even if inner is empty.
fn contains_no_empty_check(outer: &Rect, inner: &Rect) -> bool {
    outer.left() <= inner.left()
        && outer.top() <= inner.top()
        && outer.right() >= inner.right()
        && outer.bottom() >= inner.bottom()
}