diff --git a/Cargo.lock b/Cargo.lock index d509c759..0dba8cb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,6 +1194,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "deranged" version = "0.3.10" @@ -1585,7 +1591,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "674e258f4b5d2dcd63888c01c68413c51f565e8af99d2f7701c7b81d79ef41c4" dependencies = [ - "roxmltree", + "roxmltree 0.18.1", ] [[package]] @@ -1602,6 +1608,20 @@ dependencies = [ "ttf-parser 0.19.2", ] +[[package]] +name = "fontdb" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b88c54a38407f7352dd2c4238830115a6377741098ffd1f997c813d0e088a6" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2 0.9.3", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -2362,6 +2382,7 @@ dependencies = [ "serde_json", "specta", "thiserror", + "usvg 0.37.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3347,6 +3368,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45fd3a57831bf88bc63f8cebc0cf956116276e97fef3966103e96416209f7c92" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -4776,7 +4806,7 @@ dependencies = [ "pico-args", "png", "rgb", - "svgtypes", + "svgtypes 0.11.0", "tiny-skia 0.10.0", "usvg 0.35.0", ] @@ -4847,10 +4877,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad747e7384940e7bf33b15ba433b7bad9f44c0c6d5287a67c2cb22cd1743d497" dependencies = [ "log", - "roxmltree", + "roxmltree 0.18.1", "simplecss", "siphasher", - "svgtypes", + "svgtypes 0.11.0", ] [[package]] @@ -4862,6 +4892,12 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4979,6 +5015,22 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "rustybuzz" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" +dependencies = [ + "bitflags 2.4.1", + "bytemuck", + "smallvec", + "ttf-parser 0.20.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.16" @@ -5628,6 +5680,16 @@ dependencies = [ "siphasher", ] +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo 0.9.5", + "siphasher", +] + [[package]] name = "syn" version = "1.0.109" @@ -6164,6 +6226,17 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de35e8a90052baaaf61f171680ac2f8e925a1e43ea9d2e3a00514772250e541" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -6506,6 +6579,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f91c8b21fbbaa18853c3d0801c78f4fc94cdb976699bb03e832e75f7fd22f0" + [[package]] name = "unicode-script" version = "0.5.5" @@ -6584,20 +6663,35 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.5", + "log", + "pico-args", + "usvg-parser 0.37.0", + "usvg-text-layout 0.37.0", + "usvg-tree 0.37.0", + "xmlwriter", +] + [[package]] name = "usvg-parser" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7529174e721c8078d62b08399258469b1d68b4e5f2983b347d6a9d39779366c" dependencies = [ - "data-url", + "data-url 0.2.0", "flate2", "imagesize", "kurbo 0.9.5", "log", "rosvgtree", "strict-num", - "svgtypes", + "svgtypes 0.11.0", "usvg-tree 0.33.0", ] @@ -6607,25 +6701,43 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19bf93d230813599927d88557014e0908ecc3531666d47c634c6838bc8db408" dependencies = [ - "data-url", + "data-url 0.2.0", "flate2", "imagesize", "kurbo 0.9.5", "log", - "roxmltree", + "roxmltree 0.18.1", "simplecss", "siphasher", - "svgtypes", + "svgtypes 0.11.0", "usvg-tree 0.35.0", ] +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url 0.3.1", + "flate2", + "imagesize", + "kurbo 0.9.5", + "log", + "roxmltree 0.19.0", + "simplecss", + "siphasher", + "svgtypes 0.13.0", + "usvg-tree 0.37.0", +] + [[package]] name = "usvg-text-layout" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e672fbc19261c6553113cc04ff2ff38ae52fadbd90f2d814040857795fb5c50" dependencies = [ - "fontdb", + "fontdb 0.14.1", "kurbo 0.9.5", "log", "rustybuzz 0.7.0", @@ -6641,7 +6753,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "035044604e89652c0a2959b8b356946997a52649ba6cade45928c2842376feb4" dependencies = [ - "fontdb", + "fontdb 0.14.1", "kurbo 0.9.5", "log", "rustybuzz 0.7.0", @@ -6651,6 +6763,22 @@ dependencies = [ "usvg-tree 0.35.0", ] +[[package]] +name = "usvg-text-layout" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d383a3965de199d7f96d4e11a44dd859f46e86de7f3dca9a39bf82605da0a37c" +dependencies = [ + "fontdb 0.16.0", + "kurbo 0.9.5", + "log", + "rustybuzz 0.12.1", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "usvg-tree 0.37.0", +] + [[package]] name = "usvg-tree" version = "0.33.0" @@ -6660,7 +6788,7 @@ dependencies = [ "kurbo 0.9.5", "rctree", "strict-num", - "svgtypes", + "svgtypes 0.11.0", ] [[package]] @@ -6671,10 +6799,22 @@ checksum = "7939a7e4ed21cadb5d311d6339730681c3e24c3e81d60065be80e485d3fc8b92" dependencies = [ "rctree", "strict-num", - "svgtypes", + "svgtypes 0.11.0", "tiny-skia-path 0.10.0", ] +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes 0.13.0", + "tiny-skia-path 0.11.3", +] + [[package]] name = "utf-8" version = "0.7.6" diff --git a/editor/Cargo.toml b/editor/Cargo.toml index cfa342cd..5ed89dcf 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -57,6 +57,7 @@ web-sys = { workspace = true, features = [ "CanvasRenderingContext2d", "TextMetrics", ] } +usvg = "0.37" [dev-dependencies] diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 9cf7974a..7e134495 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -87,6 +87,10 @@ pub enum DocumentMessage { image: Image, mouse: Option<(f64, f64)>, }, + PasteSvg { + svg: String, + mouse: Option<(f64, f64)>, + }, Redo, RenameDocument { new_name: String, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5be295a3..d23a3ad2 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -602,6 +602,14 @@ impl MessageHandler> 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); diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index b5786197..32ea98d5 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -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); diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index 7b392666..d98bf98e 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -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)] diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 25569747..aa45c768 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -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> 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, 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, 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> { + 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 +} diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 65450a10..b492aa36 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -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, - ) { - metadata.load_structure(network, selected_nodes); - - let links = network + fn collect_links(network: &NodeNetwork) -> Vec { + 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::>(); + .collect::>() + } + fn collect_nodes(&self, links: &Vec, network: &NodeNetwork) -> Vec { let connected_node_to_output_lookup = links.iter().map(|link| ((link.link_start, link.link_start_output_index), link.link_end)).collect::>(); 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) { + 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) { + 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 diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 2187ba6c..0f8afcc7 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -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 { diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 23a94e36..7aca2318 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -34,6 +34,19 @@ pub fn new_image_layer(image_frame: ImageFrame, id: NodeId, parent: Layer LayerNodeIdentifier::new_unchecked(id) } +/// Create a new group layer from an svg +pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque) -> LayerNodeIdentifier { + let insert_index = -1; + responses.add(GraphOperationMessage::NewSvg { + id, + svg, + transform, + parent, + insert_index, + }); + LayerNodeIdentifier::new_unchecked(id) +} + /// Batch set all of the manipulator groups to mirror on a specific layer pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup], layer: LayerNodeIdentifier, mirror_angle: bool, responses: &mut VecDeque) { for manipulator_group in manipulator_groups { diff --git a/editor/src/messages/tool/common_functionality/resize.rs b/editor/src/messages/tool/common_functionality/resize.rs index 464205fe..d647ba96 100644 --- a/editor/src/messages/tool/common_functionality/resize.rs +++ b/editor/src/messages/tool/common_functionality/resize.rs @@ -45,7 +45,7 @@ impl Resize { let mut points_viewport = [start, mouse]; let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] }; let ratio = input.keyboard.get(lock_ratio as usize); - let centre = input.keyboard.get(center as usize); + let center = input.keyboard.get(center as usize); let snap_data = SnapData::ignore(document, input, &ignore); if ratio { let size = points_viewport[1] - points_viewport[0]; @@ -56,7 +56,7 @@ impl Resize { origin: self.drag_start, direction: end_document - self.drag_start, }; - if centre { + if center { let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, None); let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document); let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, None); @@ -69,7 +69,7 @@ impl Resize { points_viewport[1] = to_viewport.transform_point2(snapped.snapped_point_document); self.snap_manager.update_indicator(snapped); } - } else if centre { + } else if center { let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), None, false); let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(2. * self.drag_start - document_mouse), None, false); let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 11681f61..cd123ef5 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -100,7 +100,7 @@ impl ShapeState { let Some(position) = handle.get_position(&group) else { continue }; let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source); - let mut push_neighbour = |group: ManipulatorGroup| { + let mut push_neighbor = |group: ManipulatorGroup| { if !state.is_selected(ManipulatorPointId::new(group.id, SelectedType::Anchor)) { point.neighbors.push(to_document.transform_point2(group.anchor)); } @@ -108,15 +108,15 @@ impl ShapeState { if handle == SelectedType::Anchor { // Previous anchor (looping if closed) if index > 0 { - push_neighbour(subpath.manipulator_groups()[index - 1]); + push_neighbor(subpath.manipulator_groups()[index - 1]); } else if subpath.closed() { - push_neighbour(subpath.manipulator_groups()[subpath.len() - 1]); + push_neighbor(subpath.manipulator_groups()[subpath.len() - 1]); } // Next anchor (looping if closed) if index + 1 < subpath.len() { - push_neighbour(subpath.manipulator_groups()[index + 1]); + push_neighbor(subpath.manipulator_groups()[index + 1]); } else if subpath.closed() { - push_neighbour(subpath.manipulator_groups()[0]); + push_neighbor(subpath.manipulator_groups()[0]); } } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 3cf1b172..64540c72 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -31,7 +31,7 @@ pub enum SnapConstraint { }, Direction(DVec2), Circle { - centre: DVec2, + center: DVec2, radius: f64, }, } @@ -39,14 +39,14 @@ impl SnapConstraint { pub fn projection(&self, point: DVec2) -> DVec2 { match *self { Self::Line { origin, direction } if direction != DVec2::ZERO => (point - origin).project_onto(direction) + origin, - Self::Circle { centre, radius } => { - let from_centre = point - centre; - let distance = from_centre.length(); + Self::Circle { center, radius } => { + let from_center = point - center; + let distance = from_center.length(); if distance > 0. { - centre + radius * from_centre / distance + center + radius * from_center / distance } else { - // Point is exactly at the centre, so project right - centre + DVec2::new(radius, 0.) + // Point is exactly at the center, so project right + center + DVec2::new(radius, 0.) } } _ => point, diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index c7444e10..15da8496 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -88,7 +88,12 @@ impl LayerSnapper { let normals = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Normal)); let tangents = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Tangent)); let tolerance = snap_tolerance(document); + for path in &self.paths_to_snap { + // Skip very short paths + if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. { + continue; + } let time = path.document_curve.project(point.document_point, None); let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); @@ -120,8 +125,8 @@ impl LayerSnapper { self.collect_paths(snap_data, point.source_index == 0); let tolerance = snap_tolerance(document); - let constraint_path = if let SnapConstraint::Circle { centre, radius } = constraint { - Subpath::new_ellipse(centre - DVec2::splat(radius), centre + DVec2::splat(radius)) + let constraint_path = if let SnapConstraint::Circle { center, radius } = constraint { + Subpath::new_ellipse(center - DVec2::splat(radius), center + DVec2::splat(radius)) } else { let constrained_point = constraint.projection(point.document_point); let direction = constraint.direction().normalize_or_zero(); @@ -174,8 +179,8 @@ impl LayerSnapper { let values = BBoxSnapValues { corner_source: SnapSource::Board(BoardSnapSource::Corner), corner_target: SnapTarget::Board(BoardSnapTarget::Corner), - centre_source: SnapSource::Board(BoardSnapSource::Centre), - centre_target: SnapTarget::Board(BoardSnapTarget::Centre), + center_source: SnapSource::Board(BoardSnapSource::Center), + center_target: SnapTarget::Board(BoardSnapTarget::Center), ..Default::default() }; get_bbox_points(quad, &mut self.points_to_snap, values, document); @@ -244,8 +249,8 @@ impl LayerSnapper { fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) { if normals && path.bounds.is_none() { - for &neighbour in &point.neighbors { - for t in path.document_curve.normals_to_point(neighbour) { + for &neighbor in &point.neighbors { + for t in path.document_curve.normals_to_point(neighbor) { let normal_point = path.document_curve.evaluate(TValue::Parametric(t)); let distance = normal_point.distance(point.document_point); if distance > tolerance { @@ -265,8 +270,8 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, } } if tangents && path.bounds.is_none() { - for &neighbour in &point.neighbors { - for t in path.document_curve.tangents_to_point(neighbour) { + for &neighbor in &point.neighbors { + for t in path.document_curve.tangents_to_point(neighbor) { let tangent_point = path.document_curve.evaluate(TValue::Parametric(t)); let distance = tangent_point.distance(point.document_point); if distance > tolerance { @@ -323,9 +328,9 @@ impl SnapCandidatePoint { pub fn handle(document_point: DVec2) -> Self { Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp)) } - pub fn handle_neighbors(document_point: DVec2, neighbours: impl Into>) -> Self { + pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into>) -> Self { let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp)); - point.neighbors = neighbours.into(); + point.neighbors = neighbors.into(); point } } @@ -335,8 +340,8 @@ pub struct BBoxSnapValues { corner_target: SnapTarget, edge_source: SnapSource, edge_target: SnapTarget, - centre_source: SnapSource, - centre_target: SnapTarget, + center_source: SnapSource, + center_target: SnapTarget, } impl BBoxSnapValues { pub const BOUNDING_BOX: Self = Self { @@ -344,8 +349,8 @@ impl BBoxSnapValues { corner_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Corner), edge_source: SnapSource::BoundingBox(BoundingBoxSnapSource::EdgeMidpoint), edge_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint), - centre_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Centre), - centre_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Centre), + center_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Center), + center_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Center), }; } pub fn get_bbox_points(quad: Quad, points: &mut Vec, values: BBoxSnapValues, document: &DocumentMessageHandler) { @@ -359,8 +364,8 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec, values: points.push(SnapCandidatePoint::new_quad((start + end) / 2., values.edge_source, values.edge_target, Some(quad))); } } - if document.snapping_state.target_enabled(values.centre_target) { - points.push(SnapCandidatePoint::new_quad(quad.center(), values.centre_source, values.centre_target, Some(quad))); + if document.snapping_state.target_enabled(values.center_target) { + points.push(SnapCandidatePoint::new_quad(quad.center(), values.center_source, values.center_target, Some(quad))); } } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 84c8b3e8..4f22b095 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -153,8 +153,8 @@ impl ArtboardToolData { return; }; - let centre = from_center.then_some(bounds.center_of_transformation); - let (position, size) = movement.new_size(mouse_position, bounds.transform, centre, constrain_square, None); + let center = from_center.then_some(bounds.center_of_transformation); + let (position, size) = movement.new_size(mouse_position, bounds.transform, center, constrain_square, None); responses.add(GraphOperationMessage::ResizeArtboard { id: self.selected_artboard.unwrap().to_node(), location: position.round().as_ivec2(), diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index add4fa58..ac7eb368 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -258,7 +258,7 @@ impl Fsm for LineToolFsmState { } } -fn generate_transform(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, centre: bool) -> Message { +fn generate_transform(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> Message { let document_to_viewport = snap_data.document.metadata.document_to_viewport; let mut document_points = [tool_data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.drag_current)]; @@ -289,7 +289,7 @@ fn generate_transform(tool_data: &mut LineToolData, snap_data: SnapData, lock_an origin: document_points[0], direction: document_points[1] - document_points[0], }; - if centre { + if center { let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None); let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None); let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; @@ -301,7 +301,7 @@ fn generate_transform(tool_data: &mut LineToolData, snap_data: SnapData, lock_an document_points[1] = snapped.snapped_point_document; snap.update_indicator(snapped); } - } else if centre { + } else if center { let snapped = snap.free_snap(&snap_data, &near_point, None, false); let snapped_far = snap.free_snap(&snap_data, &far_point, None, false); let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index f714dbf6..8c837247 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -490,12 +490,12 @@ impl PenToolData { } /// Snap the angle of the line from relative to position if the key is pressed. - fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, mirror: bool, mouse: DVec2, relative: Option, neighbour: bool) -> DVec2 { + fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, mirror: bool, mouse: DVec2, relative: Option, neighbor: bool) -> DVec2 { let document = snap_data.document; let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse); let snap = &mut self.snap_manager; - let neighbours = relative.filter(|_| neighbour).map_or(Vec::new(), |neighbour| vec![neighbour]); + let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]); if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| snap_angle || lock_angle) { let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); @@ -510,8 +510,8 @@ impl PenToolData { origin: relative, direction: document_pos - relative, }; - let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbours.clone()); - let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbours); + let near_point = SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()); + let far_point = SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors); if mirror { let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None); let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None); @@ -527,8 +527,8 @@ impl PenToolData { snap.update_indicator(snapped); } } else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| mirror) { - let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbours.clone()), None, false); - let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbours), None, false); + let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors.clone()), None, false); + let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(2. * relative - document_pos, neighbors), None, false); document_pos = if snapped_far.other_snap_better(&snapped) { snapped.snapped_point_document } else { @@ -536,7 +536,7 @@ impl PenToolData { }; snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }); } else { - let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbours), None, false); + let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbors(document_pos, neighbors), None, false); document_pos = snapped.snapped_point_document; snap.update_indicator(snapped); } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index b500865e..fa84171b 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use super::tool_prelude::*; -use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; +use crate::consts::{self, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -274,7 +274,9 @@ impl SelectToolData { fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) { self.snap_candidates.clear(); for &layer in &self.layers_dragging { - snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates); + if (self.snap_candidates.len() as f64) < document.snapping_state.tolerance { + snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates); + } if let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) { let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds); snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document); @@ -642,13 +644,13 @@ impl Fsm for SelectToolFsmState { let (_center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align)); let center = false; // TODO: Reenable this feature after fixing it - let centre = center.then_some(bounds.center_of_transformation); + let center = center.then_some(bounds.center_of_transformation); let snap = Some(SizeSnapData { manager: &mut tool_data.snap_manager, points: &mut tool_data.snap_candidates, snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging), }); - let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, centre, constrain, snap); + let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); let pivot_transform = DAffine2::from_translation(pivot); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 8ae44ad5..3fbccce7 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -116,9 +116,15 @@ Array.from(dataTransfer.items).forEach(async (item) => { const file = item.getAsFile(); + if (file?.type.includes("svg")) { + const svgData = await file.text(); + editor.instance.pasteSvg(svgData, e.clientX, e.clientY); + + return; + } + if (file?.type.startsWith("image")) { const imageData = await extractPixelData(file); - editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY); } }); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index bff6ba59..8953f09b 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -272,7 +272,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (!dataTransfer || targetIsTextField(e.target || undefined)) return; e.preventDefault(); - Array.from(dataTransfer.items).forEach((item) => { + Array.from(dataTransfer.items).forEach(async (item) => { if (item.type === "text/plain") { item.getAsString((text) => { if (text.startsWith("graphite/layer: ")) { @@ -284,10 +284,17 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } const file = item.getAsFile(); + + if (file?.type === "svg") { + const text = await file.text(); + editor.instance.pasteSvg(text); + + return; + } + if (file?.type.startsWith("image")) { - extractPixelData(file).then((imageData) => { - editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); - }); + const imageData = await extractPixelData(file); + editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); } }); } @@ -327,6 +334,19 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Read an image from the clipboard and pass it to the editor to be loaded const imageType = item.types.find((type) => type.startsWith("image/")); + + if (imageType === "svg") { + const blob = await item.getType("text/plain"); + const reader = new FileReader(); + reader.onload = () => { + const text = reader.result as string; + editor.instance.pasteSvg(text); + }; + reader.readAsText(blob); + + return; + } + if (imageType) { const blob = await item.getType(imageType); const reader = new FileReader(); diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 80182ccd..81b3abb8 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -65,6 +65,14 @@ export function createPortfolioState(editor: Editor) { }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { const data = await upload("image/*", "data"); + + if (data.type.includes("svg")) { + const svg = new TextDecoder().decode(data.content); + editor.instance.pasteSvg(svg); + + return; + } + const imageData = await extractPixelData(new Blob([data.content], { type: data.type })); editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); }); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index db98e011..2f4dd554 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -660,6 +660,13 @@ impl JsEditorHandle { self.dispatch(message); } + #[wasm_bindgen(js_name = pasteSvg)] + pub fn paste_svg(&self, svg: String, mouse_x: Option, mouse_y: Option) { + let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y))); + let message = DocumentMessage::PasteSvg { svg, mouse }; + self.dispatch(message); + } + /// Toggle visibility of a layer from the layer list #[wasm_bindgen(js_name = toggleLayerVisibility)] pub fn toggle_layer_visibility(&self, id: u64) {