mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
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:
parent
20470b566b
commit
904cf09c79
35 changed files with 578 additions and 259 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()],
|
||||||
},
|
},
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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. });
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.,
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 ? "" : "*"}`;
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue