sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::{NativeShape, shape::NativePathCommand};

use super::native::{NativeRect, TemplatePath, clamp_horizontal};
use super::{ShapeTextFrameIssue, TextInsets};

pub(super) fn insets(
    shape: &NativeShape,
    rect: &NativeRect,
) -> Result<TextInsets, ShapeTextFrameIssue> {
    let path = TemplatePath::read(shape, rect)?;
    let [first, second, _] = vertices(path.commands())?;
    match shape.shape_type {
        2 => triangle(
            rect,
            first,
            shape.rotation_degrees,
            shape.vertical_flip,
            &shape.control_points,
        ),
        3 => right_triangle(
            rect,
            [first, second],
            shape.rotation_degrees,
            shape.horizontal_flip,
        ),
        _ => Err(ShapeTextFrameIssue::UnsupportedTemplate),
    }
}

fn vertices(commands: &[NativePathCommand<f32>]) -> Result<[[f32; 2]; 3], ShapeTextFrameIssue> {
    match commands {
        [
            NativePathCommand::Move(first),
            NativePathCommand::Line(second),
            NativePathCommand::Line(third),
            NativePathCommand::Close,
        ] => Ok([*first, *second, *third]),
        _ => Err(ShapeTextFrameIssue::UnsupportedTemplate),
    }
}

fn triangle(
    rect: &NativeRect,
    first: [f32; 2],
    angle: f32,
    vertical_flip: bool,
    controls: &[[f64; 2]],
) -> Result<TextInsets, ShapeTextFrameIssue> {
    let mut ratio = (first[0] - rect.left) / rect.width;
    let edge_y = if vertical_flip { rect.bottom } else { rect.top };
    let control_length = rect.width.mul_add(rect.width, 0.0).sqrt();
    for point in controls {
        let point = rect.restore_point(*point, angle)?;
        if control_length == 0.0 {
            continue;
        }
        let point = clamp_horizontal(point, [rect.left, edge_y], [rect.right, edge_y])?;
        ratio = (point[0] - rect.left) / rect.width;
    }
    let half_width = rect.width * 0.5;
    let half_height = rect.height * 0.5;
    let insets = TextInsets {
        left: half_width * ratio,
        top: if vertical_flip { 0.0 } else { half_height },
        right: half_width * (1.0 - ratio),
        bottom: if vertical_flip { half_height } else { 0.0 },
    };
    checked(insets)
}

fn right_triangle(
    rect: &NativeRect,
    mut points: [[f32; 2]; 2],
    angle: f32,
    horizontal_flip: bool,
) -> Result<TextInsets, ShapeTextFrameIssue> {
    let direction = vertex_direction(points)?;
    if (direction == 0.0 && !horizontal_flip) || (direction == 180.0 && horizontal_flip) {
        for point in &mut points {
            *point = rect.rotate_point(*point, -angle)?;
        }
    }
    let (left, right) = if points[0][0] < points[1][0] {
        (35.0, 165.0)
    } else {
        (165.0, 35.0)
    };
    let (top, bottom) = if points[0][1] < points[1][1] {
        (235.0, 35.0)
    } else {
        (35.0, 235.0)
    };
    checked(TextInsets {
        left: (rect.width * left) / 400.0,
        top: (rect.height * top) / 400.0,
        right: (rect.width * right) / 400.0,
        bottom: (rect.height * bottom) / 400.0,
    })
}

fn vertex_direction(points: [[f32; 2]; 2]) -> Result<f32, ShapeTextFrameIssue> {
    let delta_x = points[1][0] - points[0][0];
    let delta_y = points[1][1] - points[0][1];
    let radians = if delta_y == 0.0 && !delta_x.is_nan() {
        if delta_x.is_sign_negative() {
            std::f32::consts::PI.copysign(delta_y)
        } else {
            delta_y
        }
    } else {
        delta_y.atan2(delta_x)
    };
    let direction = (f64::from(radians * 180.0) / std::f64::consts::PI) as f32;
    let direction = if direction < 0.0 {
        direction + 360.0
    } else {
        direction
    };
    direction
        .is_finite()
        .then_some(direction)
        .ok_or(ShapeTextFrameIssue::InvalidGeometry)
}

fn checked(insets: TextInsets) -> Result<TextInsets, ShapeTextFrameIssue> {
    if [insets.left, insets.top, insets.right, insets.bottom]
        .into_iter()
        .all(f32::is_finite)
    {
        Ok(insets)
    } else {
        Err(ShapeTextFrameIssue::InvalidGeometry)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rect() -> NativeRect {
        NativeRect {
            left: 0.0,
            top: 0.0,
            right: 400.0,
            bottom: 200.0,
            width: 400.0,
            height: 200.0,
            center: [200.0, 100.0],
        }
    }

    fn values(insets: TextInsets) -> [f32; 4] {
        [insets.left, insets.top, insets.right, insets.bottom]
    }

    #[test]
    fn triangle_retains_path_ratio_and_vertical_flip() {
        for (flip, expected) in [
            (false, [50.0, 100.0, 150.0, 0.0]),
            (true, [50.0, 0.0, 150.0, 100.0]),
        ] {
            assert_eq!(
                values(triangle(&rect(), [100.0, 0.0], 0.0, flip, &[]).unwrap()),
                expected
            );
        }
        assert_eq!(
            values(triangle(&rect(), [-100.0, 0.0], 0.0, false, &[]).unwrap()),
            [-50.0, 100.0, 250.0, 0.0],
        );
    }

    #[test]
    fn triangle_restores_controls_and_last_control_overrides_path() {
        let controls = [[300.0, 200.0], [300.0, 0.0]];
        assert_eq!(
            values(triangle(&rect(), [200.0, 0.0], 90.0, false, &controls).unwrap()),
            [50.0, 100.0, 150.0, 0.0],
        );
        for (point, expected) in [
            ([-100.0, 50.0], [0.0, 100.0, 200.0, 0.0]),
            ([500.0, 50.0], [200.0, 100.0, 0.0, 0.0]),
        ] {
            assert_eq!(
                values(triangle(&rect(), [200.0, 0.0], 0.0, false, &[point]).unwrap()),
                expected,
            );
        }
    }

    #[test]
    fn right_triangle_uses_vertex_order_and_separate_scaled_divisions() {
        for (points, expected) in [
            ([[0.0, 0.0], [400.0, 200.0]], [35.0, 117.5, 165.0, 17.5]),
            ([[400.0, 200.0], [0.0, 0.0]], [165.0, 17.5, 35.0, 117.5]),
        ] {
            assert_eq!(
                values(right_triangle(&rect(), points, 0.0, false).unwrap()),
                expected
            );
        }
        assert_eq!(
            values(
                right_triangle(&rect(), [[-f32::MAX, 0.0], [f32::MAX, 200.0]], 0.0, false).unwrap()
            ),
            [35.0, 117.5, 165.0, 17.5],
        );
    }

    #[test]
    fn right_triangle_horizontal_reload_uses_saved_flip_and_angle() {
        let points = [[0.0, 0.0], [400.0, 0.0]];
        assert_eq!(
            values(right_triangle(&rect(), points, 90.0, false).unwrap()),
            [165.0, 17.5, 35.0, 117.5],
        );
        assert_eq!(
            values(right_triangle(&rect(), points, 90.0, true).unwrap()),
            [35.0, 17.5, 165.0, 117.5],
        );
        let reversed = [[400.0, 0.0], [0.0, 0.0]];
        assert_eq!(
            values(right_triangle(&rect(), reversed, 90.0, true).unwrap()),
            [165.0, 117.5, 35.0, 17.5],
        );
        assert_eq!(
            values(right_triangle(&rect(), reversed, 90.0, false).unwrap()),
            [165.0, 17.5, 35.0, 117.5],
        );
    }

    #[test]
    fn horizontal_vertex_directions_are_exact_for_both_zero_signs() {
        for delta_y in [0.0, -0.0] {
            for delta_x in [400.0, f32::INFINITY, -400.0, f32::NEG_INFINITY] {
                let expected = if delta_x.is_sign_negative() {
                    180.0
                } else {
                    0.0
                };
                assert_eq!(
                    vertex_direction([[0.0, 0.0], [delta_x, delta_y]]),
                    Ok(expected)
                );
            }
        }
        assert_eq!(
            vertex_direction([[0.0, 0.0], [f32::NAN, 0.0]]),
            Err(ShapeTextFrameIssue::InvalidGeometry),
        );
    }

    #[test]
    fn nearby_nonhorizontal_vertices_do_not_reload_as_horizontal() {
        let points = [[400.0, 0.0], [0.0, 0.01]];
        let direction = vertex_direction(points).unwrap();
        assert!(direction > 179.99 && direction < 180.0);
        assert_eq!(
            values(right_triangle(&rect(), points, 180.0, true).unwrap()),
            [165.0, 117.5, 35.0, 17.5],
        );
    }

    #[test]
    fn underflowed_triangle_control_length_leaves_the_saved_apex_ratio() {
        let rect = NativeRect::new(crate::BoundingBox {
            x_min: 0.0,
            y_min: 0.0,
            x_max: 1e-23,
            y_max: 100.0,
        })
        .unwrap();
        let apex = f64::from(rect.width * 0.5);
        let expected = triangle(&rect, [apex as f32, 0.0], 0.0, false, &[]).unwrap();
        assert_eq!(
            values(expected),
            [rect.width * 0.25, 50.0, rect.width * 0.25, 0.0]
        );
        assert_eq!(
            triangle(&rect, [apex as f32, 0.0], 0.0, false, &[[apex, 0.0]]).unwrap(),
            expected,
        );
    }

    #[test]
    fn triangle_requires_the_native_vertex_schema() {
        assert!(
            vertices(&[
                NativePathCommand::Move([0.0, 0.0]),
                NativePathCommand::Close
            ])
            .is_err()
        );
    }
}