Add drag-and-drop and copy-paste file importing/opening throughout the UI (#2012)

* Add file importing by dragging and dropping throughout the UI

* Disable comment-profiling-changes.yaml

* Fix CI
This commit is contained in:
Keavon Chambers
2024-09-28 00:19:43 -07:00
committed by GitHub
parent 20470b566b
commit 904cf09c79
35 changed files with 578 additions and 259 deletions

View File

@@ -40,6 +40,9 @@ pub const SLOWING_DIVISOR: f64 = 10.;
pub const NUDGE_AMOUNT: f64 = 1.;
pub const BIG_NUDGE_AMOUNT: f64 = 10.;
// Tools
pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
// Select tool
pub const SELECTION_TOLERANCE: f64 = 5.;
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
@@ -65,6 +68,7 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
// Brush tool
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
// Scrollbars
pub const SCROLLBAR_SPACING: f64 = 0.1;

View File

@@ -34,6 +34,8 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
}

View File

@@ -41,12 +41,22 @@ impl DialogLayoutHolder for CloseDocumentDialog {
impl LayoutHolder for CloseDocumentDialog {
fn layout(&self) -> Layout {
let max_length = 60;
let max_one_line_length = 40;
let mut name = self.document_name.clone();
name.truncate(max_length);
let ellipsis = if self.document_name.len() > max_length { "" } else { "" };
let break_lines = if self.document_name.len() > max_one_line_length { '\n' } else { ' ' };
Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("\"{}\" has unsaved changes", self.document_name)).multiline(true).widget_holder()],
widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_holder()],
},
]))
}

View File

@@ -77,13 +77,6 @@ pub enum DocumentMessage {
imaginate_node: Vec<NodeId>,
then_generate: bool,
},
ImportSvg {
id: NodeId,
svg: String,
transform: DAffine2,
parent: LayerNodeIdentifier,
insert_index: usize,
},
MoveSelectedLayersTo {
parent: LayerNodeIdentifier,
insert_index: usize,
@@ -98,12 +91,16 @@ pub enum DocumentMessage {
resize_opposite_corner: Key,
},
PasteImage {
name: Option<String>,
image: Image<Color>,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
PasteSvg {
name: Option<String>,
svg: String,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
Redo,
RenameDocument {
@@ -176,6 +173,9 @@ pub enum DocumentMessage {
PTZUpdate,
SelectionStepBack,
SelectionStepForward,
WrapContentInArtboard {
place_artboard_at_origin: bool,
},
ZoomCanvasTo100Percent,
ZoomCanvasTo200Percent,
ZoomCanvasToFitAll,

View File

@@ -1,9 +1,10 @@
use super::node_graph::document_node_definitions;
use super::node_graph::utility_types::Transform;
use super::overlays::utility_types::Pivot;
use super::utility_types::clipboards::Clipboard;
use super::utility_types::error::EditorError;
use super::utility_types::misc::{SnappingOptions, SnappingState, GET_SNAP_BOX_FUNCTIONS, GET_SNAP_GEOMETRY_FUNCTIONS};
use super::utility_types::network_interface::{NodeNetworkInterface, TransactionStatus};
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
@@ -18,7 +19,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_blend_mode, get_opacity};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity};
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
@@ -29,7 +30,7 @@ use graph_craft::document::{NodeId, NodeNetwork, OldNodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::vector::style::ViewMode;
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, IVec2};
pub struct DocumentMessageData<'a> {
pub document_id: DocumentId,
@@ -380,7 +381,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
let name = self.network_interface.frontend_display_name(&layer.to_node(), &[]);
let transform = self.metadata().document_to_viewport * DAffine2::from_translation(bounds[0].min(bounds[1]) - DVec2::Y * 4.);
let (_, angle, translation) = self.metadata().document_to_viewport.to_scale_angle_translation();
let translation = translation + bounds[0].min(bounds[1]) - DVec2::Y * 4.;
let transform = DAffine2::from_angle_translation(angle, translation);
overlay_context.text_with_transform(&name, COLOR_OVERLAY_GRAY, None, transform, Pivot::BottomLeft);
}
}
@@ -498,7 +503,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
let node_id = NodeId(generate_uuid());
let new_group_node = super::node_graph::document_node_definitions::resolve_document_node_type("Merge")
let new_group_node = document_node_definitions::resolve_document_node_type("Merge")
.expect("Failed to create merge node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
@@ -546,23 +551,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(DocumentMessage::ImaginateGenerate { imaginate_node });
}
}
DocumentMessage::ImportSvg {
id,
svg,
transform,
parent,
insert_index,
} => {
responses.add(DocumentMessage::StartTransaction);
responses.add(GraphOperationMessage::NewSvg {
id,
svg,
transform,
parent,
insert_index,
});
responses.add(DocumentMessage::EndTransaction);
}
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
if !self.selection_network_path.is_empty() {
log::error!("Moving selected layers is only supported for the Document Network");
@@ -608,7 +596,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
// Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first.
let layers_to_move_with_insert_offset: Vec<(LayerNodeIdentifier, usize)> = layers_to_move
let layers_to_move_with_insert_offset = layers_to_move
.iter()
.map(|layer| {
if layer.parent(self.metadata()) != Some(parent) {
@@ -727,7 +715,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
}
}
}
DocumentMessage::PasteImage { image, mouse } => {
DocumentMessage::PasteImage {
name,
image,
mouse,
parent_and_insert_index,
} => {
// All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb`
let image_size = DVec2::new(image.width as f64, image.height as f64);
@@ -744,12 +737,27 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let transform = center_in_viewport_layerspace * fit_image_size;
let layer_node_id = NodeId(generate_uuid());
let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id);
responses.add(DocumentMessage::AddTransaction);
let image_frame = ImageFrame { image, ..Default::default() };
let layer = graph_modification_utils::new_image_layer(image_frame, layer_node_id, self.new_layer_parent(true), responses);
use crate::messages::tool::common_functionality::graph_modification_utils;
let layer = graph_modification_utils::new_image_layer(image_frame, NodeId(generate_uuid()), self.new_layer_parent(true), responses);
if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName {
node_id: layer.to_node(),
alias: name,
});
}
if let Some((parent, insert_index)) = parent_and_insert_index {
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: layer_id,
parent,
insert_index,
});
}
// `layer` cannot be `ROOT_PARENT` since it is the newly created layer
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
@@ -764,12 +772,37 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
// Force chosen tool to be Select Tool after importing image.
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
}
DocumentMessage::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());
DocumentMessage::PasteSvg {
name,
svg,
mouse,
parent_and_insert_index,
} => {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
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(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(true), responses);
let layer_node_id = NodeId(generate_uuid());
let layer_id = LayerNodeIdentifier::new_unchecked(layer_node_id);
responses.add(DocumentMessage::AddTransaction);
let layer = graph_modification_utils::new_svg_layer(svg, center_in_viewport, layer_node_id, self.new_layer_parent(true), responses);
if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName {
node_id: layer.to_node(),
alias: name,
});
}
if let Some((parent, insert_index)) = parent_and_insert_index {
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: layer_id,
parent,
insert_index,
});
}
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
}
@@ -1181,6 +1214,44 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.network_interface.selection_step_forward(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged);
}
DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
// Get bounding box of all layers
let bounds = self.network_interface.document_bounds_document_space(false);
let Some(bounds) = bounds else { return };
let bounds_rounded_dimensions = (bounds[1] - bounds[0]).round();
// Create an artboard and set its dimensions to the bounding box size and location
let node_id = NodeId(generate_uuid());
let node_layer_id = LayerNodeIdentifier::new_unchecked(node_id);
let new_artboard_node = document_node_definitions::resolve_document_node_type("Artboard")
.expect("Failed to create artboard node")
.default_node_template();
responses.add(NodeGraphMessage::InsertNode {
node_id,
node_template: new_artboard_node,
});
responses.add(NodeGraphMessage::ShiftNodePosition { node_id, x: 15, y: -3 });
responses.add(GraphOperationMessage::ResizeArtboard {
layer: LayerNodeIdentifier::new_unchecked(node_id),
location: if place_artboard_at_origin { IVec2::ZERO } else { bounds[0].round().as_ivec2() },
dimensions: bounds_rounded_dimensions.as_ivec2(),
});
// Connect the current output data to the artboard's input data, and the artboard's output to the document output
responses.add(NodeGraphMessage::InsertNodeBetween {
node_id,
input_connector: network_interface::InputConnector::Export(0),
insert_node_input_index: 1,
});
// Shift the content by half its width and height so it gets centered in the artboard
responses.add(GraphOperationMessage::TransformChange {
layer: node_layer_id,
transform: DAffine2::from_translation(bounds_rounded_dimensions / 2.),
transform_in: TransformIn::Local,
skip_rerender: true,
});
}
DocumentMessage::ZoomCanvasTo100Percent => {
responses.add_front(NavigationMessage::CanvasZoomSet { zoom_factor: 1. });
}

View File

@@ -173,7 +173,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);
modify_inputs.insert_vector_data(subpaths, layer);
modify_inputs.insert_vector_data(subpaths, layer, true, true, true);
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
@@ -228,6 +228,10 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
};
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let size = tree.size();
let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.;
let transform = transform * DAffine2::from_translation(offset_to_center);
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index);
}
}
@@ -260,15 +264,20 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
usvg::Node::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();
modify_inputs.insert_vector_data(subpaths, layer);
modify_inputs.insert_vector_data(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
if let Some(transform_node_id) = modify_inputs.existing_node_id("Transform") {
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
}
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
apply_usvg_fill(path.fill(), modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform);
apply_usvg_stroke(path.stroke(), modify_inputs, transform * usvg_transform(node.abs_transform()));
if let Some(fill) = path.fill() {
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
apply_usvg_fill(fill, modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform);
}
if let Some(stroke) = path.stroke() {
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
}
}
usvg::Node::Image(_image) => {
warn!("Skip image")
@@ -281,96 +290,90 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
}
}
fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
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().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).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,
transform,
fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
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().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).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,
transform,
})
}
}
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_transform: DAffine2) {
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)];
// TODO: fix this
// let to_doc_transform = if linear.base.units() == usvg::Units::UserSpaceOnUse {
// transform
// } else {
// transformed_bound_transform
// };
let to_doc_transform = 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])];
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear,
stops,
})
} else {
warn!("Skip non-solid stroke")
}
}
}
fn apply_usvg_fill(fill: Option<&usvg::Fill>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_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)];
// TODO: fix this
// let to_doc_transform = if linear.base.units() == usvg::Units::UserSpaceOnUse {
// transform
// } else {
// transformed_bound_transform
// };
let to_doc_transform = 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])];
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear,
stops,
})
}
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)];
// TODO: fix this
// let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
// transform
// } else {
// transformed_bound_transform
// };
let to_doc_transform = 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])];
let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial,
stops,
})
}
usvg::Paint::Pattern(_) => {
warn!("Skip pattern");
return;
}
});
}
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)];
// TODO: fix this
// let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
// transform
// } else {
// transformed_bound_transform
// };
let to_doc_transform = 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])];
let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops);
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial,
stops,
})
}
usvg::Paint::Pattern(_) => {
warn!("Skip pattern");
return;
}
});
}

View File

@@ -145,32 +145,36 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
}
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier) {
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector_data = VectorData::from_subpaths(subpaths, true);
let path = resolve_document_node_type("Path")
let shape = resolve_document_node_type("Path")
.expect("Path node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::VectorData(vector_data), false))]);
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let shape_id = NodeId(generate_uuid());
self.network_interface.insert_node(shape_id, path, &[]);
self.network_interface.insert_node(shape_id, shape, &[]);
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
let transform_id = NodeId(generate_uuid());
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
if include_transform {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let transform_id = NodeId(generate_uuid());
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[]);
}
let fill_id = NodeId(generate_uuid());
self.network_interface.insert_node(fill_id, fill, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
if include_fill {
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
let fill_id = NodeId(generate_uuid());
self.network_interface.insert_node(fill_id, fill, &[]);
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[]);
}
let stroke_id = NodeId(generate_uuid());
self.network_interface.insert_node(stroke_id, stroke, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
if include_stroke {
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
let stroke_id = NodeId(generate_uuid());
self.network_interface.insert_node(stroke_id, stroke, &[]);
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
}
}
pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: LayerNodeIdentifier) {

View File

@@ -103,7 +103,6 @@ impl LayoutHolder for MenuBarMessageHandler {
label: "Import…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
action: MenuBarEntry::create_action(|_| PortfolioMessage::Import.into()),
disabled: no_active_document, // TODO: Allow importing an image (or dragging it in, or pasting) without an active document to create a new one with an artboards of the image's size (issue #1140)
..MenuBarEntry::default()
},
MenuBarEntry {

View File

@@ -4,7 +4,9 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use graphene_core::raster::Image;
use graphene_core::text::Font;
use graphene_core::Color;
#[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -87,6 +89,18 @@ pub enum PortfolioMessage {
PasteSerializedData {
data: String,
},
PasteImage {
name: Option<String>,
image: Image<Color>,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
PasteSvg {
name: Option<String>,
svg: String,
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
PrevDocument,
SetActivePanel {
panel: PanelType,

View File

@@ -317,9 +317,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
if self.active_document().is_some() {
responses.add(FrontendMessage::TriggerImport);
}
responses.add(FrontendMessage::TriggerImport);
}
PortfolioMessage::LoadDocumentResources { document_id } => {
if let Some(document) = self.document_mut(document_id) {
@@ -636,6 +634,68 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
}
}
PortfolioMessage::PasteImage {
name,
image,
mouse,
parent_and_insert_index,
} => {
let create_document = self.documents.is_empty();
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
});
}
responses.add(DocumentMessage::PasteImage {
name,
image,
mouse,
parent_and_insert_index,
});
if create_document {
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
responses.add(Message::StartBuffer);
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
}
}
PortfolioMessage::PasteSvg {
name,
svg,
mouse,
parent_and_insert_index,
} => {
let create_document = self.documents.is_empty();
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
});
}
responses.add(DocumentMessage::PasteSvg {
name,
svg,
mouse,
parent_and_insert_index,
});
if create_document {
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
responses.add(Message::StartBuffer);
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
}
}
PortfolioMessage::PrevDocument => {
if let Some(active_document_id) = self.active_document_id {
let len = self.document_ids.len();

View File

@@ -36,7 +36,7 @@ pub fn new_image_layer(image_frame: ImageFrame<Color>, id: NodeId, parent: Layer
/// 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 = 0;
responses.add(DocumentMessage::ImportSvg {
responses.add(GraphOperationMessage::NewSvg {
id,
svg,
transform,

View File

@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_BRUSH_SIZE;
use crate::messages::portfolio::document::graph_operation::transform_utils::{get_current_normalized_pivot, get_current_transform};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -41,7 +42,7 @@ pub struct BrushOptions {
impl Default for BrushOptions {
fn default() -> Self {
Self {
diameter: 40.,
diameter: DEFAULT_BRUSH_SIZE,
hardness: 0.,
flow: 100.,
spacing: 20.,

View File

@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -28,7 +29,7 @@ pub struct EllipseToolOptions {
impl Default for EllipseToolOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
}

View File

@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -31,7 +32,7 @@ pub struct FreehandOptions {
impl Default for FreehandOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_none(),
stroke: ToolColorOptions::new_primary(),
}

View File

@@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::consts::{DEFAULT_STROKE_WIDTH, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -28,7 +28,7 @@ pub struct LineOptions {
impl Default for LineOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
stroke: ToolColorOptions::new_primary(),
}
}

View File

@@ -1,6 +1,5 @@
use super::tool_prelude::*;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -34,7 +33,7 @@ pub struct PenOptions {
impl Default for PenOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
}

View File

@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -33,7 +34,7 @@ impl Default for PolygonOptions {
fn default() -> Self {
Self {
vertices: 5,
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
polygon_type: PolygonType::Convex,

View File

@@ -1,4 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DEFAULT_STROKE_WIDTH;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, overlays::utility_types::OverlayContext};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -27,7 +28,7 @@ pub struct RectangleToolOptions {
impl Default for RectangleToolOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
}

View File

@@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::consts::DRAG_THRESHOLD;
use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -27,7 +27,7 @@ pub struct SplineOptions {
impl Default for SplineOptions {
fn default() -> Self {
Self {
line_weight: 5.,
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_none(),
stroke: ToolColorOptions::new_primary(),
}