Improve rendered SVG output syntax for better compatibility and terseness (#1880)

* Improve rendered SVG output syntax for better compatibility and terseness

* Fix CI failing on boolean operations without wasm32?

* Attempt 2
This commit is contained in:
Keavon Chambers 2024-07-30 08:28:49 -07:00 committed by GitHub
parent 22ebe9a2cb
commit 44ffb635e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 249 additions and 189 deletions

File diff suppressed because one or more lines are too long

View file

@ -128,7 +128,7 @@ impl LayoutHolder for PathTool {
let related_seperator = Separator::new(SeparatorType::Related).widget_holder();
let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder();
let colinear_handles_tooltip = "Ensures both handles remain 180° apart";
let colinear_handles_tooltip = "Keep both handles unbent, each 180° apart, when moving either";
let colinear_handles_state = manipulator_angle.and_then(|angle| match angle {
ManipulatorAngle::Colinear => Some(true),
ManipulatorAngle::Free => Some(false),

View file

@ -677,10 +677,7 @@ impl Fsm for PenToolFsmState {
]),
HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]),
HintGroup(vec![
HintInfo::mouse(MouseMotion::Lmb, ""),
HintInfo::mouse(MouseMotion::LmbDrag, "Bend from Prev. Point").prepend_slash(),
]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]),
]),
PenToolFsmState::DraggingHandle => HintData(vec![
HintGroup(vec![

View file

@ -20,6 +20,7 @@ use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData;
use graphene_core::{Color, GraphicElement, SurfaceFrame};
use graphene_std::renderer::format_transform_matrix;
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypes};
@ -657,18 +658,11 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::RenderRulers);
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::CanvasFrame(frame)) => {
// Send to frontend
let matrix = frame
.transform
.to_cols_array()
.iter()
.enumerate()
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
let svg = format!(
r#"
<svg><foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>
"#,
frame.resolution.x, frame.resolution.y, matrix, frame.surface_id.0
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
responses.add(DocumentMessage::RenderScrollbars);

View file

@ -34,53 +34,56 @@
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={self}
on:focus
on:blur
on:fullscreenchange
on:fullscreenerror
on:scroll
on:cut
on:copy
on:paste
on:keydown
on:keypress
on:keyup
on:auxclick
on:blur
on:click
on:contextmenu
on:dblclick
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseover
on:mouseout
on:mouseup
on:select
on:drag
on:dragend
on:dragenter
on:dragstart
on:dragleave
on:dragover
on:drop
on:touchcancel
on:touchend
on:pointerover
on:pointerenter
on:dragstart
on:mouseup
on:pointerdown
on:pointermove
on:pointerup
on:pointercancel
on:pointerout
on:pointerenter
on:pointerleave
on:gotpointercapture
on:lostpointercapture
on:scroll
{...$$restProps}
>
<slot />
</div>
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
on:contextmenu
on:copy
on:cut
on:drag
on:dragenter
on:drop
on:focus
on:fullscreenchange
on:fullscreenerror
on:gotpointercapture
on:keydown
on:keypress
on:keyup
on:lostpointercapture
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseout
on:mouseover
on:paste
on:pointercancel
on:pointermove
on:pointerout
on:pointerover
on:pointerup
on:select
on:touchcancel
on:touchend
-->
<style lang="scss" global>
.layout-col {
display: flex;

View file

@ -34,53 +34,56 @@
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={self}
on:focus
on:blur
on:fullscreenchange
on:fullscreenerror
on:scroll
on:cut
on:copy
on:paste
on:keydown
on:keypress
on:keyup
on:auxclick
on:blur
on:click
on:contextmenu
on:dblclick
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseover
on:mouseout
on:mouseup
on:select
on:drag
on:dragend
on:dragenter
on:dragstart
on:dragleave
on:dragover
on:drop
on:touchcancel
on:touchend
on:pointerover
on:pointerenter
on:dragstart
on:mouseup
on:pointerdown
on:pointermove
on:pointerup
on:pointercancel
on:pointerout
on:pointerenter
on:pointerleave
on:gotpointercapture
on:lostpointercapture
on:scroll
{...$$restProps}
>
<slot />
</div>
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
on:contextmenu
on:copy
on:cut
on:drag
on:dragenter
on:drop
on:focus
on:fullscreenchange
on:fullscreenerror
on:gotpointercapture
on:keydown
on:keypress
on:keyup
on:lostpointercapture
on:mousedown
on:mouseenter
on:mouseleave
on:mousemove
on:mouseout
on:mouseover
on:paste
on:pointercancel
on:pointermove
on:pointerout
on:pointerover
on:pointerup
on:select
on:touchcancel
on:touchend
-->
<style lang="scss" global>
.layout-row {
display: flex;

View file

@ -1,4 +1,5 @@
mod quad;
pub use quad::Quad;
use crate::raster::bbox::Bbox;
use crate::raster::{BlendMode, Image, ImageFrame};
@ -8,12 +9,13 @@ use crate::vector::style::{Fill, Stroke, ViewMode};
use crate::vector::PointId;
use crate::SurfaceFrame;
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
pub use quad::Quad;
use bezier_rs::Subpath;
use base64::Engine;
use glam::{DAffine2, DVec2};
use num_traits::Zero;
use std::fmt::Write;
#[cfg(feature = "vello")]
use vello::*;
@ -107,11 +109,10 @@ impl SvgRender {
.map(|size| format!("viewbox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
.unwrap_or_default();
let svg_header = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g transform="{}">"#,
view_box,
format_transform_matrix(transform)
);
let matrix = format_transform_matrix(transform);
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g{transform}>"#, view_box);
self.svg.insert(0, svg_header.into());
self.svg.push("</g></svg>".into());
}
@ -193,17 +194,16 @@ impl RenderParams {
}
pub fn format_transform_matrix(transform: DAffine2) -> String {
use std::fmt::Write;
let mut result = "matrix(".to_string();
let cols = transform.to_cols_array();
for (index, item) in cols.iter().enumerate() {
write!(result, "{item}").unwrap();
if index != cols.len() - 1 {
result.push_str(", ");
}
if transform == DAffine2::IDENTITY {
return String::new();
}
result.push(')');
result
transform.to_cols_array().iter().enumerate().fold("matrix(".to_string(), |val, (i, num)| {
let num = if num.abs() < 1_000_000_000. { (num * 1_000_000_000.).round() / 1_000_000_000. } else { *num };
let num = if num.is_zero() { "0".to_string() } else { num.to_string() };
let comma = if i == 5 { "" } else { "," };
val + &(num + comma)
}) + ")"
}
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
@ -234,7 +234,10 @@ impl GraphicElementRendered for GraphicGroup {
render.parent_tag(
"g",
|attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
let matrix = format_transform_matrix(self.transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
@ -305,8 +308,6 @@ impl GraphicElementRendered for VectorData {
}
render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data");
attributes.push("d", path);
let fill_and_stroke = self
@ -443,8 +444,10 @@ impl GraphicElementRendered for Artboard {
if !render_params.hide_artboards {
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("class", "artboard-bg");
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
attributes.push("fill", format!("#{}", self.background.rgb_hex()));
if self.background.a() < 1. {
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
}
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
attributes.push("width", self.dimensions.x.abs().to_string());
@ -456,7 +459,6 @@ impl GraphicElementRendered for Artboard {
render.parent_tag(
"text",
|attributes| {
attributes.push("class", "artboard-label");
attributes.push("fill", "white");
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
@ -475,23 +477,22 @@ impl GraphicElementRendered for Artboard {
"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),
);
let matrix = format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
use std::fmt::Write;
let matrix = format_transform_matrix(self.graphic_group.transform.inverse());
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" transform="{}"/></clipPath>"##,
self.dimensions.x,
self.dimensions.y,
format_transform_matrix(self.graphic_group.transform.inverse())
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}"{transform} /></clipPath>"##,
self.dimensions.x, self.dimensions.y
)
.unwrap();
attributes.push("clip-path", selector);
@ -580,18 +581,16 @@ impl GraphicElementRendered for crate::ArtboardGroup {
impl GraphicElementRendered for SurfaceFrame {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
let transform = self.transform;
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
.to_cols_array()
.iter()
.enumerate()
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
let matrix = format_transform_matrix(transform * DAffine2::from_scale((width, height).into()).inverse());
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
let canvas = format!(
r#"<foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
r#"<foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
width.abs(),
height.abs(),
matrix,
self.surface_id
);
render.svg.push(canvas.into())
@ -617,7 +616,7 @@ impl GraphicElementRendered for SurfaceFrame {
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
let transform = self.transform * render.transform;
match render_params.image_render_mode {
ImageRenderMode::Base64 => {
@ -638,8 +637,11 @@ impl GraphicElementRendered for ImageFrame<Color> {
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);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
@ -765,7 +767,10 @@ impl GraphicElementRendered for Option<Color> {
attributes.push("width", "100");
attributes.push("height", "100");
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
attributes.push("fill", format!("#{}", color.rgb_hex()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
});
render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info))
}
@ -785,7 +790,10 @@ impl GraphicElementRendered for Vec<Color> {
attributes.push("height", "100");
attributes.push("x", (index * 120).to_string());
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
attributes.push("fill", format!("#{}", color.rgb_hex()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
});
}
}

View file

@ -1,20 +1,18 @@
use core::hash::Hash;
use half::f16;
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use super::{Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB};
use dyn_any::{DynAny, StaticType};
#[cfg(feature = "serde")]
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::Euclid;
use bytemuck::{Pod, Zeroable};
use core::hash::Hash;
use half::f16;
use std::fmt::Write;
use super::{
discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float},
Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB,
};
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable)]
@ -813,12 +811,12 @@ impl Color {
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240A261", color.rgba_hex())
/// assert_eq!("3240a261", color.rgba_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgba_hex(&self) -> String {
format!(
"{:02X?}{:02X?}{:02X?}{:02X?}",
"{:02x?}{:02x?}{:02x?}{:02x?}",
(self.r() * 255.) as u8,
(self.g() * 255.) as u8,
(self.b() * 255.) as u8,
@ -826,15 +824,34 @@ impl Color {
)
}
/// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1.
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240a261", color1.rgb_optional_a_hex())
/// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240a2", color2.rgb_optional_a_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgb_optional_a_hex(&self) -> String {
let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8);
if self.a() < 1. {
let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8);
}
result
}
/// Return a 6-character RGB hex string (without a # prefix).
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240A2", color.rgb_hex())
/// assert_eq!("3240a2", color.rgb_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgb_hex(&self) -> String {
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
}
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha.

View file

@ -1,25 +1,13 @@
//! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::renderer::format_transform_matrix;
use crate::Color;
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use std::fmt::{self, Display, Write};
/// Precision of the opacity value in digits after the decimal point.
/// A value of 3 would correspond to a precision of 10^-3.
const OPACITY_PRECISION: usize = 3;
fn format_opacity(attribute: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {attribute}="{opacity:.OPACITY_PRECISION$}""#)
} else {
String::new()
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub enum GradientType {
#[default]
@ -134,7 +122,15 @@ impl Gradient {
let mut stop = String::new();
for (position, color) in self.stops.0.iter() {
let _ = write!(stop, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
stop.push_str("<stop");
if *position != 0. {
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
}
let _ = write!(stop, r##" stop-color="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
stop.push_str(" />")
}
let mod_gradient = transformed_bound_transform.inverse();
@ -143,28 +139,25 @@ impl Gradient {
let start = mod_points.transform_point2(self.start);
let end = mod_points.transform_point2(self.end);
let transform = mod_gradient
.to_cols_array()
.iter()
.enumerate()
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
.collect::<String>();
let gradient_id = crate::uuid::generate_uuid();
let matrix = format_transform_matrix(mod_gradient);
let gradient_transform = if matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{}""#, matrix) };
match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, transform, stop
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, stop
);
}
GradientType::Radial => {
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, transform, stop
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, stop
);
}
}
@ -277,7 +270,13 @@ impl Fill {
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-opacity", color.a())),
Self::Solid(color) => {
let mut result = format!(r##" fill="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{gradient_id}')""##)
@ -505,21 +504,45 @@ impl Stroke {
/// Provide the SVG attributes for the stroke.
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()),
self.weight,
self.dash_lengths(),
self.dash_offset,
self.line_cap,
self.line_join,
self.line_join_miter_limit
)
} else {
String::new()
// Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if self.weight <= 0. || color.a() == 0. {
return String::new();
}
// Set to None if the value is the SVG default
let weight = (self.weight != 1.).then_some(self.weight);
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
let line_cap = (self.line_cap != LineCap::Butt).then_some(self.line_cap);
let line_join = (self.line_join != LineJoin::Miter).then_some(self.line_join);
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
// Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
if let Some(weight) = weight {
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
}
if let Some(dash_array) = dash_array {
let _ = write!(&mut attributes, r#" stroke-dasharray="{}""#, dash_array);
}
if let Some(dash_offset) = dash_offset {
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
}
if let Some(line_cap) = line_cap {
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap);
}
if let Some(line_join) = line_join {
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join);
}
if let Some(line_join_miter_limit) = line_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
}
attributes
}
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {

View file

@ -279,6 +279,7 @@ pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
subpaths
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(module = "/../../frontend/src/utility-functions/computational-geometry.ts")]
extern "C" {
#[wasm_bindgen(js_name = booleanUnion)]
@ -290,3 +291,19 @@ extern "C" {
#[wasm_bindgen(js_name = booleanDifference)]
fn boolean_difference(path1: String, path2: String) -> String;
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_union(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_subtract(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_intersect(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_difference(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}

View file

@ -93,7 +93,10 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
attributes.push("y", "0");
attributes.push("width", footprint.resolution.x.to_string());
attributes.push("height", footprint.resolution.y.to_string());
attributes.push("transform", format_transform_matrix(footprint.transform.inverse()));
let matrix = format_transform_matrix(footprint.transform.inverse());
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
attributes.push("fill", "white");
});
}

View file

@ -5,13 +5,8 @@ title = "Picking a task"
order = 2 # Page number after chapter intro
+++
The [task board](https://github.com/orgs/GraphiteEditor/projects/1/views/1) provides a list of [available tasks](https://github.com/orgs/GraphiteEditor/projects/1/views/5), as well as a [beginner-friendly](https://github.com/orgs/GraphiteEditor/projects/1/views/6) subset.
The [task board](https://github.com/orgs/GraphiteEditor/projects/1/views/1) provides a list of [available tasks](https://github.com/orgs/GraphiteEditor/projects/1/views/5), as well as a [beginner-friendly](https://github.com/orgs/GraphiteEditor/projects/1/views/6) subset. Issues partially or fully involving web development can also be seen [listed here](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3AWeb+-label%3ARust) which may involve HTML/CSS/TypeScript/Svelte, although depending on the task, it may also involve Rust (which can be a good way to get gently introduced to the language if you come from a web background).
If you have Rust and/or web experience, you may also pick based on:
- [Only Rust](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3ARust+-label%3AWeb) tasks
- [Only web](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3AWeb+-label%3ARust) tasks (HTML/CSS/TypeScript/Svelte)
- [Combined Rust and web](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3ARust+label%3AWeb) tasks
Writing new documentation by commenting existing code is another valuable way to contribute as you learn from reading code.
Feel free to pick whatever task interests you, then comment on the issue that you would like to start. After commenting, you can dig in right away, then we will assign the issue to your GitHub user to keep the work status of tasks organized.
Writing new documentation by commenting existing code is another valuable way to contribute as you learn.
Feel free to pick whatever task interests you, then comment on the issue that you would like to start. After commenting, you can dig in right away, then we will assign the issue to you once you have a PR ready. (Always remembering to leave a comment is important, since GitHub doesn't allow assigning issues to people who haven't commented on them.)