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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 578 additions and 259 deletions

View file

@ -9,6 +9,8 @@ env:
jobs: jobs:
profile: profile:
# TODO(TrueDoctor): Fix and reenable this action
if: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View file

@ -40,6 +40,9 @@ pub const SLOWING_DIVISOR: f64 = 10.;
pub const NUDGE_AMOUNT: f64 = 1.; pub const NUDGE_AMOUNT: f64 = 1.;
pub const BIG_NUDGE_AMOUNT: f64 = 10.; pub const BIG_NUDGE_AMOUNT: f64 = 10.;
// Tools
pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
// Select tool // Select tool
pub const SELECTION_TOLERANCE: f64 = 5.; pub const SELECTION_TOLERANCE: f64 = 5.;
pub const SELECTION_DRAG_ANGLE: f64 = 90.; pub const SELECTION_DRAG_ANGLE: f64 = 90.;
@ -65,6 +68,7 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
// Brush tool // Brush tool
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.; pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
// Scrollbars // Scrollbars
pub const SCROLLBAR_SPACING: f64 = 0.1; 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::RunDocumentGraph);
responses.add(NodeGraphMessage::UpdateNewNodeGraph); 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(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll); responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
} }

View file

@ -41,12 +41,22 @@ impl DialogLayoutHolder for CloseDocumentDialog {
impl LayoutHolder for CloseDocumentDialog { impl LayoutHolder for CloseDocumentDialog {
fn layout(&self) -> Layout { 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![ Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { LayoutGroup::Row {
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()], widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()],
}, },
LayoutGroup::Row { 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>, imaginate_node: Vec<NodeId>,
then_generate: bool, then_generate: bool,
}, },
ImportSvg {
id: NodeId,
svg: String,
transform: DAffine2,
parent: LayerNodeIdentifier,
insert_index: usize,
},
MoveSelectedLayersTo { MoveSelectedLayersTo {
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: usize, insert_index: usize,
@ -98,12 +91,16 @@ pub enum DocumentMessage {
resize_opposite_corner: Key, resize_opposite_corner: Key,
}, },
PasteImage { PasteImage {
name: Option<String>,
image: Image<Color>, image: Image<Color>,
mouse: Option<(f64, f64)>, mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
}, },
PasteSvg { PasteSvg {
name: Option<String>,
svg: String, svg: String,
mouse: Option<(f64, f64)>, mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
}, },
Redo, Redo,
RenameDocument { RenameDocument {
@ -176,6 +173,9 @@ pub enum DocumentMessage {
PTZUpdate, PTZUpdate,
SelectionStepBack, SelectionStepBack,
SelectionStepForward, SelectionStepForward,
WrapContentInArtboard {
place_artboard_at_origin: bool,
},
ZoomCanvasTo100Percent, ZoomCanvasTo100Percent,
ZoomCanvasTo200Percent, ZoomCanvasTo200Percent,
ZoomCanvasToFitAll, ZoomCanvasToFitAll,

View file

@ -1,9 +1,10 @@
use super::node_graph::document_node_definitions;
use super::node_graph::utility_types::Transform; use super::node_graph::utility_types::Transform;
use super::overlays::utility_types::Pivot; use super::overlays::utility_types::Pivot;
use super::utility_types::clipboards::Clipboard; use super::utility_types::clipboards::Clipboard;
use super::utility_types::error::EditorError; 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::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 super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH}; 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}; 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::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*; 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::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType; 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::raster::{BlendMode, ImageFrame};
use graphene_core::vector::style::ViewMode; use graphene_core::vector::style::ViewMode;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2, IVec2};
pub struct DocumentMessageData<'a> { pub struct DocumentMessageData<'a> {
pub document_id: DocumentId, 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 Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
let name = self.network_interface.frontend_display_name(&layer.to_node(), &[]); 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); 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 insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
let node_id = NodeId(generate_uuid()); 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") .expect("Failed to create merge node")
.default_node_template(); .default_node_template();
responses.add(NodeGraphMessage::InsertNode { responses.add(NodeGraphMessage::InsertNode {
@ -546,23 +551,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(DocumentMessage::ImaginateGenerate { imaginate_node }); 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 } => { DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
if !self.selection_network_path.is_empty() { if !self.selection_network_path.is_empty() {
log::error!("Moving selected layers is only supported for the Document Network"); 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); 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. // 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() .iter()
.map(|layer| { .map(|layer| {
if layer.parent(self.metadata()) != Some(parent) { 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` // 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); 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 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); responses.add(DocumentMessage::AddTransaction);
let image_frame = ImageFrame { image, ..Default::default() }; 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; if let Some(name) = name {
let layer = graph_modification_utils::new_image_layer(image_frame, NodeId(generate_uuid()), self.new_layer_parent(true), responses); 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 // `layer` cannot be `ROOT_PARENT` since it is the newly created layer
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); 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. // Force chosen tool to be Select Tool after importing image.
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select }); responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select });
} }
DocumentMessage::PasteSvg { svg, mouse } => { DocumentMessage::PasteSvg {
use crate::messages::tool::common_functionality::graph_modification_utils; name,
let viewport_location = mouse.map_or(ipp.viewport_bounds.center() + ipp.viewport_bounds.top_left, |pos| pos.into()); svg,
mouse,
parent_and_insert_index,
} => {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); 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 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(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select }); 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); self.network_interface.selection_step_forward(&self.selection_network_path);
responses.add(BroadcastEvent::SelectionChanged); 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 => { DocumentMessage::ZoomCanvasTo100Percent => {
responses.add_front(NavigationMessage::CanvasZoomSet { zoom_factor: 1. }); 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 } => { GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id); 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, &[]); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
@ -228,6 +228,10 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
}; };
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); 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); 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) => { usvg::Node::Path(path) => {
let subpaths = convert_usvg_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 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") { 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())); 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]); if let Some(fill) = path.fill() {
apply_usvg_fill(path.fill(), modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform); let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
apply_usvg_stroke(path.stroke(), modify_inputs, transform * usvg_transform(node.abs_transform())); 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) => { usvg::Node::Image(_image) => {
warn!("Skip 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) { fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
if let Some(stroke) = stroke { if let usvg::Paint::Color(color) = &stroke.paint() {
if let usvg::Paint::Color(color) = &stroke.paint() { modify_inputs.stroke_set(Stroke {
modify_inputs.stroke_set(Stroke { color: Some(usvg_color(*color, stroke.opacity().get())),
color: Some(usvg_color(*color, stroke.opacity().get())), weight: stroke.width().get() as f64,
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_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(), dash_offset: stroke.dashoffset() as f64,
dash_offset: stroke.dashoffset() as f64, line_cap: match stroke.linecap() {
line_cap: match stroke.linecap() { usvg::LineCap::Butt => LineCap::Butt,
usvg::LineCap::Butt => LineCap::Butt, usvg::LineCap::Round => LineCap::Round,
usvg::LineCap::Round => LineCap::Round, usvg::LineCap::Square => LineCap::Square,
usvg::LineCap::Square => LineCap::Square, },
}, line_join: match stroke.linejoin() {
line_join: match stroke.linejoin() { usvg::LineJoin::Miter => LineJoin::Miter,
usvg::LineJoin::Miter => LineJoin::Miter, usvg::LineJoin::MiterClip => LineJoin::Miter,
usvg::LineJoin::MiterClip => LineJoin::Miter, usvg::LineJoin::Round => LineJoin::Round,
usvg::LineJoin::Round => LineJoin::Round, usvg::LineJoin::Bevel => LineJoin::Bevel,
usvg::LineJoin::Bevel => LineJoin::Bevel, },
}, line_join_miter_limit: stroke.miterlimit().get() as f64,
line_join_miter_limit: stroke.miterlimit().get() as f64, transform,
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")
} }
} 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)];
fn apply_usvg_fill(fill: Option<&usvg::Fill>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2, bounds_transform: DAffine2) { // TODO: fix this
if let Some(fill) = &fill { // let to_doc_transform = if radial.base.units == usvg::Units::UserSpaceOnUse {
modify_inputs.fill_set(match &fill.paint() { // transform
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())), // } else {
usvg::Paint::LinearGradient(linear) => { // transformed_bound_transform
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 = transform;
// TODO: fix this let to_doc = to_doc_transform * usvg_transform(radial.transform());
// let to_doc_transform = if linear.base.units() == usvg::Units::UserSpaceOnUse {
// transform let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])];
// } else { let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])];
// transformed_bound_transform
// }; let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
let to_doc_transform = transform; let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let to_doc = to_doc_transform * usvg_transform(linear.transform()); let stops = GradientStops(stops);
let document = [to_doc.transform_point2(local[0]), to_doc.transform_point2(local[1])]; Fill::Gradient(Gradient {
let layer = [transform.inverse().transform_point2(document[0]), transform.inverse().transform_point2(document[1])]; start,
end,
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])]; transform: DAffine2::IDENTITY,
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); gradient_type: GradientType::Radial,
let stops = GradientStops(stops); stops,
})
Fill::Gradient(Gradient { }
start, usvg::Paint::Pattern(_) => {
end, warn!("Skip pattern");
transform: DAffine2::IDENTITY, return;
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;
}
});
}
} }

View file

@ -145,32 +145,36 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]); 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 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") .expect("Path node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::VectorData(vector_data), false))]); .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()); 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, &[]); self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
let transform_id = NodeId(generate_uuid()); if include_transform {
self.network_interface.insert_node(transform_id, transform, &[]); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
self.network_interface.move_node_to_chain_start(&transform_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, &[]);
}
let fill_id = NodeId(generate_uuid()); if include_fill {
self.network_interface.insert_node(fill_id, fill, &[]); let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
self.network_interface.move_node_to_chain_start(&fill_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, &[]);
}
let stroke_id = NodeId(generate_uuid()); if include_stroke {
self.network_interface.insert_node(stroke_id, stroke, &[]); let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
self.network_interface.move_node_to_chain_start(&stroke_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, &[]);
}
} }
pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: LayerNodeIdentifier) { 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(), label: "Import…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Import), shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
action: MenuBarEntry::create_action(|_| PortfolioMessage::Import.into()), 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::default()
}, },
MenuBarEntry { 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::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_core::raster::Image;
use graphene_core::text::Font; use graphene_core::text::Font;
use graphene_core::Color;
#[impl_message(Message, Portfolio)] #[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -87,6 +89,18 @@ pub enum PortfolioMessage {
PasteSerializedData { PasteSerializedData {
data: String, 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, PrevDocument,
SetActivePanel { SetActivePanel {
panel: PanelType, panel: PanelType,

View file

@ -317,9 +317,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
} }
PortfolioMessage::Import => { PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages // 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 } => { PortfolioMessage::LoadDocumentResources { document_id } => {
if let Some(document) = self.document_mut(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 => { PortfolioMessage::PrevDocument => {
if let Some(active_document_id) = self.active_document_id { if let Some(active_document_id) = self.active_document_id {
let len = self.document_ids.len(); 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 /// 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 { pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = 0; let insert_index = 0;
responses.add(DocumentMessage::ImportSvg { responses.add(GraphOperationMessage::NewSvg {
id, id,
svg, svg,
transform, transform,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,7 @@
on:dragleave on:dragleave
on:dragover on:dragover
on:dragstart on:dragstart
on:drop
on:mouseup on:mouseup
on:pointerdown on:pointerdown
on:pointerenter on:pointerenter
@ -58,7 +59,6 @@ on:copy
on:cut on:cut
on:drag on:drag
on:dragenter on:dragenter
on:drop
on:focus on:focus
on:fullscreenchange on:fullscreenchange
on:fullscreenerror on:fullscreenerror

View file

@ -42,6 +42,7 @@
on:dragleave on:dragleave
on:dragover on:dragover
on:dragstart on:dragstart
on:drop
on:mouseup on:mouseup
on:pointerdown on:pointerdown
on:pointerenter on:pointerenter
@ -58,7 +59,6 @@ on:copy
on:cut on:cut
on:drag on:drag
on:dragenter on:dragenter
on:drop
on:focus on:focus
on:fullscreenchange on:fullscreenchange
on:fullscreenerror on:fullscreenerror

View file

@ -118,23 +118,33 @@
}; };
})($document.toolShelfLayout.layout[0]); })($document.toolShelfLayout.layout[0]);
function pasteFile(e: DragEvent) { function dropFile(e: DragEvent) {
const { dataTransfer } = e; const { dataTransfer } = e;
const [x, y] = e.target instanceof Element && e.target.closest("[data-viewport]") ? [e.clientX, e.clientY] : [undefined, undefined];
if (!dataTransfer) return; if (!dataTransfer) return;
e.preventDefault(); e.preventDefault();
Array.from(dataTransfer.items).forEach(async (item) => { Array.from(dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile(); const file = item.getAsFile();
if (file?.type.includes("svg")) { if (!file) return;
const svgData = await file.text();
editor.handle.pasteSvg(svgData, e.clientX, e.clientY);
if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData, x, y);
return; return;
} }
if (file?.type.startsWith("image")) { if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file); const imageData = await extractPixelData(file);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, x, y);
return;
}
if (file.name.endsWith(".graphite")) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
return;
} }
}); });
} }
@ -426,7 +436,7 @@
}); });
</script> </script>
<LayoutCol class="document"> <LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
<LayoutRow class="options-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}> <LayoutRow class="options-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}>
{#if !$document.graphViewOverlayOpen} {#if !$document.graphViewOverlayOpen}
<WidgetLayout layout={$document.documentModeLayout} /> <WidgetLayout layout={$document.documentModeLayout} />
@ -482,7 +492,7 @@
y={cursorTop} y={cursorTop}
/> />
{/if} {/if}
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} on:dragover={(e) => e.preventDefault()} on:drop={(e) => pasteFile(e)} bind:this={viewport} data-viewport> <div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}> <svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artworkSvg} {@html artworkSvg}
</svg> </svg>

View file

@ -4,6 +4,7 @@
import { beginDraggingElement } from "@graphite/io-managers/drag"; import { beginDraggingElement } from "@graphite/io-managers/drag";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import { platformIsMac } from "@graphite/utility-functions/platform"; import { platformIsMac } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import type { Editor } from "@graphite/wasm-communication/editor"; import type { Editor } from "@graphite/wasm-communication/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages"; import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
import type { DataBuffer, LayerPanelEntry } from "@graphite/wasm-communication/messages"; import type { DataBuffer, LayerPanelEntry } from "@graphite/wasm-communication/messages";
@ -305,6 +306,8 @@
} }
function updateInsertLine(event: DragEvent) { function updateInsertLine(event: DragEvent) {
if (!draggable) return;
// Stop the drag from being shown as cancelled // Stop the drag from being shown as cancelled
event.preventDefault(); event.preventDefault();
dragInPanel = true; dragInPanel = true;
@ -312,13 +315,48 @@
if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select); if (list) draggingData = calculateDragIndex(list, event.clientY, draggingData?.select);
} }
async function drop() { function drop(e: DragEvent) {
if (draggingData && dragInPanel) { if (!draggingData) return;
const { select, insertParentId, insertIndex } = draggingData; const { select, insertParentId, insertIndex } = draggingData;
select?.(); e.preventDefault();
editor.handle.moveLayerInTree(insertParentId, insertIndex);
if (e.dataTransfer) {
// Moving layers
if (e.dataTransfer.items.length === 0) {
if (draggable && dragInPanel) {
select?.();
editor.handle.moveLayerInTree(insertParentId, insertIndex);
}
}
// Importing files
else {
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;
if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex);
return;
}
if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex);
return;
}
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
if (file.name.endsWith(".graphite")) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
return;
}
});
}
} }
draggingData = undefined; draggingData = undefined;
fakeHighlight = undefined; fakeHighlight = undefined;
dragInPanel = false; dragInPanel = false;
@ -369,7 +407,7 @@
<WidgetLayout layout={layersPanelOptionsLayout} /> <WidgetLayout layout={layersPanelOptionsLayout} />
</LayoutRow> </LayoutRow>
<LayoutRow class="list-area" scrollableY={true}> <LayoutRow class="list-area" scrollableY={true}>
<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}> <LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={updateInsertLine} on:dragend={drop} on:drop={drop}>
{#each layers as listing, index} {#each layers as listing, index}
<LayoutRow <LayoutRow
class="layer" class="layer"

View file

@ -2,8 +2,6 @@
import Document from "@graphite/components/panels/Document.svelte"; import Document from "@graphite/components/panels/Document.svelte";
import Layers from "@graphite/components/panels/Layers.svelte"; import Layers from "@graphite/components/panels/Layers.svelte";
import Properties from "@graphite/components/panels/Properties.svelte"; import Properties from "@graphite/components/panels/Properties.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
const PANEL_COMPONENTS = { const PANEL_COMPONENTS = {
Document, Document,
@ -18,11 +16,14 @@
import { platformIsMac, isEventSupported } from "@graphite/utility-functions/platform"; import { platformIsMac, isEventSupported } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import type { Editor } from "@graphite/wasm-communication/editor"; import type { Editor } from "@graphite/wasm-communication/editor";
import { type LayoutKeysGroup, type Key } from "@graphite/wasm-communication/messages"; import { type LayoutKeysGroup, type Key } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte"; import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
@ -50,6 +51,35 @@
return reservedKey ? [CONTROL, ALT] : [CONTROL]; return reservedKey ? [CONTROL, ALT] : [CONTROL];
} }
function dropFile(e: DragEvent) {
if (!e.dataTransfer) return;
e.preventDefault();
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;
if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData);
return;
}
if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
return;
}
if (file.name.endsWith(".graphite")) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
return;
}
});
}
export async function scrollTabIntoView(newIndex: number) { export async function scrollTabIntoView(newIndex: number) {
await tick(); await tick();
tabElements[newIndex]?.div?.()?.scrollIntoView(); tabElements[newIndex]?.div?.()?.scrollIntoView();
@ -76,7 +106,7 @@
} }
}} }}
on:mouseup={(e) => { on:mouseup={(e) => {
// Fallback for Safari: // Middle mouse button click fallback for Safari:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility // https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
// The downside of using mouseup is that the mousedown didn't have to originate in the same element. // The downside of using mouseup is that the mousedown didn't have to originate in the same element.
// A possible future improvement could save the target element during mousedown and check if it's the same here. // A possible future improvement could save the target element during mousedown and check if it's the same here.
@ -110,7 +140,7 @@
{#if panelType} {#if panelType}
<svelte:component this={PANEL_COMPONENTS[panelType]} /> <svelte:component this={PANEL_COMPONENTS[panelType]} />
{:else} {:else}
<LayoutCol class="empty-panel"> <LayoutCol class="empty-panel" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
<LayoutCol class="content"> <LayoutCol class="content">
<LayoutRow class="logotype"> <LayoutRow class="logotype">
<IconLabel icon="GraphiteLogotypeSolid" /> <IconLabel icon="GraphiteLogotypeSolid" />

View file

@ -11,9 +11,7 @@ export function createDragManager(): () => void {
// Return the destructor // Return the destructor
return () => { return () => {
// We use setTimeout to sequence this drop after any potential users in the current call stack progression, since this will begin in an entirely new call stack later // We use setTimeout to sequence this drop after any potential users in the current call stack progression, since this will begin in an entirely new call stack later
setTimeout(() => { setTimeout(() => document.removeEventListener("drop", clearDraggingElement), 0);
document.removeEventListener("drop", clearDraggingElement);
}, 0);
}; };
} }

View file

@ -283,17 +283,21 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
} }
const file = item.getAsFile(); const file = item.getAsFile();
if (!file) return;
if (file?.type === "svg") { if (file.type.includes("svg")) {
const text = await file.text(); const text = await file.text();
editor.handle.pasteSvg(text); editor.handle.pasteSvg(file.name, text);
return; return;
} }
if (file?.type.startsWith("image")) { if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file); const imageData = await extractPixelData(file);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
}
if (file.name.endsWith(".graphite")) {
editor.handle.openDocumentFile(file.name, await file.text());
} }
}); });
} }
@ -316,52 +320,63 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (!clipboardItems) throw new Error("Clipboard API unsupported"); if (!clipboardItems) throw new Error("Clipboard API unsupported");
// Read any layer data or images from the clipboard // Read any layer data or images from the clipboard
Array.from(clipboardItems).forEach(async (item) => { const success = await Promise.any(
// Read plain text and, if it is a layer, pass it to the editor Array.from(clipboardItems).map(async (item) => {
if (item.types.includes("text/plain")) { // Read plain text and, if it is a layer, pass it to the editor
const blob = await item.getType("text/plain"); if (item.types.includes("text/plain")) {
const reader = new FileReader(); const blob = await item.getType("text/plain");
reader.onload = () => { const reader = new FileReader();
const text = reader.result as string; reader.onload = () => {
const text = reader.result as string;
if (text.startsWith("graphite/layer: ")) { if (text.startsWith("graphite/layer: ")) {
editor.handle.pasteSerializedData(text.substring(16, text.length)); editor.handle.pasteSerializedData(text.substring(16, text.length));
} }
}; };
reader.readAsText(blob); reader.readAsText(blob);
} return true;
}
// Read an image from the clipboard and pass it to the editor to be loaded // Read an image from the clipboard and pass it to the editor to be loaded
const imageType = item.types.find((type) => type.startsWith("image/")); const imageType = item.types.find((type) => type.startsWith("image/"));
if (imageType === "svg") { // Import the actual SVG content if it's an SVG
const blob = await item.getType("text/plain"); if (imageType?.includes("svg")) {
const reader = new FileReader(); const blob = await item.getType("text/plain");
reader.onload = () => { const reader = new FileReader();
const text = reader.result as string; reader.onload = () => {
editor.handle.pasteSvg(text); const text = reader.result as string;
}; editor.handle.pasteSvg(undefined, text);
reader.readAsText(blob); };
reader.readAsText(blob);
return true;
}
return; // Import the bitmap image if it's an image
} if (imageType) {
const blob = await item.getType(imageType);
const reader = new FileReader();
reader.onload = async () => {
if (reader.result instanceof ArrayBuffer) {
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
editor.handle.pasteImage(undefined, new Uint8Array(imageData.data), imageData.width, imageData.height);
}
};
reader.readAsArrayBuffer(blob);
return true;
}
if (imageType) { // The API limits what kinds of data we can access, so we can get copied images and our text encodings of copied nodes, but not files (like
const blob = await item.getType(imageType); // .graphite or even image files). However, the user can paste those with Ctrl+V, which we recommend they in the error message that's shown to them.
const reader = new FileReader(); return false;
reader.onload = async () => { }),
if (reader.result instanceof ArrayBuffer) { );
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); if (!success) throw new Error("No valid clipboard data");
}
};
reader.readAsArrayBuffer(blob);
}
});
} catch (err) { } catch (err) {
const unsupported = stripIndents` const unsupported = stripIndents`
This browser does not support reading from the clipboard. This browser does not support reading from the clipboard.
Use the keyboard shortcut to paste instead. Use the standard keyboard shortcut to paste instead.
`; `;
const denied = stripIndents` const denied = stripIndents`
The browser's clipboard permission has been denied. The browser's clipboard permission has been denied.
@ -369,11 +384,16 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
Open the browser's website settings (usually accessible Open the browser's website settings (usually accessible
just left of the URL) to allow this permission. just left of the URL) to allow this permission.
`; `;
const nothing = stripIndents`
No valid clipboard data was found. You may have better
luck pasting with the standard keyboard shortcut instead.
`;
const matchMessage = { const matchMessage = {
"clipboard-read": unsupported, "clipboard-read": unsupported,
"Clipboard API unsupported": unsupported, "Clipboard API unsupported": unsupported,
"Permission denied": denied, "Permission denied": denied,
"No valid clipboard data": nothing,
}; };
const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err); const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);

View file

@ -92,11 +92,9 @@ export function createDocumentState(editor: Editor) {
}); });
editor.subscriptions.subscribeJsMessage(TriggerDelayedZoomCanvasToFitAll, () => { editor.subscriptions.subscribeJsMessage(TriggerDelayedZoomCanvasToFitAll, () => {
// TODO: This is horribly hacky // TODO: This is horribly hacky
setTimeout(() => editor.handle.zoomCanvasToFitAll(), 0); [0, 1, 10, 50, 100, 200, 300, 400, 500].forEach((delay) => {
setTimeout(() => editor.handle.zoomCanvasToFitAll(), 1); setTimeout(() => editor.handle.zoomCanvasToFitAll(), delay);
setTimeout(() => editor.handle.zoomCanvasToFitAll(), 10); });
setTimeout(() => editor.handle.zoomCanvasToFitAll(), 50);
setTimeout(() => editor.handle.zoomCanvasToFitAll(), 100);
}); });
return { return {

View file

@ -65,17 +65,22 @@ export function createPortfolioState(editor: Editor) {
editor.handle.openDocumentFile(data.filename, data.content); editor.handle.openDocumentFile(data.filename, data.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
const data = await upload("image/*", "data"); const data = await upload("image/*", "both");
if (data.type.includes("svg")) { if (data.type.includes("svg")) {
const svg = new TextDecoder().decode(data.content); const svg = new TextDecoder().decode(data.content.data);
editor.handle.pasteSvg(svg); editor.handle.pasteSvg(data.filename, svg);
return; return;
} }
const imageData = await extractPixelData(new Blob([data.content], { type: data.type })); // In case the user accidentally uploads a Graphite file, open it instead of failing to import it
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height); if (data.filename.endsWith(".graphite")) {
editor.handle.openDocumentFile(data.filename, data.content.text);
return;
}
const imageData = await extractPixelData(new Blob([data.content.data], { type: data.type }));
editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height);
}); });
editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => { editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => {
downloadFileText(triggerFileDownload.name, triggerFileDownload.document); downloadFileText(triggerFileDownload.name, triggerFileDownload.document);

View file

@ -22,7 +22,7 @@ export function downloadFileText(filename: string, text: string) {
downloadFileBlob(filename, blob); downloadFileBlob(filename, blob);
} }
export async function upload<T extends "text" | "data">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> { export async function upload<T extends "text" | "data" | "both">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
return new Promise<UploadResult<T>>((resolve, _) => { return new Promise<UploadResult<T>>((resolve, _) => {
const element = document.createElement("input"); const element = document.createElement("input");
element.type = "file"; element.type = "file";
@ -36,7 +36,15 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
const filename = file.name; const filename = file.name;
const type = file.type; const type = file.type;
const content = (textOrData === "text" ? await file.text() : new Uint8Array(await file.arrayBuffer())) as UploadResultType<T>; const content = (
textOrData === "text"
? await file.text()
: textOrData === "data"
? new Uint8Array(await file.arrayBuffer())
: textOrData === "both"
? { text: await file.text(), data: new Uint8Array(await file.arrayBuffer()) }
: undefined
) as UploadResultType<T>;
resolve({ filename, type, content }); resolve({ filename, type, content });
} }
@ -50,7 +58,7 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
}); });
} }
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> }; export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never; type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never;
export function blobToBase64(blob: Blob): Promise<string> { export function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {

View file

@ -129,7 +129,8 @@ export abstract class DocumentDetails {
readonly isSaved!: boolean; readonly isSaved!: boolean;
readonly id!: bigint | string; // This field must be provided by the subclass implementation
// readonly id!: bigint | string;
get displayName(): string { get displayName(): string {
return `${this.name}${this.isSaved ? "" : "*"}`; return `${this.name}${this.isSaved ? "" : "*"}`;

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ESNext",
"module": "esnext", "module": "ESNext",
"strict": true, "strict": true,
"importHelpers": true, "importHelpers": true,
"moduleResolution": "node", "moduleResolution": "node",
@ -18,7 +18,7 @@
"@graphite-frontend/*": ["./*"], "@graphite-frontend/*": ["./*"],
"@graphite/*": ["src/*"] "@graphite/*": ["src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte", "*.ts", "*.js", "*.cjs"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte", "*.ts", "*.js", "*.cjs"],
"exclude": ["node_modules"], "exclude": ["node_modules"],

View file

@ -593,17 +593,55 @@ impl EditorHandle {
/// Pastes an image /// Pastes an image
#[wasm_bindgen(js_name = pasteImage)] #[wasm_bindgen(js_name = pasteImage)]
pub fn paste_image(&self, image_data: Vec<u8>, width: u32, height: u32, mouse_x: Option<f64>, mouse_y: Option<f64>) { pub fn paste_image(
&self,
name: Option<String>,
image_data: Vec<u8>,
width: u32,
height: u32,
mouse_x: Option<f64>,
mouse_y: Option<f64>,
insert_parent_id: Option<u64>,
insert_index: Option<usize>,
) {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y))); let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let image = graphene_core::raster::Image::from_image_data(&image_data, width, height); let image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
let message = DocumentMessage::PasteImage { image, mouse };
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
Some((parent, insert_index))
} else {
None
};
let message = PortfolioMessage::PasteImage {
name,
image,
mouse,
parent_and_insert_index,
};
self.dispatch(message); self.dispatch(message);
} }
#[wasm_bindgen(js_name = pasteSvg)] #[wasm_bindgen(js_name = pasteSvg)]
pub fn paste_svg(&self, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>) { pub fn paste_svg(&self, name: Option<String>, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y))); let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let message = DocumentMessage::PasteSvg { svg, mouse };
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
Some((parent, insert_index))
} else {
None
};
let message = PortfolioMessage::PasteSvg {
name,
svg,
mouse,
parent_and_insert_index,
};
self.dispatch(message); self.dispatch(message);
} }

View file

@ -141,7 +141,7 @@ async fn stroke<F: 'n + Send, T: Into<Option<Color>> + 'n + Send>(
)] )]
#[default(Color::BLACK)] #[default(Color::BLACK)]
color: T, color: T,
#[default(5.)] weight: f64, #[default(2.)] weight: f64,
dash_lengths: Vec<f64>, dash_lengths: Vec<f64>,
dash_offset: f64, dash_offset: f64,
line_cap: crate::vector::style::LineCap, line_cap: crate::vector::style::LineCap,

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ESNext",
"module": "esnext", "module": "ESNext",
"strict": true, "strict": true,
"importHelpers": true, "importHelpers": true,
"moduleResolution": "node", "moduleResolution": "node",
@ -14,7 +14,7 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "*.ts", "*.js", "*.cjs"], "include": ["src/**/*.ts", "src/**/*.d.ts", "*.ts", "*.js", "*.cjs"],
"exclude": ["node_modules"], "exclude": ["node_modules"],