mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-31 02:07:21 +00: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:
parent
8eef96511e
commit
002151d9c0
22 changed files with 528 additions and 93 deletions
166
Cargo.lock
generated
166
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -57,6 +57,7 @@ web-sys = { workspace = true, features = [
|
|||
"CanvasRenderingContext2d",
|
||||
"TextMetrics",
|
||||
] }
|
||||
usvg = "0.37"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -34,6 +34,19 @@ pub fn new_image_layer(image_frame: ImageFrame<Color>, 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<Message>) -> 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<ManipulatorGroupId>], layer: LayerNodeIdentifier, mirror_angle: bool, responses: &mut VecDeque<Message>) {
|
||||
for manipulator_group in manipulator_groups {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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<ManipulatorGroupId>| {
|
||||
let mut push_neighbor = |group: ManipulatorGroup<ManipulatorGroupId>| {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Vec<DVec2>>) -> Self {
|
||||
pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> 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<SnapCandidatePoint>, values: BBoxSnapValues, document: &DocumentMessageHandler) {
|
||||
|
@ -359,8 +364,8 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, 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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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<DVec2>, 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<DVec2>, 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -660,6 +660,13 @@ impl JsEditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = pasteSvg)]
|
||||
pub fn paste_svg(&self, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
||||
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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue