mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 09:38:13 +08:00
SVG import (#1579)
* SVG import * Fix error * Transforms * Code review nits --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
8eef96511e
commit
002151d9c0
@@ -87,6 +87,10 @@ pub enum DocumentMessage {
|
||||
image: Image<Color>,
|
||||
mouse: Option<(f64, f64)>,
|
||||
},
|
||||
PasteSvg {
|
||||
svg: String,
|
||||
mouse: Option<(f64, f64)>,
|
||||
},
|
||||
Redo,
|
||||
RenameDocument {
|
||||
new_name: String,
|
||||
|
||||
@@ -602,6 +602,14 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
// Force chosen tool to be Select Tool after importing image.
|
||||
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
|
||||
}
|
||||
PasteSvg { svg, mouse } => {
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
let viewport_location = mouse.map_or(ipp.viewport_bounds.center() + ipp.viewport_bounds.top_left, |pos| pos.into());
|
||||
let center_in_viewport = DAffine2::from_translation(self.metadata().document_to_viewport.inverse().transform_point2(viewport_location - ipp.viewport_bounds.top_left));
|
||||
let layer = graph_modification_utils::new_svg_layer(svg, center_in_viewport, NodeId(generate_uuid()), self.new_layer_parent(), responses);
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
|
||||
}
|
||||
Redo => {
|
||||
responses.add(SelectToolMessage::Abort);
|
||||
responses.add(DocumentMessage::DocumentHistoryForward);
|
||||
|
||||
@@ -378,14 +378,14 @@ impl NavigationMessageHandler {
|
||||
}
|
||||
|
||||
pub fn calculate_offset_transform(&self, viewport_center: DVec2, pan: DVec2, tilt: f64, zoom: f64) -> DAffine2 {
|
||||
let scaled_centre = viewport_center / self.snapped_scale(zoom);
|
||||
let scaled_center = viewport_center / self.snapped_scale(zoom);
|
||||
|
||||
// Try to avoid fractional coordinates to reduce anti aliasing.
|
||||
let scale = self.snapped_scale(zoom);
|
||||
let rounded_pan = ((pan + scaled_centre) * scale).round() / scale - scaled_centre;
|
||||
let rounded_pan = ((pan + scaled_center) * scale).round() / scale - scaled_center;
|
||||
|
||||
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
|
||||
let offset_transform = DAffine2::from_translation(scaled_centre);
|
||||
let offset_transform = DAffine2::from_translation(scaled_center);
|
||||
let scale_transform = DAffine2::from_scale(DVec2::splat(scale));
|
||||
let angle_transform = DAffine2::from_angle(self.snapped_angle(tilt));
|
||||
let translation_transform = DAffine2::from_translation(rounded_pan);
|
||||
|
||||
@@ -105,6 +105,13 @@ pub enum GraphOperationMessage {
|
||||
id: NodeId,
|
||||
},
|
||||
ClearArtboards,
|
||||
NewSvg {
|
||||
id: NodeId,
|
||||
svg: String,
|
||||
transform: DAffine2,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: isize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
+213
-2
@@ -3,18 +3,20 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
use graphene_core::vector::style::{Fill, FillType, Stroke};
|
||||
use graphene_core::vector::style::{Fill, FillType, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||
use graphene_core::{Artboard, Color};
|
||||
use transform_utils::LayerBounds;
|
||||
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use usvg::NodeExt;
|
||||
|
||||
pub mod transform_utils;
|
||||
|
||||
@@ -752,6 +754,29 @@ impl MessageHandler<GraphOperationMessage, GraphOperationHandlerData<'_>> for Gr
|
||||
}
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
GraphOperationMessage::NewSvg {
|
||||
id,
|
||||
svg,
|
||||
transform,
|
||||
parent,
|
||||
insert_index,
|
||||
} => {
|
||||
use usvg::TreeParsing;
|
||||
let tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "SVG parsing failed".to_string(),
|
||||
description: e.to_string(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
|
||||
import_usvg_node(&mut modify_inputs, &tree.root, transform, id, parent, insert_index);
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,3 +789,189 @@ pub fn load_network_structure(document_network: &NodeNetwork, document_metadata:
|
||||
document_metadata.load_structure(document_network, selected_nodes);
|
||||
collapsed.0.retain(|&layer| document_metadata.layer_exists(layer));
|
||||
}
|
||||
|
||||
fn usvg_color(c: usvg::Color, a: f32) -> Color {
|
||||
Color::from_rgbaf32_unchecked(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., a)
|
||||
}
|
||||
|
||||
fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
||||
DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64])
|
||||
}
|
||||
|
||||
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: isize) {
|
||||
let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) else {
|
||||
return;
|
||||
};
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
match &*node.borrow() {
|
||||
usvg::NodeKind::Group(_group) => {
|
||||
for child in node.children() {
|
||||
import_usvg_node(modify_inputs, &child, transform, NodeId(generate_uuid()), LayerNodeIdentifier::new_unchecked(layer), -1);
|
||||
}
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
}
|
||||
usvg::NodeKind::Path(path) => {
|
||||
let subpaths = convert_usvg_path(path);
|
||||
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();
|
||||
let transformed_bounds = subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box_with_transform(transform * usvg_transform(node.abs_transform())))
|
||||
.reduce(Quad::combine_bounds)
|
||||
.unwrap_or_default();
|
||||
modify_inputs.insert_vector_data(subpaths, layer);
|
||||
|
||||
let center = DAffine2::from_translation((bounds[0] + bounds[1]) / 2.);
|
||||
|
||||
modify_inputs.modify_inputs("Transform", true, |inputs, _node_id, _metadata| {
|
||||
transform_utils::update_transform(inputs, center.inverse() * transform * usvg_transform(node.abs_transform()) * center);
|
||||
});
|
||||
let bounds_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]);
|
||||
apply_usvg_fill(
|
||||
&path.fill,
|
||||
modify_inputs,
|
||||
transform * usvg_transform(node.abs_transform()),
|
||||
bounds_transform,
|
||||
transformed_bound_transform,
|
||||
);
|
||||
apply_usvg_stroke(&path.stroke, modify_inputs);
|
||||
}
|
||||
usvg::NodeKind::Image(_image) => {
|
||||
warn!("Skip image")
|
||||
}
|
||||
usvg::NodeKind::Text(text) => {
|
||||
let font = Font::new(crate::consts::DEFAULT_FONT_FAMILY.to_string(), crate::consts::DEFAULT_FONT_STYLE.to_string());
|
||||
modify_inputs.insert_text(text.chunks.iter().map(|chunk| chunk.text.clone()).collect(), font, 24., layer);
|
||||
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_stroke(stroke: &Option<usvg::Stroke>, modify_inputs: &mut ModifyInputsContext) {
|
||||
if let Some(stroke) = stroke {
|
||||
if let usvg::Paint::Color(color) = &stroke.paint {
|
||||
modify_inputs.stroke_set(Stroke {
|
||||
color: Some(usvg_color(*color, stroke.opacity.get())),
|
||||
weight: stroke.width.get() as f64,
|
||||
dash_lengths: stroke.dasharray.clone().unwrap_or_default(),
|
||||
dash_offset: stroke.dashoffset as f64,
|
||||
line_cap: match stroke.linecap {
|
||||
usvg::LineCap::Butt => LineCap::Butt,
|
||||
usvg::LineCap::Round => LineCap::Round,
|
||||
usvg::LineCap::Square => LineCap::Square,
|
||||
},
|
||||
line_join: match stroke.linejoin {
|
||||
usvg::LineJoin::Miter => LineJoin::Miter,
|
||||
usvg::LineJoin::MiterClip => LineJoin::Miter,
|
||||
usvg::LineJoin::Round => LineJoin::Round,
|
||||
usvg::LineJoin::Bevel => LineJoin::Bevel,
|
||||
},
|
||||
line_join_miter_limit: stroke.miterlimit.get() as f64,
|
||||
})
|
||||
} else {
|
||||
warn!("Skip non-solid stroke")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_fill(fill: &Option<usvg::Fill>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_transform: DAffine2, transformed_bound_transform: DAffine2) {
|
||||
if let Some(fill) = &fill {
|
||||
modify_inputs.fill_set(match &fill.paint {
|
||||
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity.get())),
|
||||
usvg::Paint::LinearGradient(linear) => {
|
||||
let local = [DVec2::new(linear.x1 as f64, linear.y1 as f64), DVec2::new(linear.x2 as f64, linear.y2 as f64)];
|
||||
|
||||
let to_doc_transform = if linear.base.units == usvg::Units::UserSpaceOnUse {
|
||||
transform
|
||||
} else {
|
||||
transformed_bound_transform
|
||||
};
|
||||
let to_doc = to_doc_transform * usvg_transform(linear.transform);
|
||||
|
||||
let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
|
||||
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
||||
|
||||
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
||||
|
||||
Fill::Gradient(Gradient {
|
||||
start,
|
||||
end,
|
||||
transform: DAffine2::IDENTITY,
|
||||
gradient_type: GradientType::Linear,
|
||||
positions: linear.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
|
||||
})
|
||||
}
|
||||
usvg::Paint::RadialGradient(radial) => {
|
||||
let local = [DVec2::new(radial.cx as f64, radial.cy as f64), DVec2::new(radial.fx as f64, radial.fy as f64)];
|
||||
|
||||
let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
|
||||
transform
|
||||
} else {
|
||||
transformed_bound_transform
|
||||
};
|
||||
let to_doc = to_doc_transform * usvg_transform(radial.transform);
|
||||
|
||||
let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
|
||||
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
|
||||
|
||||
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
|
||||
|
||||
Fill::Gradient(Gradient {
|
||||
start,
|
||||
end,
|
||||
transform: DAffine2::IDENTITY,
|
||||
gradient_type: GradientType::Radial,
|
||||
positions: radial.stops.iter().map(|stop| (stop.offset.get() as f64, usvg_color(stop.color, stop.opacity.get()))).collect(),
|
||||
})
|
||||
}
|
||||
usvg::Paint::Pattern(_) => {
|
||||
warn!("Skip pattern");
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
let mut subpaths = Vec::new();
|
||||
let mut groups = Vec::new();
|
||||
|
||||
let mut points = path.data.points().iter();
|
||||
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
|
||||
|
||||
for verb in path.data.verbs() {
|
||||
match verb {
|
||||
usvg::tiny_skia_path::PathVerb::Move => {
|
||||
subpaths.push(Subpath::new(std::mem::take(&mut groups), false));
|
||||
let Some(start) = points.next().map(to_vec) else { continue };
|
||||
groups.push(ManipulatorGroup::new(start, Some(start), Some(start)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Line => {
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
groups.push(ManipulatorGroup::new(end, Some(end), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Quad => {
|
||||
let Some(handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
if let Some(last) = groups.last_mut() {
|
||||
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
|
||||
}
|
||||
groups.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Cubic => {
|
||||
let Some(first_handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(second_handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
if let Some(last) = groups.last_mut() {
|
||||
last.out_handle = Some(first_handle);
|
||||
}
|
||||
groups.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Close => {
|
||||
subpaths.push(Subpath::new(std::mem::take(&mut groups), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
subpaths.push(Subpath::new(groups, false));
|
||||
subpaths
|
||||
}
|
||||
|
||||
@@ -278,19 +278,8 @@ impl NodeGraphMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: clean up this massive function
|
||||
fn send_graph(
|
||||
&self,
|
||||
network: &NodeNetwork,
|
||||
graph_view_overlay_open: bool,
|
||||
metadata: &mut DocumentMetadata,
|
||||
selected_nodes: &mut SelectedNodes,
|
||||
collapsed: &CollapsedLayers,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
metadata.load_structure(network, selected_nodes);
|
||||
|
||||
let links = network
|
||||
fn collect_links(network: &NodeNetwork) -> Vec<FrontendNodeLink> {
|
||||
network
|
||||
.nodes
|
||||
.iter()
|
||||
.flat_map(|(link_end, node)| node.inputs.iter().filter(|input| input.is_exposed()).enumerate().map(move |(index, input)| (input, link_end, index)))
|
||||
@@ -312,8 +301,10 @@ impl NodeGraphMessageHandler {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn collect_nodes(&self, links: &Vec<FrontendNodeLink>, network: &NodeNetwork) -> Vec<FrontendNode> {
|
||||
let connected_node_to_output_lookup = links.iter().map(|link| ((link.link_start, link.link_start_output_index), link.link_end)).collect::<HashMap<_, _>>();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
@@ -376,7 +367,12 @@ impl NodeGraphMessageHandler {
|
||||
disabled: network.disabled.contains(&node_id),
|
||||
errors: errors.map(|e| format!("{e:?}")),
|
||||
});
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
fn update_layer_panel(network: &NodeNetwork, metadata: &DocumentMetadata, collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
|
||||
for (&node_id, node) in &network.nodes {
|
||||
if node.is_layer() {
|
||||
let layer = LayerNodeIdentifier::new(node_id, network);
|
||||
let layer_classification = {
|
||||
@@ -402,12 +398,19 @@ impl NodeGraphMessageHandler {
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_graph(&self, network: &NodeNetwork, graph_open: bool, metadata: &mut DocumentMetadata, selected_nodes: &mut SelectedNodes, collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
|
||||
metadata.load_structure(network, selected_nodes);
|
||||
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
if graph_view_overlay_open {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
Self::update_layer_panel(network, metadata, collapsed, responses);
|
||||
if graph_open {
|
||||
let links = Self::collect_links(network);
|
||||
let nodes = self.collect_nodes(&links, network);
|
||||
responses.add(FrontendMessage::UpdateNodeGraph { nodes, links });
|
||||
}
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
|
||||
/// Updates the frontend's selection state in line with the backend
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Default for SnappingState {
|
||||
edges: true,
|
||||
corners: true,
|
||||
edge_midpoints: false,
|
||||
centres: true,
|
||||
centers: true,
|
||||
},
|
||||
nodes: NodeSnapping {
|
||||
paths: true,
|
||||
@@ -107,7 +107,7 @@ impl SnappingState {
|
||||
BoundingBoxSnapTarget::Corner => self.bounds.corners,
|
||||
BoundingBoxSnapTarget::Edge => self.bounds.edges,
|
||||
BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints,
|
||||
BoundingBoxSnapTarget::Centre => self.bounds.centres,
|
||||
BoundingBoxSnapTarget::Center => self.bounds.centers,
|
||||
},
|
||||
SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes {
|
||||
GeometrySnapTarget::Smooth => self.nodes.smooth_nodes,
|
||||
@@ -129,7 +129,7 @@ pub struct BoundsSnapping {
|
||||
pub edges: bool,
|
||||
pub corners: bool,
|
||||
pub edge_midpoints: bool,
|
||||
pub centres: bool,
|
||||
pub centers: bool,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NodeSnapping {
|
||||
@@ -217,12 +217,12 @@ impl GridSnapping {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoundingBoxSnapSource {
|
||||
Corner,
|
||||
Centre,
|
||||
Center,
|
||||
EdgeMidpoint,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoardSnapSource {
|
||||
Centre,
|
||||
Center,
|
||||
Corner,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -254,7 +254,7 @@ pub enum BoundingBoxSnapTarget {
|
||||
Corner,
|
||||
Edge,
|
||||
EdgeMidpoint,
|
||||
Centre,
|
||||
Center,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeometrySnapTarget {
|
||||
@@ -270,7 +270,7 @@ pub enum GeometrySnapTarget {
|
||||
pub enum BoardSnapTarget {
|
||||
Edge,
|
||||
Corner,
|
||||
Centre,
|
||||
Center,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GridSnapTarget {
|
||||
|
||||
Reference in New Issue
Block a user