openfree's picture
Deploy from GitHub repository
2409829 verified
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType;
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(
document: &DocumentMessageHandler,
goal: DVec2,
tolerance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>,
preferences: &PreferencesMessageHandler,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
closest_point(document, goal, tolerance, layers, |_| false, preferences)
}
/// Determine the closest point to the goal point under max_distance.
/// Additionally exclude checking closeness to the point which given to exclude() returns true.
pub fn closest_point<T>(
document: &DocumentMessageHandler,
goal: DVec2,
max_distance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>,
exclude: T,
preferences: &PreferencesMessageHandler,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)>
where
T: Fn(PointId) -> bool,
{
let mut best = None;
let mut best_distance_squared = max_distance * max_distance;
for layer in layers {
let viewspace = document.metadata().transform_to_viewport(layer);
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
for id in vector_data.extendable_points(preferences.vector_meshes) {
if exclude(id) {
continue;
}
let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };
let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
if distance_squared < best_distance_squared {
best = Some((layer, id, point));
best_distance_squared = distance_squared;
}
}
}
best
}
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
let Some((text, font, typesetting)) = get_text(layer, &document.network_interface) else {
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
};
let font_data = font_cache.get(font).map(|data| load_font(data));
let far = graphene_std::text::bounding_box(text, font_data, typesetting, false);
Quad::from_box([DVec2::ZERO, far])
}
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, prefer_handle_direction: bool) -> Option<f64> {
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
let start_point = if is_start(anchor, segment) {
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
} else {
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
};
let required_handle = if is_start(anchor, segment) {
start_handle
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
.or(end_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
} else {
end_handle
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
.or(start_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
};
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
}
pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector_data: &VectorData, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
let Some(other_handle) = vector_data.other_colinear_handle(handle) else { return };
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
return;
};
let Some(direction) = (anchor_position - target_control_point).try_normalize() else { return };
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
pub fn restore_previous_handle_position(
handle: HandleId,
original_c: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) -> Option<HandleId> {
let other_handle = vector_data.other_colinear_handle(handle)?;
let handle_position = other_handle.to_manipulator_point().get_position(vector_data)?;
let direction = (anchor_position - original_c).try_normalize()?;
let old_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(old_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
Some(other_handle)
}
pub fn restore_g1_continuity(
handle: HandleId,
other_handle: HandleId,
control_point: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) {
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
return;
};
let Some(direction) = (anchor_position - control_point).try_normalize() else { return };
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
/// Check whether a point is visible in the current overlay mode.
pub fn is_visible_point(
manipulator_point_id: ManipulatorPointId,
vector_data: &VectorData,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
selected_segments: Vec<SegmentId>,
selected_points: &HashSet<ManipulatorPointId>,
) -> bool {
match manipulator_point_id {
ManipulatorPointId::Anchor(_) => true,
ManipulatorPointId::EndHandle(segment_id) | ManipulatorPointId::PrimaryHandle(segment_id) => {
match (path_overlay_mode, selected_points.len() == 1) {
(PathOverlayMode::AllHandles, _) => true,
(PathOverlayMode::SelectedPointHandles, _) | (PathOverlayMode::FrontierHandles, true) => {
if selected_segments.contains(&segment_id) {
return true;
}
// Either the segment is a part of selected segments or the opposite handle is a part of existing selection
let Some(handle_pair) = manipulator_point_id.get_handle_pair(vector_data) else { return false };
let other_handle = handle_pair[1].to_manipulator_point();
// Return whether the list of selected points contain the other handle
selected_points.contains(&other_handle)
}
(PathOverlayMode::FrontierHandles, false) => {
let Some(anchor) = manipulator_point_id.get_anchor(vector_data) else {
warn!("No anchor for selected handle");
return false;
};
let Some(frontier_handles) = &frontier_handles_info else {
warn!("No frontier handles info provided");
return false;
};
frontier_handles.get(&segment_id).map(|anchors| anchors.contains(&anchor)).unwrap_or_default()
}
}
}
}
}
/// Function to find the bounding box of bezier (uses method from kurbo)
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let bbox = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
CubicBez::new(start, p1, p2, end).bounding_box()
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
QuadBez::new(start, p1, end).bounding_box()
}
BezierHandles::Linear => Line::new(start, end).bounding_box(),
};
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
}
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
let to_layerspace = transform.inverse();
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let segment = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
PathSeg::Cubic(CubicBez::new(start, p1, p2, end))
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
PathSeg::Quad(QuadBez::new(start, p1, end))
}
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
};
// Create a list of all the sides
let sides = [
Line::new((quad[0].x, quad[0].y), (quad[1].x, quad[0].y)),
Line::new((quad[0].x, quad[0].y), (quad[0].x, quad[1].y)),
Line::new((quad[1].x, quad[1].y), (quad[1].x, quad[0].y)),
Line::new((quad[1].x, quad[1].y), (quad[0].x, quad[1].y)),
];
let mut is_intersecting = false;
for line in sides {
let intersections = segment.intersect_line(line);
let mut intersects = false;
for intersection in intersections {
if intersection.line_t <= 1. && intersection.line_t >= 0. && intersection.segment_t <= 1. && intersection.segment_t >= 0. {
// There is a valid intersection point
intersects = true;
break;
}
}
if intersects {
is_intersecting = true;
break;
}
}
is_intersecting
}
#[allow(clippy::too_many_arguments)]
pub fn resize_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
snap_manager: &mut SnapManager,
snap_candidates: &mut Vec<SnapCandidatePoint>,
input: &InputPreprocessorMessageHandler,
center: bool,
constrain: bool,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let center = center.then_some(bounds.center_of_transformation);
let snap = Some(SizeSnapData {
manager: snap_manager,
points: snap_candidates,
snap_data: SnapData::ignore(document, input, dragging_layers),
});
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
let pivot_transform = DAffine2::from_translation(pivot);
let transformation = pivot_transform * delta * pivot_transform.inverse();
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, dragging_layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
}
}
#[allow(clippy::too_many_arguments)]
pub fn rotate_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
drag_start: DVec2,
mouse_position: DVec2,
snap_angle: bool,
tool: ToolType,
) {
let angle = {
let start_offset = drag_start - bounds.center_of_transformation;
let end_offset = mouse_position - bounds.center_of_transformation;
start_offset.angle_to(end_offset)
};
let snapped_angle = if snap_angle {
let snap_resolution = ROTATE_INCREMENT.to_radians();
(angle / snap_resolution).round() * snap_resolution
} else {
angle
};
let delta = DAffine2::from_angle(snapped_angle);
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of replacement_selected_layers");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
dragging_layers,
responses,
&document.network_interface,
None,
&tool,
None,
);
selected.update_transforms(delta, None, None);
}
pub fn skew_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
free_movement: bool,
layers: &mut Vec<LayerNodeIdentifier>,
mouse_position: DVec2,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let mut pivot = DVec2::ZERO;
let transformation = movement.skew_transform(mouse_position, bounds.original_bound_transform, free_movement);
layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
}
}
// TODO: Replace returned tuple (where at most 1 element is true at a time) with an enum.
/// Returns the tuple (resize, rotate, skew).
pub fn transforming_transform_cage(
document: &DocumentMessageHandler,
mut bounding_box_manager: &mut Option<BoundingBoxManager>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
layers_dragging: &mut Vec<LayerNodeIdentifier>,
) -> (bool, bool, bool) {
let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
selected_edges
});
edges
});
let rotating_bounds = bounding_box_manager.as_ref().map(|bounding_box| bounding_box.check_rotate(input.mouse.position)).unwrap_or_default();
let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
let is_flat_layer = bounding_box_manager.as_ref().map(|bounding_box_manager| bounding_box_manager.transform_tampered).unwrap_or(true);
if dragging_bounds.is_some() && !is_flat_layer {
responses.add(DocumentMessage::StartTransaction);
*layers_dragging = selected;
if let Some(bounds) = &mut bounding_box_manager {
bounds.original_bound_transform = bounds.transform;
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
layers_dragging,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
// Check if we're hovering over a skew triangle
let edges = bounds.check_selected_edges(input.mouse.position);
if let Some(edges) = edges {
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
// No resize or rotate, just skew
return (false, false, true);
}
}
}
// Just resize, no rotate or skew
return (true, false, false);
}
if rotating_bounds {
responses.add(DocumentMessage::StartTransaction);
if let Some(bounds) = &mut bounding_box_manager {
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
&selected,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
}
*layers_dragging = selected;
// No resize or skew, just rotate
return (false, true, false);
}
// No resize, rotate, or skew
(false, false, false)
}
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
#[allow(clippy::too_many_arguments)]
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
let start_handle_length = a.exp();
let end_handle_length = b.exp();
// Compute the handle positions of new bezier curve
let c1 = p1 + d1 * start_handle_length;
let c2 = p3 + d2 * end_handle_length;
let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y);
// Sample 2*n points from new curve and get the L2 metric between all of points
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
dist / (2 * n) as f64
}
/// Calculates optimal handle lengths with adam optimization.
#[allow(clippy::too_many_arguments)]
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
let h = 1e-6;
let tol = 1e-6;
let max_iter = 200;
let mut a = (5_f64).ln();
let mut b = (5_f64).ln();
let mut m_a = 0.;
let mut v_a = 0.;
let mut m_b = 0.;
let mut v_b = 0.;
let initial_alpha = 0.05;
let decay_rate: f64 = 0.99;
let beta1 = 0.9;
let beta2 = 0.999;
let epsilon = 1e-8;
let n = 20;
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
farther_segment.reverse()
} else {
farther_segment
};
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment };
// Now we sample points proportional to the lengths of the beziers
let l1 = farther_segment.length(None);
let l2 = other_segment.length(None);
let ratio = l1 / (l1 + l2);
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
points1.append(&mut points2);
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };
for t in 1..=max_iter {
let dfa = (f(a + h, b) - f(a - h, b)) / (2. * h);
let dfb = (f(a, b + h) - f(a, b - h)) / (2. * h);
m_a = beta1 * m_a + (1. - beta1) * dfa;
m_b = beta1 * m_b + (1. - beta1) * dfb;
v_a = beta2 * v_a + (1. - beta2) * dfa * dfa;
v_b = beta2 * v_b + (1. - beta2) * dfb * dfb;
let m_a_hat = m_a / (1. - beta1.powi(t));
let v_a_hat = v_a / (1. - beta2.powi(t));
let m_b_hat = m_b / (1. - beta1.powi(t));
let v_b_hat = v_b / (1. - beta2.powi(t));
let alpha_t = initial_alpha * decay_rate.powi(t);
// Update log-lengths
a -= alpha_t * m_a_hat / (v_a_hat.sqrt() + epsilon);
b -= alpha_t * m_b_hat / (v_b_hat.sqrt() + epsilon);
// Convergence check
if dfa.abs() < tol && dfb.abs() < tol {
break;
}
}
let len1 = a.exp().max(min_len1);
let len2 = b.exp().max(min_len2);
(d1 * len1, d2 * len2)
}