Add viewing/editing layer names, add Blend Mode node, and clean up Layer node (#1489)

This commit is contained in:
Keavon Chambers 2023-12-07 15:10:47 -08:00 committed by GitHub
parent b7e304a708
commit 60a9c27bf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 437 additions and 463 deletions

View file

@ -19,6 +19,7 @@ pub mod renderer;
pub struct GraphicGroup {
elements: Vec<GraphicElement>,
pub opacity: f32,
pub blend_mode: BlendMode,
pub transform: DAffine2,
}
@ -41,29 +42,16 @@ pub enum GraphicElementData {
Artboard(Artboard),
}
/// A named [`GraphicElementData`] with a blend mode, opacity, as well as visibility, locked, and collapsed states.
// TODO: Remove this wrapper and directly use GraphicElementData
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicElement {
pub name: String,
pub blend_mode: BlendMode,
/// In range 0..=1
pub opacity: f32,
pub visible: bool,
pub locked: bool,
pub collapsed: bool,
pub graphic_element_data: GraphicElementData,
}
impl Default for GraphicElement {
fn default() -> Self {
Self {
name: "".to_owned(),
blend_mode: BlendMode::Normal,
opacity: 1.,
visible: true,
locked: false,
collapsed: false,
graphic_element_data: GraphicElementData::VectorShape(Box::new(VectorData::empty())),
}
}
@ -93,14 +81,8 @@ impl Artboard {
}
}
pub struct ConstructLayerNode<GraphicElementData, Name, BlendMode, Opacity, Visible, Locked, Collapsed, Stack> {
pub struct ConstructLayerNode<GraphicElementData, Stack> {
graphic_element_data: GraphicElementData,
name: Name,
blend_mode: BlendMode,
opacity: Opacity,
visible: Visible,
locked: Locked,
collapsed: Collapsed,
stack: Stack,
}
@ -108,23 +90,11 @@ pub struct ConstructLayerNode<GraphicElementData, Name, BlendMode, Opacity, Visi
async fn construct_layer<Data: Into<GraphicElementData>, Fut1: Future<Output = Data>, Fut2: Future<Output = GraphicGroup>>(
footprint: crate::transform::Footprint,
graphic_element_data: impl Node<crate::transform::Footprint, Output = Fut1>,
name: String,
blend_mode: BlendMode,
opacity: f32,
visible: bool,
locked: bool,
collapsed: bool,
mut stack: impl Node<crate::transform::Footprint, Output = Fut2>,
) -> GraphicGroup {
let graphic_element_data = self.graphic_element_data.eval(footprint).await;
let mut stack = self.stack.eval(footprint).await;
stack.push(GraphicElement {
name,
blend_mode,
opacity: opacity / 100.,
visible,
locked,
collapsed,
graphic_element_data: graphic_element_data.into(),
});
stack
@ -154,7 +124,7 @@ async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
background: Color,
clip: bool,
) -> Artboard {
footprint.transform = footprint.transform * DAffine2::from_translation(location.as_dvec2());
footprint.transform *= DAffine2::from_translation(location.as_dvec2());
let graphic_group = self.contents.eval(footprint).await;
Artboard {
graphic_group,
@ -212,13 +182,11 @@ where
T: ToGraphicElement,
{
fn from(value: T) -> Self {
let element = GraphicElement {
graphic_element_data: value.into(),
..Default::default()
};
let element = GraphicElement { graphic_element_data: value.into() };
Self {
elements: (vec![element]),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
}
}
@ -228,6 +196,7 @@ impl GraphicGroup {
pub const EMPTY: Self = Self {
elements: Vec::new(),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
};
@ -337,12 +306,6 @@ impl GraphicElement {
impl core::hash::Hash for GraphicElement {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.blend_mode.hash(state);
self.opacity.to_bits().hash(state);
self.visible.hash(state);
self.locked.hash(state);
self.collapsed.hash(state);
self.graphic_element_data.hash(state);
}
}

View file

@ -59,8 +59,6 @@ pub struct SvgRender {
pub svg: SvgSegmentList,
pub svg_defs: String,
pub transform: DAffine2,
pub opacity: f32,
pub blend_mode: BlendMode,
pub image_data: Vec<(u64, Image<Color>)>,
indent: usize,
}
@ -71,8 +69,6 @@ impl SvgRender {
svg: SvgSegmentList::default(),
svg_defs: String::new(),
transform: DAffine2::IDENTITY,
opacity: 1.,
blend_mode: BlendMode::Normal,
image_data: Vec::new(),
indent: 0,
}
@ -121,6 +117,7 @@ impl SvgRender {
self.indent();
self.svg.push("<");
self.svg.push(name.clone());
// Wraps `self` in a newtype (1-tuple) which is then mutated by the `attributes` closure
attributes(&mut SvgRenderAttrs(self));
self.svg.push(">");
let length = self.svg.len();
@ -183,6 +180,7 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
result.push(')');
result
}
fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
@ -204,6 +202,7 @@ pub trait GraphicElementRendered {
let tree = usvg::Tree::from_str(&svg, &opt).expect("Failed to parse SVG");
tree.root.clone()
}
fn to_usvg_tree(&self, resolution: glam::UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
let root_node = self.to_usvg_node();
usvg::Tree {
@ -219,26 +218,33 @@ pub trait GraphicElementRendered {
impl GraphicElementRendered for GraphicGroup {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let old_opacity = render.opacity;
render.opacity *= self.opacity;
render.parent_tag(
"g",
|attributes| attributes.push("transform", format_transform_matrix(self.transform)),
|attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
if self.opacity < 1. {
attributes.push("opacity", self.opacity.to_string());
}
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
},
|render| {
for element in self.iter() {
render.blend_mode = element.blend_mode;
element.graphic_element_data.render_svg(render, render_params);
}
},
);
render.opacity = old_opacity;
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.iter()
.filter_map(|element| element.graphic_element_data.bounding_box(transform * self.transform))
.reduce(Quad::combine_bounds)
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
fn to_usvg_node(&self) -> usvg::Node {
@ -260,24 +266,31 @@ impl GraphicElementRendered for VectorData {
for subpath in &self.subpaths {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
}
render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data");
attributes.push("d", path);
let render = &mut attributes.0;
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
attributes.push_val(style);
if attributes.0.blend_mode != BlendMode::default() {
attributes.push_complex("style", |v| {
v.svg.push("mix-blend-mode: ");
v.svg.push(v.blend_mode.to_svg_style_name());
v.svg.push(";");
})
let fill_and_stroke = self
.style
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
attributes.push_val(fill_and_stroke);
if self.style.opacity < 1. {
attributes.push("opacity", self.style.opacity.to_string());
}
if self.style.blend_mode != BlendMode::default() {
attributes.push("style", self.style.blend_mode.render());
}
});
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform)
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
let update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| {
@ -345,19 +358,24 @@ impl GraphicElementRendered for Artboard {
attributes.push("font-size", "14px");
},
|render| {
// TODO: Use the artboard's layer name
render.svg.push("Artboard");
},
);
// Contents group
// Contents group (includes the artwork but not the background)
render.parent_tag(
// SVG group tag
"g",
// Group tag attributes
|attributes| {
attributes.push("class", "artboard");
attributes.push(
"transform",
format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform),
);
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
@ -373,19 +391,15 @@ impl GraphicElementRendered for Artboard {
attributes.push("clip-path", selector);
}
},
// Artboard contents
|render| {
let old_opacity = render.opacity;
render.opacity *= self.graphic_group.opacity;
// Contents
for element in self.graphic_group.iter() {
render.blend_mode = element.blend_mode;
element.graphic_element_data.render_svg(render, render_params);
}
render.opacity = old_opacity;
},
);
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
@ -394,6 +408,7 @@ impl GraphicElementRendered for Artboard {
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
click_targets.push(ClickTarget { stroke_width: 0., subpath });
@ -412,7 +427,10 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid))
attributes.push("href", SvgSegment::BlobUrl(uuid));
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
});
render.image_data.push((uuid, self.image.clone()))
}
@ -429,11 +447,13 @@ impl GraphicElementRendered for ImageFrame<Color> {
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", base64_string)
attributes.push("href", base64_string);
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
}
});
}
ImageRenderMode::Canvas => {
@ -441,10 +461,12 @@ impl GraphicElementRendered for ImageFrame<Color> {
}
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = self.transform * transform;
(transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget { subpath, stroke_width: 0. });
@ -563,6 +585,7 @@ impl GraphicElementRendered for Option<Color> {
render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color"));
return;
};
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb());
render.leaf_tag("rect", |attributes| {
attributes.push("width", "100");
@ -570,7 +593,6 @@ impl GraphicElementRendered for Option<Color> {
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
});
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb());
render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info))
}

View file

@ -5,6 +5,7 @@ pub struct LogToConsoleNode;
#[node_macro::node_fn(LogToConsoleNode)]
fn log_to_console<T: core::fmt::Debug>(value: T) -> T {
#[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
debug!("{value:#?}");
value
}

View file

@ -90,15 +90,15 @@ pub enum BlendMode {
// Not supported by SVG, but we should someday support: Dissolve
// Darken group
Multiply,
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Screen,
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
@ -172,6 +172,7 @@ impl core::fmt::Display for BlendMode {
}
}
}
impl BlendMode {
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
@ -206,6 +207,11 @@ impl BlendMode {
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(r#" mix-blend-mode: {};"#, self.to_svg_style_name())
}
/// List of all the blend modes in their conventional ordering and grouping.
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
[
@ -898,32 +904,55 @@ pub struct OpacityNode<O> {
}
#[node_macro::node_fn(OpacityNode)]
fn image_opacity(color: Color, opacity_multiplier: f32) -> Color {
fn opacity_node(color: Color, opacity_multiplier: f32) -> Color {
let opacity_multiplier = opacity_multiplier / 100.;
Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier)
}
#[node_macro::node_impl(OpacityNode)]
fn image_opacity(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
fn opacity_node(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
let opacity_multiplier = opacity_multiplier / 100.;
vector_data.style.opacity *= opacity_multiplier;
vector_data
}
#[node_macro::node_impl(OpacityNode)]
fn image_opacity(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
fn opacity_node(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
let opacity_multiplier = opacity_multiplier / 100.;
graphic_group.opacity *= opacity_multiplier;
graphic_group
}
#[derive(Debug, Clone, Copy)]
pub struct BlendModeNode<BM> {
blend_mode: BM,
}
#[node_macro::node_fn(BlendModeNode)]
fn blend_mode_node(mut vector_data: VectorData, blend_mode: BlendMode) -> VectorData {
vector_data.style.blend_mode = blend_mode;
vector_data
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut graphic_group: GraphicGroup, blend_mode: BlendMode) -> GraphicGroup {
graphic_group.blend_mode = blend_mode;
graphic_group
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut image_frame: ImageFrame<Color>, blend_mode: BlendMode) -> ImageFrame<Color> {
image_frame.blend_mode = blend_mode;
image_frame
}
#[derive(Debug, Clone, Copy)]
pub struct PosterizeNode<P> {
posterize_value: P,
}
// Based on http://www.axiomx.com/posterize.htm
// This algorithm is perfectly accurate.
// This algorithm produces fully accurate output in relation to the industry standard.
#[node_macro::node_fn(PosterizeNode)]
fn posterize(color: Color, posterize_value: f32) -> Color {
let color = color.to_gamma_srgb();

View file

@ -250,7 +250,6 @@ fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
// The transform that maps image space to layer space.
//
// Image space is unitless [0, 1] for both axes, with x axis positive
@ -261,6 +260,7 @@ pub struct ImageFrame<P: Pixel> {
// positive going right and y axis positive going down, with the origin
// being an unspecified quantity.
pub transform: DAffine2,
pub blend_mode: BlendMode,
}
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
@ -312,6 +312,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::ZERO,
blend_mode: BlendMode::Normal,
}
}
@ -319,6 +320,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
}
}
@ -379,6 +381,7 @@ impl From<ImageFrame<Color>> for ImageFrame<SRGBA8> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
}
}
}
@ -393,6 +396,7 @@ impl From<ImageFrame<SRGBA8>> for ImageFrame<Color> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
}
}
}

View file

@ -106,8 +106,8 @@ impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
#[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval");
self.0.clone()

View file

@ -99,6 +99,7 @@ fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData {
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
#[derive(Debug, Clone)]
pub struct PathGenerator<Mirror> {
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
mirror: Mirror,
}

View file

@ -1,6 +1,7 @@
//! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::raster::BlendMode;
use crate::Color;
use dyn_any::{DynAny, StaticType};
@ -12,9 +13,9 @@ use std::fmt::{self, Display, Write};
/// A value of 3 would correspond to a precision of 10^-3.
const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
fn format_opacity(attribute: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {name}-opacity="{opacity:.OPACITY_PRECISION$}""#)
format!(r#" {attribute}="{opacity:.OPACITY_PRECISION$}""#)
} else {
String::new()
}
@ -64,15 +65,15 @@ impl Gradient {
}
}
/// Adds the gradient def, returning the gradient id
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> u64 {
/// 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 {
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 mut positions = String::new();
for (position, color) in self.positions.iter().filter_map(|(pos, color)| color.map(|color| (pos, color))) {
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a() * opacity).rgba_hex());
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
}
let mod_gradient = transformed_bound_transform.inverse();
@ -178,13 +179,13 @@ impl Fill {
}
}
/// Renders the fill, adding necessary defs.
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> String {
/// 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 {
match self {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a() * opacity)),
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill-opacity", color.a())),
Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds, opacity);
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{gradient_id}')""##)
}
}
@ -326,12 +327,12 @@ impl Stroke {
}
/// Provide the SVG attributes for the stroke.
pub fn render(&self, opacity: f32) -> String {
pub fn render(&self) -> String {
if let Some(color) = self.color {
format!(
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
color.rgb_hex(),
format_opacity("stroke", opacity * color.a()),
format_opacity("stroke-opacity", color.a()),
self.weight,
self.dash_lengths(),
self.dash_offset,
@ -410,6 +411,7 @@ pub struct PathStyle {
stroke: Option<Stroke>,
fill: Fill,
pub opacity: f32,
pub blend_mode: BlendMode,
}
impl core::hash::Hash for PathStyle {
@ -417,12 +419,18 @@ impl core::hash::Hash for PathStyle {
self.stroke.hash(state);
self.fill.hash(state);
self.opacity.to_bits().hash(state);
self.blend_mode.hash(state);
}
}
impl PathStyle {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self { stroke, fill, opacity: 1. }
Self {
stroke,
fill,
opacity: 1.,
blend_mode: BlendMode::Normal,
}
}
/// Get the current path's [Fill].
@ -529,18 +537,20 @@ impl PathStyle {
self.stroke = None;
}
/// 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 {
let fill_attribute = match (view_mode, &self.fill) {
(ViewMode::Outline, _) => Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity),
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity),
};
let stroke_attribute = match (view_mode, &self.stroke) {
(ViewMode::Outline, _) => Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(self.opacity),
(_, Some(stroke)) => stroke.render(self.opacity),
(_, None) => String::new(),
};
format!("{fill_attribute}{stroke_attribute}")
match view_mode {
ViewMode::Outline => {
let fill_attribute = Fill::None.render(svg_defs, multiplied_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 stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
format!("{fill_attribute}{stroke_attribute}")
}
}
}
}

View file

@ -15,6 +15,7 @@ pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
pub mirror_angle: Vec<ManipulatorGroupId>,
}
@ -47,12 +48,12 @@ impl VectorData {
self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
}
/// Construct some new vector data from a single subpath with an identy transform and black fill.
/// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
Self::from_subpaths(vec![subpath])
}
/// Construct some new vector data from subpaths with an identy transform and black fill.
/// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
super::VectorData { subpaths, ..Self::empty() }
}