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:
James Lindsay 2024-09-15 23:26:11 +01:00 committed by GitHub
parent 2fa8773092
commit dd4a97b09f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 152 additions and 59 deletions

View file

@ -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"],

View file

@ -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")

View file

@ -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();

View file

@ -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
}

View file

@ -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);

View file

@ -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))

View file

@ -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;

View file

@ -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
}

View file

@ -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
}

View file

@ -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;

View file

@ -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

View file

@ -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

View 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

View file

@ -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 },

View file

@ -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
}

View file

@ -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 {

View file

@ -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}")
}

View file

@ -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
}

View file

@ -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);