Give whole-expanse gradient layers a transform cage based on the gradient line (#4413)

This commit is contained in:
Keavon Chambers
2026-08-05 17:47:54 -07:00
committed by GitHub
parent fd01d66070
commit faed7f81bf
2 changed files with 121 additions and 0 deletions

View File

@@ -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> {
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`.
@@ -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`).