mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Give whole-expanse gradient layers a transform cage based on the gradient line (#4413)
This commit is contained in:
@@ -2977,6 +2977,53 @@ mod test_gradient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn whole_expanse_gradient_reports_control_geometry_outline_bounds() {
|
||||||
|
use graph_craft::document::NodeId;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||||
|
id: NodeId::new(),
|
||||||
|
nodes: Vec::new(),
|
||||||
|
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||||
|
insert_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
// A gradient dragged across a blank layer paints its whole expanse via a 'Gradient Value' chain
|
||||||
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
assert!(get_upstream_gradient_value_node_id(layer, &editor.active_document().network_interface).is_some());
|
||||||
|
|
||||||
|
editor.eval_graph().await.expect("graph should evaluate");
|
||||||
|
let bounds = editor.active_document().metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY);
|
||||||
|
assert_eq!(bounds, Some([DVec2::ZERO, DVec2::X]), "a linear expanse's local bounds should be its canonical gradient line");
|
||||||
|
|
||||||
|
// A linear's control line has no interior, so it registers as outline only
|
||||||
|
let click_targets = editor.active_document().metadata().click_targets(layer);
|
||||||
|
assert!(click_targets.is_none_or(|targets| targets.is_empty()), "a linear expanse should gain no click targets");
|
||||||
|
|
||||||
|
// Switching the form swaps the control geometry to the radial unit circle
|
||||||
|
editor
|
||||||
|
.handle_message(GradientToolMessage::UpdateOptions {
|
||||||
|
options: super::GradientOptionsUpdate::Form(GradientForm::Radial),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
editor.eval_graph().await.expect("graph should evaluate");
|
||||||
|
let bounds = editor.active_document().metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY);
|
||||||
|
assert_eq!(bounds, Some([DVec2::splat(-1.), DVec2::splat(1.)]), "a radial expanse's local bounds should be its unit circle");
|
||||||
|
|
||||||
|
// A radial's main ellipse doubles as the layer's draggable handle regardless of spread
|
||||||
|
let click_targets = editor.active_document().metadata().click_targets(layer).expect("a radial expanse should be clickable");
|
||||||
|
assert!(
|
||||||
|
click_targets.iter().any(|target| target.intersect_point_no_stroke(DVec2::ZERO)),
|
||||||
|
"the ellipse interior should register a hit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn chain_stop_placement_composes_through_setter_nodes() {
|
async fn chain_stop_placement_composes_through_setter_nodes() {
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops;
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops;
|
||||||
|
|||||||
@@ -2158,6 +2158,19 @@ impl Render for List<Color> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A gradient's control geometry in its local space: the unit circle a radial gradient's transform carries to its drawn ellipse, or the (0,0) to (1,0) gradient line for a linear one.
|
||||||
|
fn gradient_control_outline(gradient_form: GradientForm) -> Subpath<graphic_types::vector_types::vector::PointId> {
|
||||||
|
match gradient_form {
|
||||||
|
GradientForm::Linear => Subpath::new_line(DVec2::ZERO, DVec2::X),
|
||||||
|
GradientForm::Radial => Subpath::new_ellipse(DVec2::splat(-1.), DVec2::splat(1.)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the control geometry's interior is a draggable click area: a radial's main ellipse acts as the layer's handle regardless of spread, while a linear's control line has no interior.
|
||||||
|
fn gradient_control_interior_is_clickable(gradient_form: GradientForm) -> bool {
|
||||||
|
gradient_form == GradientForm::Radial
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for List<Gradient> {
|
impl Render for List<Gradient> {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
||||||
@@ -2332,6 +2345,67 @@ impl Render for List<Gradient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>) {
|
||||||
|
let Some(element_id) = element_id else { return };
|
||||||
|
if self.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`
|
||||||
|
let item_zero_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
|
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
|
||||||
|
item_zero_transform.inverse()
|
||||||
|
} else {
|
||||||
|
DAffine2::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut outline_targets = Vec::new();
|
||||||
|
let mut click_targets = Vec::new();
|
||||||
|
for index in 0..self.len() {
|
||||||
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
|
||||||
|
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
|
||||||
|
target.apply_transform(item_zero_inverse * item_transform);
|
||||||
|
let target = Arc::new(target);
|
||||||
|
|
||||||
|
if gradient_control_interior_is_clickable(gradient_form) {
|
||||||
|
click_targets.push(target.clone());
|
||||||
|
}
|
||||||
|
outline_targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.outlines.insert(element_id, outline_targets);
|
||||||
|
if !click_targets.is_empty() {
|
||||||
|
metadata.click_targets.insert(element_id, click_targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
|
for index in 0..self.len() {
|
||||||
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
|
if !gradient_control_interior_is_clickable(gradient_form) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
|
||||||
|
target.apply_transform(transform);
|
||||||
|
click_targets.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
||||||
|
for index in 0..self.len() {
|
||||||
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
|
||||||
|
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.);
|
||||||
|
target.apply_transform(transform);
|
||||||
|
outlines.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`).
|
/// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`).
|
||||||
|
|||||||
Reference in New Issue
Block a user