mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-22 05:54:05 +00:00
fix fixes
This commit is contained in:
parent
76c9f1c894
commit
a73dec33ff
1 changed files with 180 additions and 136 deletions
|
@ -19,7 +19,7 @@ use graphene_core::transform::{Footprint, Transform};
|
|||
use graphene_core::uuid::{NodeId, generate_uuid};
|
||||
use graphene_core::vector::Vector;
|
||||
use graphene_core::vector::click_target::{ClickTarget, FreePoint};
|
||||
use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
|
||||
use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode, PaintOrder};
|
||||
use graphene_core::{Artboard, Graphic};
|
||||
use kurbo::Affine;
|
||||
use num_traits::Zero;
|
||||
|
@ -697,13 +697,15 @@ impl Render for Table<Vector> {
|
|||
} else {
|
||||
MaskType::Mask
|
||||
};
|
||||
|
||||
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
|
||||
let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered());
|
||||
let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip);
|
||||
|
||||
let needs_separate_fill = can_draw_aligned_stroke && !can_use_paint_order;
|
||||
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
|
||||
|
||||
if needs_separate_fill {
|
||||
if needs_separate_fill && !wants_stroke_below {
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path.clone());
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
|
@ -742,7 +744,7 @@ impl Render for Table<Vector> {
|
|||
});
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
attributes.push("d", path.clone());
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
|
@ -753,8 +755,8 @@ impl Render for Table<Vector> {
|
|||
let mut svg = SvgRender::new();
|
||||
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
|
||||
let stroke = row.element.style.stroke().unwrap();
|
||||
let stroke_px = stroke.weight * max_scale(applied_stroke_transform);
|
||||
let quad = Quad::from_box(transformed_bounds).inflate(stroke_px);
|
||||
let weight = stroke.weight * max_scale(applied_stroke_transform);
|
||||
let quad = Quad::from_box(transformed_bounds).inflate(weight);
|
||||
let (x, y) = quad.top_left().into();
|
||||
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
||||
|
||||
|
@ -765,7 +767,7 @@ impl Render for Table<Vector> {
|
|||
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
|
||||
MaskType::Mask => write!(
|
||||
defs,
|
||||
r##"<mask id="{id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="{x}" y="{y}" width="{width}" height="{height}" mask-type="luminance">{}{}</mask>"##,
|
||||
r##"<mask id="{id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="{x}" y="{y}" width="{width}" height="{height}">{}{}</mask>"##,
|
||||
rect,
|
||||
svg.svg.to_svg_string()
|
||||
)
|
||||
|
@ -799,6 +801,28 @@ impl Render for Table<Vector> {
|
|||
attributes.push("style", row.alpha_blending.blend_mode.render());
|
||||
}
|
||||
});
|
||||
|
||||
// When splitting passes and stroke is below, draw the fill after the stroke.
|
||||
if needs_separate_fill && wants_stroke_below {
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
let mut style = row.element.style.clone();
|
||||
style.clear_stroke();
|
||||
let fill_and_stroke = style.render(
|
||||
&mut attributes.0.svg_defs,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
&render_params,
|
||||
);
|
||||
attributes.push_val(fill_and_stroke);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -839,7 +863,7 @@ impl Render for Table<Vector> {
|
|||
if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() {
|
||||
layer = true;
|
||||
let weight = row.element.style.stroke().unwrap().weight;
|
||||
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant());
|
||||
let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
|
||||
let layer_bounds = quad.bounding_box();
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||
|
@ -852,36 +876,100 @@ impl Render for Table<Vector> {
|
|||
let can_draw_aligned_stroke =
|
||||
row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed());
|
||||
|
||||
let reorder_part = row.element.style.stroke().is_some_and(|stroke| stroke.align == StrokeAlign::Outside) && !row.element.style.fill().is_none();
|
||||
let reorder_for_outside = reorder_part && row.element.style.fill().is_opaque();
|
||||
let use_layer = can_draw_aligned_stroke && !reorder_part;
|
||||
if use_layer {
|
||||
let mut element = row.element.clone();
|
||||
element.style.clear_stroke();
|
||||
element.style.set_fill(Fill::solid(Color::BLACK));
|
||||
let use_layer = can_draw_aligned_stroke;
|
||||
let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == graphene_core::vector::style::PaintOrder::StrokeBelow);
|
||||
|
||||
let vector_table = Table::new_from_row(TableRow {
|
||||
element,
|
||||
alpha_blending: *row.alpha_blending,
|
||||
transform: *row.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
// Closures to avoid duplicated fill/stroke drawing logic
|
||||
let do_fill = |scene: &mut Scene| {
|
||||
match row.element.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
|
||||
let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
|
||||
let weight = row.element.style.stroke().unwrap().weight;
|
||||
let quad = Quad::from_box(bounds).inflate(weight * element_transform.matrix2.determinant());
|
||||
let bounds = quad.bounding_box();
|
||||
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||
let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
|
||||
peniko::Compose::SrcOut
|
||||
} else {
|
||||
peniko::Compose::SrcIn
|
||||
};
|
||||
let bounds = row.element.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
}
|
||||
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
|
||||
parent_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.end);
|
||||
|
||||
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient.gradient_type {
|
||||
GradientType::Linear => peniko::GradientKind::Linear {
|
||||
start: to_point(start),
|
||||
end: to_point(end),
|
||||
},
|
||||
GradientType::Radial => {
|
||||
let radius = start.distance(end);
|
||||
peniko::GradientKind::Radial {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
|
||||
element_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
}
|
||||
};
|
||||
|
||||
let do_stroke = |scene: &mut Scene, width_scale: f64| {
|
||||
if let Some(stroke) = row.element.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => Cap::Butt,
|
||||
StrokeCap::Round => Cap::Round,
|
||||
StrokeCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => Join::Miter,
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight * width_scale,
|
||||
miter_limit: stroke.join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render the path
|
||||
match render_params.view_mode {
|
||||
|
@ -905,120 +993,76 @@ impl Render for Table<Vector> {
|
|||
scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path);
|
||||
}
|
||||
_ => {
|
||||
enum Op {
|
||||
Fill,
|
||||
Stroke,
|
||||
}
|
||||
if use_layer {
|
||||
let mut element = row.element.clone();
|
||||
element.style.clear_stroke();
|
||||
element.style.set_fill(Fill::solid(Color::BLACK));
|
||||
|
||||
let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside {
|
||||
true => [Op::Stroke, Op::Fill],
|
||||
false => [Op::Fill, Op::Stroke], // Default
|
||||
};
|
||||
let vector_table = Table::new_from_row(TableRow {
|
||||
element,
|
||||
alpha_blending: *row.alpha_blending,
|
||||
transform: *row.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
|
||||
for operation in order {
|
||||
match operation {
|
||||
Op::Fill => {
|
||||
match row.element.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
// Compute bounding box of the shape to determine the gradient start and end points
|
||||
let bounds = row.element.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
|
||||
let weight = row.element.style.stroke().unwrap().weight;
|
||||
let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform));
|
||||
let bounds = quad.bounding_box();
|
||||
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||
|
||||
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
|
||||
parent_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
|
||||
peniko::Compose::SrcOut
|
||||
} else {
|
||||
peniko::Compose::SrcIn
|
||||
};
|
||||
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.end);
|
||||
if wants_stroke_below {
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
|
||||
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient.gradient_type {
|
||||
GradientType::Linear => peniko::GradientKind::Linear {
|
||||
start: to_point(start),
|
||||
end: to_point(end),
|
||||
},
|
||||
GradientType::Radial => {
|
||||
let radius = start.distance(end);
|
||||
peniko::GradientKind::Radial {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
|
||||
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
|
||||
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
|
||||
element_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
};
|
||||
}
|
||||
Op::Stroke => {
|
||||
if let Some(stroke) = row.element.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => Cap::Butt,
|
||||
StrokeCap::Round => Cap::Round,
|
||||
StrokeCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => Join::Miter,
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. },
|
||||
miter_limit: stroke.join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
do_stroke(scene, 2.);
|
||||
|
||||
// Draw the stroke if it's visible
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
|
||||
do_fill(scene);
|
||||
} else {
|
||||
// Fill first (unclipped), then stroke (clipped) above
|
||||
do_fill(scene);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
|
||||
do_stroke(scene, 2.);
|
||||
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
} else {
|
||||
// Non-aligned strokes or open paths: default order behavior
|
||||
enum Op {
|
||||
Fill,
|
||||
Stroke,
|
||||
}
|
||||
|
||||
let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) {
|
||||
true => [Op::Stroke, Op::Fill],
|
||||
false => [Op::Fill, Op::Stroke], // Default
|
||||
};
|
||||
|
||||
for operation in order {
|
||||
match operation {
|
||||
Op::Fill => do_fill(scene),
|
||||
Op::Stroke => do_stroke(scene, 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if use_layer {
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
|
||||
// If we pushed a layer for opacity or a blend mode, we need to pop it
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue