mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 19:08:05 +08:00
Add draggable diamond midpoint gizmos to the Gradient tool (#3826)
This commit is contained in:
@@ -175,6 +175,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
|
||||
entry!(KeyDown(Enter); modifiers=[Accel], action_dispatch=TextToolMessage::Abort),
|
||||
//
|
||||
// GradientToolMessage
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=GradientToolMessage::DoubleClick),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=GradientToolMessage::PointerDown),
|
||||
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: Shift }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=GradientToolMessage::PointerUp),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::consts::{
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLACK, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW,
|
||||
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
||||
MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -273,8 +273,12 @@ impl OverlayContext {
|
||||
self.internal().manipulator_anchor(position, selected, color);
|
||||
}
|
||||
|
||||
pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) {
|
||||
self.internal().gradient_color_stop(position, selected, color);
|
||||
pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str, small: bool) {
|
||||
self.internal().gradient_color_stop(position, selected, color, small);
|
||||
}
|
||||
|
||||
pub fn gradient_midpoint(&mut self, position: DVec2, selected: bool, angle: f64) {
|
||||
self.internal().gradient_midpoint(position, selected, angle);
|
||||
}
|
||||
|
||||
pub fn resize_handle(&mut self, position: DVec2, rotation: f64) {
|
||||
@@ -587,12 +591,12 @@ impl OverlayContextInternal {
|
||||
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
|
||||
}
|
||||
|
||||
fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) {
|
||||
fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str, small: bool) {
|
||||
let transform = self.get_transform();
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) };
|
||||
let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset;
|
||||
let radius = (MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset) * if small { 0.5 } else { 1. };
|
||||
|
||||
let mut draw_circle = |radius: f64, width: Option<f64>, color: &str| {
|
||||
let circle = kurbo::Circle::new((position.x, position.y), radius);
|
||||
@@ -610,6 +614,31 @@ impl OverlayContextInternal {
|
||||
draw_circle(radius, Some(stroke_width), COLOR_OVERLAY_WHITE);
|
||||
}
|
||||
|
||||
fn gradient_midpoint(&mut self, position: DVec2, selected: bool, angle: f64) {
|
||||
let transform = self.get_transform();
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
// Rotate diamond points by the gradient line angle
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let rotate = |dx: f64, dy: f64| DVec2::new(dx * cos - dy * sin, dx * sin + dy * cos);
|
||||
|
||||
let top = rotate(0., -GRADIENT_MIDPOINT_DIAMOND_RADIUS);
|
||||
let right = rotate(GRADIENT_MIDPOINT_DIAMOND_RADIUS, 0.);
|
||||
let bottom = rotate(0., GRADIENT_MIDPOINT_DIAMOND_RADIUS);
|
||||
let left = rotate(-GRADIENT_MIDPOINT_DIAMOND_RADIUS, 0.);
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(position.x + top.x, position.y + top.y));
|
||||
path.line_to(kurbo::Point::new(position.x + right.x, position.y + right.y));
|
||||
path.line_to(kurbo::Point::new(position.x + bottom.x, position.y + bottom.y));
|
||||
path.line_to(kurbo::Point::new(position.x + left.x, position.y + left.y));
|
||||
path.close_path();
|
||||
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &path);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
fn resize_handle(&mut self, position: DVec2, rotation: f64) {
|
||||
let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);
|
||||
self.quad(quad, None, Some(COLOR_OVERLAY_WHITE));
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::utility_functions::overlay_canvas_context;
|
||||
use crate::consts::{
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLACK, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW,
|
||||
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
||||
MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS,
|
||||
SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
@@ -468,13 +469,13 @@ impl OverlayContext {
|
||||
self.square(position, None, Some(color_fill), Some(color_stroke));
|
||||
}
|
||||
|
||||
pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str) {
|
||||
pub fn gradient_color_stop(&mut self, position: DVec2, selected: bool, color: &str, small: bool) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
let (radius_offset, stroke_width) = if selected { (1., 3.) } else { (0., 1.) };
|
||||
let radius = MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset;
|
||||
let radius = (MANIPULATOR_GROUP_MARKER_SIZE / 1.5 + 1. + radius_offset) * if small { 0.5 } else { 1. };
|
||||
|
||||
let draw_circle = |radius: f64, width: Option<f64>, color: &str| {
|
||||
self.render_context.begin_path();
|
||||
@@ -500,6 +501,36 @@ impl OverlayContext {
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn gradient_midpoint(&mut self, position: DVec2, selected: bool, angle: f64) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
// Rotate diamond points by the gradient line angle
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let rotate = |dx: f64, dy: f64| DVec2::new(dx * cos - dy * sin, dx * sin + dy * cos);
|
||||
|
||||
let top = rotate(0., -GRADIENT_MIDPOINT_DIAMOND_RADIUS);
|
||||
let right = rotate(GRADIENT_MIDPOINT_DIAMOND_RADIUS, 0.);
|
||||
let bottom = rotate(0., GRADIENT_MIDPOINT_DIAMOND_RADIUS);
|
||||
let left = rotate(-GRADIENT_MIDPOINT_DIAMOND_RADIUS, 0.);
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context.move_to(position.x + top.x, position.y + top.y);
|
||||
self.render_context.line_to(position.x + right.x, position.y + right.y);
|
||||
self.render_context.line_to(position.x + bottom.x, position.y + bottom.y);
|
||||
self.render_context.line_to(position.x + left.x, position.y + left.y);
|
||||
self.render_context.close_path();
|
||||
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.render_context.set_fill_style_str(fill);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.fill();
|
||||
self.render_context.stroke();
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD};
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, GRADIENT_MIDPOINT_DIAMOND_RADIUS, GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN, GRADIENT_STOP_MIN_VIEWPORT_GAP, LINE_ROTATE_SNAP_ANGLE,
|
||||
MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD,
|
||||
};
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
@@ -29,6 +32,7 @@ pub enum GradientToolMessage {
|
||||
|
||||
// Tool-specific messages
|
||||
DeleteStop,
|
||||
DoubleClick,
|
||||
InsertStop,
|
||||
PointerDown,
|
||||
PointerMove { constrain_axis: Key },
|
||||
@@ -113,6 +117,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
PointerMove,
|
||||
DoubleClick,
|
||||
Abort,
|
||||
DeleteStop,
|
||||
);
|
||||
@@ -163,12 +168,18 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
|
||||
multiplied * bound_transform
|
||||
}
|
||||
|
||||
/// Whether two adjacent stops are too closely packed in viewport space for a midpoint diamond to be shown or interacted with.
|
||||
fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewport_line_length: f64) -> bool {
|
||||
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
|
||||
pub enum GradientDragTarget {
|
||||
Start,
|
||||
#[default]
|
||||
End,
|
||||
Step(usize),
|
||||
Stop(usize),
|
||||
Midpoint(usize),
|
||||
New,
|
||||
}
|
||||
|
||||
@@ -197,6 +208,23 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
|
||||
return None;
|
||||
}
|
||||
|
||||
// Don't insert when clicking near a (currently visible) midpoint diamond
|
||||
let line_length = start.distance(end);
|
||||
for i in 0..stops.position.len().saturating_sub(1) {
|
||||
let left = stops.position[i];
|
||||
let right = stops.position[i + 1];
|
||||
|
||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let midpoint_pos = left + stops.midpoint[i] * (right - left);
|
||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
||||
if midpoint_viewport.distance_squared(mouse) < GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
return Some(projection);
|
||||
}
|
||||
|
||||
@@ -278,10 +306,17 @@ impl SelectedGradient {
|
||||
self.gradient.start = self.transform.inverse().transform_point2(drag_start);
|
||||
self.gradient.end = transformed_mouse;
|
||||
}
|
||||
GradientDragTarget::Step(s) => {
|
||||
GradientDragTarget::Stop(s) => {
|
||||
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
|
||||
|
||||
let (viewport_start, viewport_end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end));
|
||||
|
||||
let line_length = viewport_start.distance(viewport_end);
|
||||
if line_length < f64::EPSILON {
|
||||
self.render_gradient(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let (document_start, document_end) = (
|
||||
document_to_viewport.inverse().transform_point2(viewport_start),
|
||||
document_to_viewport.inverse().transform_point2(viewport_end),
|
||||
@@ -306,16 +341,90 @@ impl SelectedGradient {
|
||||
snap_manager.update_indicator(snapped);
|
||||
|
||||
// Calculate the new position by finding the closest point on the line
|
||||
let new_pos = ((viewport_end - viewport_start).angle_to(projected_mouse - viewport_start)).cos() * viewport_start.distance(projected_mouse) / viewport_start.distance(viewport_end);
|
||||
let new_pos = ((viewport_end - viewport_start).angle_to(projected_mouse - viewport_start)).cos() * viewport_start.distance(projected_mouse) / line_length;
|
||||
|
||||
// Should not go off end but can swap
|
||||
let clamped = new_pos.clamp(0., 1.);
|
||||
if !new_pos.is_finite() {
|
||||
self.render_gradient(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp within neighboring stops with a minimum viewport-space gap away from the neighboring stops on either end
|
||||
let min_gap = GRADIENT_STOP_MIN_VIEWPORT_GAP / line_length;
|
||||
let initial_pos = self.initial_gradient.stops.position[s];
|
||||
|
||||
let left_bound = if s > 0 {
|
||||
let left_neighbor = self.gradient.stops.position[s - 1];
|
||||
(left_neighbor + min_gap).min(initial_pos)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
let right_bound = if s + 1 < self.gradient.stops.len() {
|
||||
let right_neighbor = self.gradient.stops.position[s + 1];
|
||||
(right_neighbor - min_gap).max(initial_pos)
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
|
||||
let clamped = new_pos.clamp(left_bound, right_bound);
|
||||
self.gradient.stops.position[s] = clamped;
|
||||
let new_position = self.gradient.stops.position[s];
|
||||
let new_color = self.gradient.stops.color[s];
|
||||
|
||||
self.gradient.stops.sort();
|
||||
self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|s| s.position == new_position && s.color == new_color).unwrap());
|
||||
if let Some(new_index) = self.gradient.stops.iter().position(|s| s.position == new_position && s.color == new_color) {
|
||||
self.dragging = GradientDragTarget::Stop(new_index);
|
||||
}
|
||||
}
|
||||
GradientDragTarget::Midpoint(midpoint_index) => {
|
||||
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
|
||||
|
||||
let (viewport_start, viewport_end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end));
|
||||
|
||||
let line_length = viewport_start.distance(viewport_end);
|
||||
if line_length < f64::EPSILON {
|
||||
self.render_gradient(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
let (document_start, document_end) = (
|
||||
document_to_viewport.inverse().transform_point2(viewport_start),
|
||||
document_to_viewport.inverse().transform_point2(viewport_end),
|
||||
);
|
||||
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: document_start,
|
||||
direction: document_end - document_start,
|
||||
};
|
||||
|
||||
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
|
||||
let point_candidate = SnapCandidatePoint::gradient_handle(document_mouse);
|
||||
|
||||
let snapped = snap_manager.constrained_snap(&snap_data, &point_candidate, constraint, SnapTypeConfiguration::default());
|
||||
|
||||
let projected_mouse_document = if snapped.is_snapped() {
|
||||
snapped.snapped_point_document
|
||||
} else {
|
||||
constraint.projection(document_mouse)
|
||||
};
|
||||
let projected_mouse = document_to_viewport.transform_point2(projected_mouse_document);
|
||||
snap_manager.update_indicator(snapped);
|
||||
|
||||
// Calculate the position along the full gradient (0-1)
|
||||
let full_pos = ((viewport_end - viewport_start).angle_to(projected_mouse - viewport_start)).cos() * viewport_start.distance(projected_mouse) / line_length;
|
||||
|
||||
if !full_pos.is_finite() {
|
||||
self.render_gradient(responses);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to a midpoint ratio within the interval between the two surrounding stops
|
||||
let left_stop = self.gradient.stops.position[midpoint_index];
|
||||
let right_stop = self.gradient.stops.position[midpoint_index + 1];
|
||||
let range = right_stop - left_stop;
|
||||
if range > 0. {
|
||||
let midpoint_ratio = ((full_pos - left_stop) / range).clamp(GRADIENT_MIDPOINT_MIN, GRADIENT_MIDPOINT_MAX);
|
||||
self.gradient.stops.midpoint[midpoint_index] = midpoint_ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.render_gradient(responses);
|
||||
@@ -391,7 +500,8 @@ impl Fsm for GradientToolFsmState {
|
||||
.filter(|selected| selected.layer.is_some_and(|selected_layer| selected_layer == layer))
|
||||
.map(|selected| selected.dragging);
|
||||
|
||||
let gradient = if dragging.is_some()
|
||||
let gradient = if self == GradientToolFsmState::Drawing
|
||||
&& dragging.is_some()
|
||||
&& let Some(selected_gradient) = selected.filter(|s| s.layer == Some(layer))
|
||||
{
|
||||
&selected_gradient.gradient
|
||||
@@ -409,21 +519,75 @@ impl Fsm for GradientToolFsmState {
|
||||
let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
let end_hex = stops.color.last().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||
|
||||
// Check if the first/last stops are at position ~0/~1 (rendered as the endpoint dots rather than as separate stops)
|
||||
let first_at_start = stops.position.first().is_some_and(|&p| p.abs() < f64::EPSILON * 1000.);
|
||||
let last_at_end = stops.position.last().is_some_and(|&p| (1. - p).abs() < f64::EPSILON * 1000.);
|
||||
|
||||
let start_selected = first_at_start && (dragging == Some(GradientDragTarget::Start) || dragging == Some(GradientDragTarget::Stop(0)));
|
||||
let end_selected = last_at_end && !stops.is_empty() && (dragging == Some(GradientDragTarget::End) || dragging == Some(GradientDragTarget::Stop(stops.len() - 1)));
|
||||
|
||||
overlay_context.line(start, end, None, None);
|
||||
overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), &start_hex);
|
||||
overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), &end_hex);
|
||||
overlay_context.gradient_color_stop(start, start_selected, &start_hex, !first_at_start);
|
||||
overlay_context.gradient_color_stop(end, end_selected, &end_hex, !last_at_end);
|
||||
|
||||
for (index, stop) in stops.iter().enumerate() {
|
||||
if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. {
|
||||
continue;
|
||||
}
|
||||
overlay_context.gradient_color_stop(start.lerp(end, stop.position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(stop.color));
|
||||
overlay_context.gradient_color_stop(start.lerp(end, stop.position), dragging == Some(GradientDragTarget::Stop(index)), &color_to_hex(stop.color), false);
|
||||
}
|
||||
|
||||
if let (Some(projection), Some(dir)) = (calculate_insertion(start, end, stops, mouse), (end - start).try_normalize()) {
|
||||
// Draw midpoint diamonds between adjacent stops (hidden when stops are too close in viewport space)
|
||||
let line_angle = (end - start).to_angle();
|
||||
let line_length = start.distance(end);
|
||||
for i in 0..stops.position.len().saturating_sub(1) {
|
||||
let left = stops.position[i];
|
||||
let right = stops.position[i + 1];
|
||||
|
||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let midpoint_pos = left + stops.midpoint[i] * (right - left);
|
||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
||||
|
||||
overlay_context.gradient_midpoint(midpoint_viewport, dragging == Some(GradientDragTarget::Midpoint(i)), line_angle);
|
||||
}
|
||||
|
||||
if self != GradientToolFsmState::Drawing
|
||||
&& calculate_insertion(start, end, stops, mouse).is_some()
|
||||
&& let Some(dir) = (end - start).try_normalize()
|
||||
{
|
||||
let perp = dir.perp();
|
||||
let point = start.lerp(end, projection);
|
||||
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), Some(1.));
|
||||
|
||||
// Snap the insertion point along the gradient line
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
|
||||
let (document_start, document_end) = (document_to_viewport.inverse().transform_point2(start), document_to_viewport.inverse().transform_point2(end));
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: document_start,
|
||||
direction: document_end - document_start,
|
||||
};
|
||||
|
||||
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
|
||||
let point_candidate = SnapCandidatePoint::gradient_handle(document_mouse);
|
||||
|
||||
let snap_data = SnapData::new(document, input, viewport);
|
||||
let snapped = tool_data.snap_manager.constrained_snap(&snap_data, &point_candidate, constraint, SnapTypeConfiguration::default());
|
||||
|
||||
let snapped_point = if snapped.is_snapped() {
|
||||
document_to_viewport.transform_point2(snapped.snapped_point_document)
|
||||
} else {
|
||||
let projected = constraint.projection(document_mouse);
|
||||
document_to_viewport.transform_point2(projected)
|
||||
};
|
||||
|
||||
overlay_context.line(
|
||||
snapped_point - perp * SEGMENT_OVERLAY_SIZE,
|
||||
snapped_point + perp * SEGMENT_OVERLAY_SIZE,
|
||||
Some(COLOR_OVERLAY_BLUE),
|
||||
Some(1.),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,14 +600,34 @@ impl Fsm for GradientToolFsmState {
|
||||
tool_data.selected_gradient = None;
|
||||
self
|
||||
}
|
||||
(GradientToolFsmState::Ready { .. }, GradientToolMessage::DeleteStop) => {
|
||||
(_, GradientToolMessage::DoubleClick) => {
|
||||
// Only reset if the mouse hasn't moved so we don't trigger from a click-then-click-and-drag being reported as a double-click
|
||||
if input.mouse.position.distance(tool_data.drag_start) <= DRAG_THRESHOLD
|
||||
&& let Some(selected_gradient) = &mut tool_data.selected_gradient
|
||||
&& let GradientDragTarget::Midpoint(index) = selected_gradient.dragging
|
||||
{
|
||||
selected_gradient.gradient.stops.midpoint[index] = 0.5;
|
||||
selected_gradient.render_gradient(responses);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
self
|
||||
}
|
||||
(state, GradientToolMessage::DeleteStop) => {
|
||||
let Some(selected_gradient) = &mut tool_data.selected_gradient else {
|
||||
return self;
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
};
|
||||
|
||||
// Skip if invalid gradient
|
||||
if selected_gradient.gradient.stops.len() < 2 {
|
||||
return self;
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
|
||||
// If we're in the middle of a drag, abort it first and revert to the initial gradient
|
||||
if state == GradientToolFsmState::Drawing {
|
||||
selected_gradient.gradient = selected_gradient.initial_gradient.clone();
|
||||
selected_gradient.render_gradient(responses);
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
@@ -451,15 +635,39 @@ impl Fsm for GradientToolFsmState {
|
||||
// Remove the selected point
|
||||
match selected_gradient.dragging {
|
||||
GradientDragTarget::Start => {
|
||||
selected_gradient.gradient.stops.remove(0);
|
||||
// Only delete if there's a real color stop at position ~0 (not the endpoint of the line which isn't itself a color stop)
|
||||
if selected_gradient.gradient.stops.position.first().is_some_and(|&p| p.abs() < f64::EPSILON * 1000.) {
|
||||
selected_gradient.gradient.stops.remove(0);
|
||||
} else {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
}
|
||||
GradientDragTarget::End => {
|
||||
let _ = selected_gradient.gradient.stops.pop();
|
||||
// Only delete if there's a real color stop at position ~1 (not the endpoint of the line which isn't itself a color stop)
|
||||
if selected_gradient.gradient.stops.position.last().is_some_and(|&p| (1. - p).abs() < f64::EPSILON * 1000.) {
|
||||
let _ = selected_gradient.gradient.stops.pop();
|
||||
} else {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
}
|
||||
GradientDragTarget::Step(index) => {
|
||||
GradientDragTarget::New => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
GradientDragTarget::Stop(index) => {
|
||||
selected_gradient.gradient.stops.remove(index);
|
||||
}
|
||||
GradientDragTarget::New => {}
|
||||
GradientDragTarget::Midpoint(index) => {
|
||||
selected_gradient.gradient.stops.midpoint[index] = 0.5;
|
||||
selected_gradient.render_gradient(responses);
|
||||
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
};
|
||||
|
||||
// The gradient has only one point and so should become a fill
|
||||
@@ -472,7 +680,7 @@ impl Fsm for GradientToolFsmState {
|
||||
}
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
return self;
|
||||
return GradientToolFsmState::Ready { hover_insertion: false };
|
||||
}
|
||||
|
||||
// Find the minimum and maximum positions
|
||||
@@ -497,7 +705,7 @@ impl Fsm for GradientToolFsmState {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
tool_data.selected_gradient = None;
|
||||
|
||||
self
|
||||
GradientToolFsmState::Ready { hover_insertion: false }
|
||||
}
|
||||
(_, GradientToolMessage::InsertStop) => {
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
@@ -519,7 +727,7 @@ impl Fsm for GradientToolFsmState {
|
||||
let mut selected_gradient = SelectedGradient::new(gradient, layer, document);
|
||||
|
||||
// Select the new point
|
||||
selected_gradient.dragging = GradientDragTarget::Step(index);
|
||||
selected_gradient.dragging = GradientDragTarget::Stop(index);
|
||||
|
||||
// Update the layer fill
|
||||
selected_gradient.render_gradient(responses);
|
||||
@@ -554,33 +762,81 @@ impl Fsm for GradientToolFsmState {
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
||||
let transform = gradient_space_transform(layer, document);
|
||||
|
||||
// Check for dragging a midpoint diamond
|
||||
if !dragging {
|
||||
let (start, end) = (transform.transform_point2(gradient.start), transform.transform_point2(gradient.end));
|
||||
let line_length = start.distance(end);
|
||||
let midpoint_tolerance = GRADIENT_MIDPOINT_DIAMOND_RADIUS.powi(2);
|
||||
for i in 0..gradient.stops.position.len().saturating_sub(1) {
|
||||
let left = gradient.stops.position[i];
|
||||
let right = gradient.stops.position[i + 1];
|
||||
|
||||
if midpoint_hidden_by_proximity(left, right, line_length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let midpoint_pos = left + gradient.stops.midpoint[i] * (right - left);
|
||||
let midpoint_viewport = start.lerp(end, midpoint_pos);
|
||||
|
||||
if midpoint_viewport.distance_squared(mouse) < midpoint_tolerance {
|
||||
dragging = true;
|
||||
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: GradientDragTarget::Midpoint(i),
|
||||
initial_gradient: gradient.clone(),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dragging step
|
||||
for (index, stop) in gradient.stops.iter().enumerate() {
|
||||
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, stop.position));
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: GradientDragTarget::Step(index),
|
||||
initial_gradient: gradient.clone(),
|
||||
})
|
||||
if !dragging {
|
||||
for (index, stop) in gradient.stops.iter().enumerate() {
|
||||
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, stop.position));
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
|
||||
// Stops at position 0 or 1 are locked endpoints: dragging moves the
|
||||
// gradient line endpoint geometry (start/end) instead of stop position
|
||||
let drag_target = if stop.position.abs() < f64::EPSILON * 1000. {
|
||||
GradientDragTarget::Start
|
||||
} else if (1. - stop.position).abs() < f64::EPSILON * 1000. {
|
||||
GradientDragTarget::End
|
||||
} else {
|
||||
GradientDragTarget::Stop(index)
|
||||
};
|
||||
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: drag_target,
|
||||
initial_gradient: gradient.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check dragging start or end handle
|
||||
for (pos, dragging_target) in [(gradient.start, GradientDragTarget::Start), (gradient.end, GradientDragTarget::End)] {
|
||||
let pos = transform.transform_point2(pos);
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: dragging_target,
|
||||
initial_gradient: gradient.clone(),
|
||||
})
|
||||
if !dragging {
|
||||
for (pos, dragging_target) in [(gradient.start, GradientDragTarget::Start), (gradient.end, GradientDragTarget::End)] {
|
||||
let pos = transform.transform_point2(pos);
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: dragging_target,
|
||||
initial_gradient: gradient.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +853,7 @@ impl Fsm for GradientToolFsmState {
|
||||
transaction_started = true;
|
||||
|
||||
let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document);
|
||||
selected_gradient.dragging = GradientDragTarget::Step(index);
|
||||
selected_gradient.dragging = GradientDragTarget::Stop(index);
|
||||
// No offset when inserting a new stop, it should be exactly under the mouse
|
||||
selected_gradient.render_gradient(responses);
|
||||
tool_data.selected_gradient = Some(selected_gradient);
|
||||
@@ -701,14 +957,16 @@ impl Fsm for GradientToolFsmState {
|
||||
(GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
let was_dragging = tool_data.selected_gradient.is_some();
|
||||
|
||||
if !was_dragging
|
||||
&& let Some(selected_layer) = document.click(input, viewport)
|
||||
&& let Some(gradient) = get_gradient(selected_layer, &document.network_interface)
|
||||
{
|
||||
tool_data.selected_gradient = Some(SelectedGradient::new(gradient, selected_layer, document));
|
||||
// Clear the selection if we were dragging an endpoint of the gradient which isn't a stop
|
||||
if tool_data.selected_gradient.as_ref().is_some_and(|s| match s.dragging {
|
||||
GradientDragTarget::Start => !s.gradient.stops.position.first().is_some_and(|&p| p.abs() < f64::EPSILON * 1000.),
|
||||
GradientDragTarget::End => !s.gradient.stops.position.last().is_some_and(|&p| (1. - p).abs() < f64::EPSILON * 1000.),
|
||||
_ => false,
|
||||
}) {
|
||||
tool_data.selected_gradient = None;
|
||||
}
|
||||
|
||||
GradientToolFsmState::Ready { hover_insertion: false }
|
||||
}
|
||||
(GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerMove { .. }) => {
|
||||
@@ -940,9 +1198,9 @@ mod test_gradient {
|
||||
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
||||
|
||||
editor.select_tool(ToolType::Gradient).await;
|
||||
editor.move_mouse(50., 0., ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(50., 0., ModifierKeys::empty()).await;
|
||||
editor.left_mouseup(50., 0., ModifierKeys::empty()).await;
|
||||
editor.move_mouse(25., 0., ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(25., 0., ModifierKeys::empty()).await;
|
||||
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
||||
|
||||
// Check that a new stop has been added
|
||||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
||||
@@ -950,8 +1208,8 @@ mod test_gradient {
|
||||
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||
assert!(
|
||||
positions.iter().any(|pos| (pos - 0.5).abs() < 0.1),
|
||||
"Expected to find a stop near position 0.5, but found: {positions:?}"
|
||||
positions.iter().any(|pos| (pos - 0.25).abs() < 0.1),
|
||||
"Expected to find a stop near position 0.25, but found: {positions:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1034,10 +1292,10 @@ mod test_gradient {
|
||||
|
||||
editor.select_tool(ToolType::Gradient).await;
|
||||
|
||||
// Add a middle stop at 50%
|
||||
editor.move_mouse(50., 0., ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(50., 0., ModifierKeys::empty()).await;
|
||||
editor.left_mouseup(50., 0., ModifierKeys::empty()).await;
|
||||
// Add a middle stop at 25%
|
||||
editor.move_mouse(25., 0., ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(25., 0., ModifierKeys::empty()).await;
|
||||
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
||||
|
||||
let (initial_gradient, _) = get_gradient(&mut editor).await;
|
||||
assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len());
|
||||
@@ -1047,12 +1305,12 @@ mod test_gradient {
|
||||
stops.sort();
|
||||
|
||||
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
||||
assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1);
|
||||
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
|
||||
|
||||
let middle_color = stops.color[1].to_rgba8_srgb();
|
||||
|
||||
// Simulate dragging the middle stop to position 0.8
|
||||
let click_position = DVec2::new(50., 0.);
|
||||
let click_position = DVec2::new(25., 0.);
|
||||
editor
|
||||
.mousedown(
|
||||
EditorMouseState {
|
||||
|
||||
Reference in New Issue
Block a user