mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
Correctly apply transforms to vector data and strokes (#1977)
* Fix adding a layer to a transformed group * Fix assorted transform issues * Default stroke transform * Fix bench * Transform gradient * Gradient fix * Add gradient reversal buttons to Fill node in the Properties panel --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
2fa8773092
commit
dd4a97b09f
19 changed files with 152 additions and 59 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -33,7 +33,6 @@
|
|||
},
|
||||
// Rust Analyzer config
|
||||
"rust-analyzer.cargo.allTargets": false,
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
// ESLint config
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"],
|
||||
|
|
|
@ -266,7 +266,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||
|
||||
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
apply_usvg_fill(path.fill(), modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform);
|
||||
apply_usvg_stroke(path.stroke(), modify_inputs);
|
||||
apply_usvg_stroke(path.stroke(), modify_inputs, transform * usvg_transform(node.abs_transform()));
|
||||
}
|
||||
usvg::Node::Image(_image) => {
|
||||
warn!("Skip image")
|
||||
|
@ -279,7 +279,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext) {
|
||||
fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
|
||||
if let Some(stroke) = stroke {
|
||||
if let usvg::Paint::Color(color) = &stroke.paint() {
|
||||
modify_inputs.stroke_set(Stroke {
|
||||
|
@ -299,6 +299,7 @@ fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyIn
|
|||
usvg::LineJoin::Bevel => LineJoin::Bevel,
|
||||
},
|
||||
line_join_miter_limit: stroke.miterlimit().get() as f64,
|
||||
transform,
|
||||
})
|
||||
} else {
|
||||
warn!("Skip non-solid stroke")
|
||||
|
|
|
@ -2597,7 +2597,28 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context:
|
|||
|
||||
let fill_type_switch = {
|
||||
let mut row = vec![TextLabel::new("").widget_holder()];
|
||||
add_blank_assist(&mut row);
|
||||
match fill {
|
||||
Fill::Solid(_) | Fill::None => add_blank_assist(&mut row),
|
||||
Fill::Gradient(gradient) => {
|
||||
let reverse_button = IconButton::new("Reverse", 24)
|
||||
.tooltip("Reverse the gradient color stops")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
gradient.stops = gradient.stops.reversed();
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
))
|
||||
.widget_holder();
|
||||
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
row.push(reverse_button);
|
||||
}
|
||||
}
|
||||
|
||||
let entries = vec![
|
||||
RadioEntryData::new("solid")
|
||||
|
@ -2619,9 +2640,35 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context:
|
|||
};
|
||||
widgets.push(fill_type_switch);
|
||||
|
||||
if let Fill::Gradient(gradient) = fill {
|
||||
if let Fill::Gradient(gradient) = fill.clone() {
|
||||
let mut row = vec![TextLabel::new("").widget_holder()];
|
||||
add_blank_assist(&mut row);
|
||||
match gradient.gradient_type {
|
||||
GradientType::Linear => add_blank_assist(&mut row),
|
||||
GradientType::Radial => {
|
||||
let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 {
|
||||
gradient.end.x > gradient.start.x
|
||||
} else {
|
||||
(gradient.start.x + gradient.start.y) < (gradient.end.x + gradient.end.y)
|
||||
};
|
||||
let reverse_radial_gradient_button = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
.tooltip("Reverse which end the gradient radiates from")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
std::mem::swap(&mut gradient.start, &mut gradient.end);
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
))
|
||||
.widget_holder();
|
||||
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
row.push(reverse_radial_gradient_button);
|
||||
}
|
||||
}
|
||||
|
||||
let new_gradient1 = gradient.clone();
|
||||
let new_gradient2 = gradient.clone();
|
||||
|
|
|
@ -206,16 +206,15 @@ impl Fsm for EllipseToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
EllipseToolFsmState::Drawing
|
||||
}
|
||||
|
|
|
@ -233,7 +233,7 @@ impl Fsm for FreehandToolFsmState {
|
|||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_data.layer = Some(layer);
|
||||
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let transform = document.metadata().transform_to_viewport(parent);
|
||||
let position = transform.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
extend_path_with_next_segment(tool_data, position, responses);
|
||||
|
|
|
@ -86,11 +86,11 @@ impl LayoutHolder for GradientTool {
|
|||
let gradient_type = RadioInput::new(vec![
|
||||
RadioEntryData::new("linear")
|
||||
.label("Linear")
|
||||
.tooltip("Linear Gradient")
|
||||
.tooltip("Linear gradient")
|
||||
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()),
|
||||
RadioEntryData::new("radial")
|
||||
.label("Radial")
|
||||
.tooltip("Radial Gradient")
|
||||
.tooltip("Radial gradient")
|
||||
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()),
|
||||
])
|
||||
.selected_index(Some((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32))
|
||||
|
|
|
@ -188,15 +188,14 @@ impl Fsm for LineToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_data.layer = Some(layer);
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_data.layer = Some(layer);
|
||||
|
||||
tool_data.layer = Some(layer);
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
|
|
@ -265,16 +265,15 @@ impl Fsm for PolygonToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
polygon_data.layer = Some(layer);
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
polygon_data.layer = Some(layer);
|
||||
|
||||
PolygonToolFsmState::Drawing
|
||||
}
|
||||
|
|
|
@ -212,16 +212,15 @@ impl Fsm for RectangleToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
RectangleToolFsmState::Drawing
|
||||
}
|
||||
|
|
|
@ -284,7 +284,7 @@ impl NodeRuntime {
|
|||
|
||||
for monitor_node_path in &self.monitor_nodes {
|
||||
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
|
||||
let Some(parent_network_node_id) = monitor_node_path.get(monitor_node_path.len() - 2).copied() else {
|
||||
let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
|
||||
warn!("Monitor node has invalid node id");
|
||||
|
||||
continue;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M8,7.5c0-1.38,1.12-2.5,2.5-2.5s2.5,1.12,2.5,2.5-1.12,2.5-2.5,2.5-2.5-1.12-2.5-2.5ZM9,7.5c0,.83.67,1.5,1.5,1.5s1.5-.67,1.5-1.5-.67-1.5-1.5-1.5-1.5.67-1.5,1.5z" />
|
||||
<path d="M5,7.5c0-3.03,2.47-5.5,5.5-5.5s5.5,2.47,5.5,5.5-2.47,5.5-5.5,5.5-5.5-2.47-5.5-5.5ZM6,7.5c0,2.48,2.02,4.5,4.5,4.5s4.5-2.02,4.5-4.5-2.02-4.5-4.5-4.5-4.5,2.02-4.5,4.5z" />
|
||||
<path d="M3,5v5S0,7.5,0,7.5l3-2.5z" />
|
||||
<path d="M11,7.5h0c0,.28-.22.5-.5.5H2v-1h8.5c.28,0,.5.22.5.5z" />
|
||||
</svg>
|
After Width: | Height: | Size: 526 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M5.5,10c-1.38,0-2.5-1.12-2.5-2.5s1.12-2.5,2.5-2.5,2.5,1.12,2.5,2.5-1.12,2.5-2.5,2.5ZM5.5,6c-.83,0-1.5.67-1.5,1.5s.67,1.5,1.5,1.5,1.5-.67,1.5-1.5-.67-1.5-1.5-1.5z" />
|
||||
<path d="M5.5,13c-3.03,0-5.5-2.47-5.5-5.5S2.47,2,5.5,2s5.5,2.47,5.5,5.5-2.47,5.5-5.5,5.5ZM5.5,3C3.02,3,1,5.02,1,7.5s2.02,4.5,4.5,4.5,4.5-2.02,4.5-4.5S7.98,3,5.5,3z" />
|
||||
<path d="M16,7.5l-3,2.5v-5l3,2.5z" />
|
||||
<path d="M5.5,7h8.5v1H5.5c-.28,0-.5-.22-.5-.5h0c0-.28.22-.5.5-.5z" />
|
||||
</svg>
|
After Width: | Height: | Size: 523 B |
3
frontend/assets/icon-16px-solid/reverse.svg
Normal file
3
frontend/assets/icon-16px-solid/reverse.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<polygon points="16,7.5 13,5 13,7 3,7 3,5 0,7.5 3,10 3,8 13,8 13,10 16,7.5" />
|
||||
</svg>
|
After Width: | Height: | Size: 148 B |
|
@ -148,6 +148,9 @@ import Regenerate from "@graphite-frontend/assets/icon-16px-solid/regenerate.svg
|
|||
import Reload from "@graphite-frontend/assets/icon-16px-solid/reload.svg";
|
||||
import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg";
|
||||
import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg";
|
||||
import ReverseRadialGradientToLeft from "@graphite-frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg";
|
||||
import ReverseRadialGradientToRight from "@graphite-frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg";
|
||||
import Reverse from "@graphite-frontend/assets/icon-16px-solid/reverse.svg";
|
||||
import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg";
|
||||
import Stack from "@graphite-frontend/assets/icon-16px-solid/stack.svg";
|
||||
import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
|
||||
|
@ -223,6 +226,9 @@ const SOLID_16PX = {
|
|||
Reload: { svg: Reload, size: 16 },
|
||||
Rescale: { svg: Rescale, size: 16 },
|
||||
Reset: { svg: Reset, size: 16 },
|
||||
ReverseRadialGradientToLeft: { svg: ReverseRadialGradientToLeft, size: 16 },
|
||||
ReverseRadialGradientToRight: { svg: ReverseRadialGradientToRight, size: 16 },
|
||||
Reverse: { svg: Reverse, size: 16 },
|
||||
Settings: { svg: Settings, size: 16 },
|
||||
Stack: { svg: Stack, size: 16 },
|
||||
Trash: { svg: Trash, size: 16 },
|
||||
|
|
|
@ -244,7 +244,15 @@ async fn construct_layer<Data: Into<GraphicElement> + Send>(
|
|||
) -> GraphicGroup {
|
||||
let graphic_element = self.graphic_element.eval(footprint).await;
|
||||
let mut stack = self.stack.eval(footprint).await;
|
||||
stack.push(graphic_element.into());
|
||||
let mut element: GraphicElement = graphic_element.into();
|
||||
if stack.transform.matrix2.determinant() != 0. {
|
||||
*element.transform_mut() = stack.transform.inverse() * element.transform();
|
||||
} else {
|
||||
stack.clear();
|
||||
stack.transform = DAffine2::IDENTITY;
|
||||
}
|
||||
|
||||
stack.push(element);
|
||||
stack
|
||||
}
|
||||
|
||||
|
|
|
@ -360,20 +360,27 @@ impl GraphicElementRendered for GraphicGroup {
|
|||
impl GraphicElementRendered for VectorData {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let multiplied_transform = render.transform * self.transform;
|
||||
let set_stroke_transform = self.style.stroke().map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(self.transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = self.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
|
||||
let mut path = String::new();
|
||||
for subpath in self.stroke_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
|
||||
let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform);
|
||||
}
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
attributes.push("transform", matrix);
|
||||
|
||||
let defs = &mut attributes.0.svg_defs;
|
||||
let fill_and_stroke = self
|
||||
.style
|
||||
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
|
||||
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
|
||||
attributes.push_val(fill_and_stroke);
|
||||
|
||||
if self.alpha_blending.opacity < 1. {
|
||||
|
@ -411,31 +418,31 @@ impl GraphicElementRendered for VectorData {
|
|||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) {
|
||||
use crate::vector::style::GradientType;
|
||||
use vello::peniko;
|
||||
|
||||
let transformed_bounds = GraphicElementRendered::bounding_box(self, transform).unwrap_or_default();
|
||||
let mut layer = false;
|
||||
let stroke_transform = self.style.stroke().map_or(DAffine2::IDENTITY, |stroke| stroke.transform);
|
||||
let path_transform = (transform * self.transform) * stroke_transform.inverse();
|
||||
let transformed_bounds = GraphicElementRendered::bounding_box(self, path_transform).unwrap_or_default();
|
||||
|
||||
if self.alpha_blending.opacity < 1. || self.alpha_blending.blend_mode != BlendMode::default() {
|
||||
layer = true;
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(self.alpha_blending.blend_mode.into(), peniko::Compose::SrcOver),
|
||||
self.alpha_blending.opacity,
|
||||
kurbo::Affine::IDENTITY,
|
||||
kurbo::Affine::new((path_transform).to_cols_array()),
|
||||
&kurbo::Rect::new(transformed_bounds[0].x, transformed_bounds[0].y, transformed_bounds[1].x, transformed_bounds[1].y),
|
||||
);
|
||||
}
|
||||
|
||||
let kurbo_transform = kurbo::Affine::new(transform.to_cols_array());
|
||||
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
|
||||
let mut path = kurbo::BezPath::new();
|
||||
for subpath in self.stroke_bezier_paths() {
|
||||
subpath.to_vello_path(self.transform, &mut path);
|
||||
subpath.to_vello_path(stroke_transform, &mut path);
|
||||
}
|
||||
|
||||
match self.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
|
@ -472,7 +479,7 @@ impl GraphicElementRendered for VectorData {
|
|||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::None => (),
|
||||
};
|
||||
|
@ -504,7 +511,7 @@ impl GraphicElementRendered for VectorData {
|
|||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo_transform, color, None, &path);
|
||||
scene.stroke(&stroke, kurbo::Affine::new(path_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
if layer {
|
||||
|
@ -596,7 +603,8 @@ impl GraphicElementRendered for Artboard {
|
|||
|
||||
// Render background
|
||||
let color = peniko::Color::rgba(self.background.r() as f64, self.background.g() as f64, self.background.b() as f64, self.background.a() as f64);
|
||||
let rect = kurbo::Rect::new(self.location.x as f64, self.location.y as f64, self.dimensions.x as f64, self.dimensions.y as f64);
|
||||
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
|
||||
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
|
@ -606,7 +614,8 @@ impl GraphicElementRendered for Artboard {
|
|||
if self.clip {
|
||||
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
}
|
||||
self.graphic_group.render_to_vello(scene, transform, context);
|
||||
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform;
|
||||
self.graphic_group.render_to_vello(scene, child_transform, context);
|
||||
if self.clip {
|
||||
scene.pop_layer();
|
||||
}
|
||||
|
@ -614,8 +623,10 @@ impl GraphicElementRendered for Artboard {
|
|||
|
||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
subpath.apply_transform(self.graphic_group.transform.inverse());
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
if self.graphic_group.transform.matrix2.determinant() != 0. {
|
||||
subpath.apply_transform(self.graphic_group.transform.inverse());
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
|
|
|
@ -145,10 +145,9 @@ impl Gradient {
|
|||
}
|
||||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
let updated_transform = multiplied_transform * bound_transform;
|
||||
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
|
@ -168,7 +167,7 @@ impl Gradient {
|
|||
} else {
|
||||
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
|
||||
};
|
||||
let mod_points = updated_transform;
|
||||
let mod_points = element_transform * stroke_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(self.start);
|
||||
let end = mod_points.transform_point2(self.end);
|
||||
|
@ -301,7 +300,7 @@ impl Fill {
|
|||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
|
@ -312,7 +311,7 @@ impl Fill {
|
|||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
|
@ -450,6 +449,10 @@ impl Display for LineJoin {
|
|||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct Stroke {
|
||||
|
@ -462,6 +465,8 @@ pub struct Stroke {
|
|||
pub line_cap: LineCap,
|
||||
pub line_join: LineJoin,
|
||||
pub line_join_miter_limit: f64,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Stroke {
|
||||
|
@ -487,6 +492,7 @@ impl Stroke {
|
|||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,6 +505,10 @@ impl Stroke {
|
|||
line_cap: if time < 0.5 { self.line_cap } else { other.line_cap },
|
||||
line_join: if time < 0.5 { self.line_join } else { other.line_join },
|
||||
line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time,
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,6 +645,7 @@ impl Default for Stroke {
|
|||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -787,15 +798,15 @@ impl PathStyle {
|
|||
}
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match view_mode {
|
||||
ViewMode::Outline => {
|
||||
let fill_attribute = Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let stroke_attribute = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
_ => {
|
||||
let fill_attribute = self.fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ fn set_vector_data_stroke(
|
|||
line_cap,
|
||||
line_join,
|
||||
line_join_miter_limit: miter_limit,
|
||||
transform: vector_data.transform,
|
||||
});
|
||||
vector_data
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use graph_craft::document::NodeNetwork;
|
||||
#[cfg(any(feature = "criterion", feature = "iai"))]
|
||||
use graph_craft::{document::NodeNetwork, graphene_compiler::Compiler, proto::ProtoNetwork};
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
#[cfg(any(feature = "criterion", feature = "iai"))]
|
||||
use graph_craft::proto::ProtoNetwork;
|
||||
|
||||
#[cfg(feature = "criterion")]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
#[cfg(all(not(feature = "criterion"), feature = "iai"))]
|
||||
use iai_callgrind::{black_box, library_benchmark, library_benchmark_group, main};
|
||||
|
||||
|
@ -18,7 +20,6 @@ fn compile(network: NodeNetwork) -> ProtoNetwork {
|
|||
let compiler = Compiler {};
|
||||
compiler.compile_single(network).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "criterion"), feature = "iai"))]
|
||||
fn load_from_name(name: &str) -> NodeNetwork {
|
||||
let content = std::fs::read(&format!("../../demo-artwork/{name}.graphite")).expect("failed to read file");
|
||||
|
@ -27,7 +28,6 @@ fn load_from_name(name: &str) -> NodeNetwork {
|
|||
black_box(compile(black_box(network)));
|
||||
load_network(content)
|
||||
}
|
||||
|
||||
#[cfg(feature = "criterion")]
|
||||
fn compile_to_proto(c: &mut Criterion) {
|
||||
let artworks = glob::glob("../../demo-artwork/*.graphite").expect("failed to read glob pattern");
|
||||
|
@ -42,17 +42,15 @@ fn compile_to_proto(c: &mut Criterion) {
|
|||
|
||||
#[cfg_attr(all(feature = "iai", not(feature = "criterion")), library_benchmark)]
|
||||
#[cfg_attr(all(feature = "iai", not(feature="criterion")), benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "red-dress", "valley-of-spires"], setup = load_from_name))]
|
||||
#[cfg(all(not(feature = "criterion"), feature = "iai"))]
|
||||
pub fn iai_compile_to_proto(input: NodeNetwork) {
|
||||
black_box(compile(input));
|
||||
pub fn iai_compile_to_proto(_input: NodeNetwork) {
|
||||
#[cfg(all(feature = "iai", not(feature = "criterion")))]
|
||||
black_box(compile(_input));
|
||||
}
|
||||
|
||||
#[cfg(feature = "criterion")]
|
||||
criterion_group!(benches, compile_to_proto);
|
||||
|
||||
#[cfg(feature = "criterion")]
|
||||
criterion_main!(benches);
|
||||
|
||||
#[cfg(all(not(feature = "criterion"), feature = "iai"))]
|
||||
library_benchmark_group!(name = compile_group; benchmarks = iai_compile_to_proto);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue