Add embedable images (#564)
* Add embedable bitmaps * Initial work on blob urls * Finish implementing data url * Fix some bugs * Rename bitmap to image * Fix loading image on document load * Add transform properties for image * Remove some logging * Add image dimensions * Implement system copy and paste * Fix pasting images * Fix test * Address code review Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
0ee492a857
commit
51c31f042b
|
|
@ -64,6 +64,12 @@ impl Dispatcher {
|
||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
NoOp => {}
|
NoOp => {}
|
||||||
Frontend(message) => {
|
Frontend(message) => {
|
||||||
|
// Image data should be immediatly handled
|
||||||
|
if let FrontendMessage::UpdateImageData { .. } = message {
|
||||||
|
self.responses.push(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
|
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
|
||||||
self.responses.push(message);
|
self.responses.push(message);
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +175,9 @@ mod test {
|
||||||
let mut editor = create_editor_with_three_layers();
|
let mut editor = create_editor_with_three_layers();
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User });
|
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
|
|
@ -208,9 +214,9 @@ mod test {
|
||||||
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
||||||
replacement_selected_layers: vec![vec![shape_id]],
|
replacement_selected_layers: vec![vec![shape_id]],
|
||||||
});
|
});
|
||||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User });
|
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
|
|
@ -273,15 +279,15 @@ mod test {
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User });
|
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
|
|
@ -344,16 +350,16 @@ mod test {
|
||||||
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
||||||
replacement_selected_layers: vec![vec![rect_id], vec![ellipse_id]],
|
replacement_selected_layers: vec![vec![rect_id], vec![ellipse_id]],
|
||||||
});
|
});
|
||||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User });
|
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
||||||
editor.draw_rect(0., 800., 12., 200.);
|
editor.draw_rect(0., 800., 12., 200.);
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::User,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: vec![],
|
folder_path: vec![],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ use serde::{Deserialize, Serialize};
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum Clipboard {
|
pub enum Clipboard {
|
||||||
System,
|
Internal,
|
||||||
User,
|
|
||||||
_ClipboardCount, // Keep this as the last entry since it is used for counting the number of enum variants
|
_InternalClipboardCount, // Keep this as the last entry in internal clipboards since it is used for counting the number of enum variants
|
||||||
|
|
||||||
|
Device,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
|
pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CopyBufferEntry {
|
pub struct CopyBufferEntry {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@ pub enum DocumentMessage {
|
||||||
delta_x: f64,
|
delta_x: f64,
|
||||||
delta_y: f64,
|
delta_y: f64,
|
||||||
},
|
},
|
||||||
|
PasteImage {
|
||||||
|
mime: String,
|
||||||
|
image_data: Vec<u8>,
|
||||||
|
mouse: Option<(f64, f64)>,
|
||||||
|
},
|
||||||
Redo,
|
Redo,
|
||||||
RenameLayer {
|
RenameLayer {
|
||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandl
|
||||||
use crate::consts::{
|
use crate::consts::{
|
||||||
ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
|
ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
|
||||||
};
|
};
|
||||||
|
use crate::frontend::utility_types::FrontendImageData;
|
||||||
use crate::input::InputPreprocessorMessageHandler;
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
use crate::layout::widgets::{
|
use crate::layout::widgets::{
|
||||||
IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget,
|
IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget,
|
||||||
|
|
@ -470,6 +471,33 @@ impl DocumentMessageHandler {
|
||||||
path.push(generate_uuid());
|
path.push(generate_uuid());
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates the blob URLs for the image data in the document
|
||||||
|
pub fn load_image_data(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>) {
|
||||||
|
let mut image_data = Vec::new();
|
||||||
|
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, responses: &mut VecDeque<Message>, image_data: &mut Vec<FrontendImageData>) {
|
||||||
|
match data {
|
||||||
|
LayerDataType::Folder(f) => {
|
||||||
|
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) {
|
||||||
|
path.push(*id);
|
||||||
|
walk_layers(&layer.data, path, responses, image_data);
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LayerDataType::Image(img) => image_data.push(FrontendImageData {
|
||||||
|
path: path.clone(),
|
||||||
|
image_data: img.image_data.clone(),
|
||||||
|
mime: img.mime.clone(),
|
||||||
|
}),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk_layers(root, &mut path, responses, &mut image_data);
|
||||||
|
if !image_data.is_empty() {
|
||||||
|
responses.push_front(FrontendMessage::UpdateImageData { image_data }.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyHolder for DocumentMessageHandler {
|
impl PropertyHolder for DocumentMessageHandler {
|
||||||
|
|
@ -783,7 +811,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
}
|
}
|
||||||
DirtyRenderDocument => {
|
DirtyRenderDocument => {
|
||||||
// Mark all non-overlay caches as dirty
|
// Mark all non-overlay caches as dirty
|
||||||
GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {});
|
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||||
|
|
||||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||||
}
|
}
|
||||||
|
|
@ -865,13 +893,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
|
|
||||||
new_folder_path.push(generate_uuid());
|
new_folder_path.push(generate_uuid());
|
||||||
|
|
||||||
responses.push_back(PortfolioMessage::Copy { clipboard: Clipboard::System }.into());
|
responses.push_back(PortfolioMessage::Copy { clipboard: Clipboard::Internal }.into());
|
||||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||||
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
|
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
|
||||||
responses.push_back(DocumentMessage::ToggleLayerExpansion { layer_path: new_folder_path.clone() }.into());
|
responses.push_back(DocumentMessage::ToggleLayerExpansion { layer_path: new_folder_path.clone() }.into());
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
PortfolioMessage::PasteIntoFolder {
|
PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::System,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: new_folder_path.clone(),
|
folder_path: new_folder_path.clone(),
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
}
|
}
|
||||||
|
|
@ -904,11 +932,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
|
|
||||||
let insert_index = self.update_insert_index(&selected_layers, &folder_path, insert_index, reverse_index).unwrap();
|
let insert_index = self.update_insert_index(&selected_layers, &folder_path, insert_index, reverse_index).unwrap();
|
||||||
|
|
||||||
responses.push_back(PortfolioMessage::Copy { clipboard: Clipboard::System }.into());
|
responses.push_back(PortfolioMessage::Copy { clipboard: Clipboard::Internal }.into());
|
||||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
PortfolioMessage::PasteIntoFolder {
|
PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::System,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path,
|
folder_path,
|
||||||
insert_index,
|
insert_index,
|
||||||
}
|
}
|
||||||
|
|
@ -926,6 +954,39 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
}
|
}
|
||||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
}
|
}
|
||||||
|
PasteImage { mime, image_data, mouse } => {
|
||||||
|
let path = vec![generate_uuid()];
|
||||||
|
responses.push_front(
|
||||||
|
FrontendMessage::UpdateImageData {
|
||||||
|
image_data: vec![FrontendImageData {
|
||||||
|
path: path.clone(),
|
||||||
|
image_data: image_data.clone(),
|
||||||
|
mime: mime.clone(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(
|
||||||
|
DocumentOperation::AddImage {
|
||||||
|
path: path.clone(),
|
||||||
|
transform: DAffine2::ZERO.to_cols_array(),
|
||||||
|
insert_index: -1,
|
||||||
|
mime,
|
||||||
|
image_data,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(
|
||||||
|
DocumentMessage::SetSelectedLayers {
|
||||||
|
replacement_selected_layers: vec![path.clone()],
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mouse = mouse.map_or(ipp.mouse.position, |pos| pos.into());
|
||||||
|
let transform = DAffine2::from_translation(mouse - ipp.viewport_bounds.top_left).to_cols_array();
|
||||||
|
responses.push_back(DocumentOperation::SetLayerTransformInViewport { path, transform }.into());
|
||||||
|
}
|
||||||
Redo => {
|
Redo => {
|
||||||
responses.push_back(SelectToolMessage::Abort.into());
|
responses.push_back(SelectToolMessage::Abort.into());
|
||||||
responses.push_back(DocumentHistoryForward.into());
|
responses.push_back(DocumentHistoryForward.into());
|
||||||
|
|
@ -1200,10 +1261,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
||||||
// Select them
|
// Select them
|
||||||
DocumentMessage::SetSelectedLayers { replacement_selected_layers: select }.into(),
|
DocumentMessage::SetSelectedLayers { replacement_selected_layers: select }.into(),
|
||||||
// Copy them
|
// Copy them
|
||||||
PortfolioMessage::Copy { clipboard: Clipboard::System }.into(),
|
PortfolioMessage::Copy { clipboard: Clipboard::Internal }.into(),
|
||||||
// Paste them into the folder above
|
// Paste them into the folder above
|
||||||
PortfolioMessage::PasteIntoFolder {
|
PortfolioMessage::PasteIntoFolder {
|
||||||
clipboard: Clipboard::System,
|
clipboard: Clipboard::Internal,
|
||||||
folder_path: folder_path[..folder_path.len() - 1].to_vec(),
|
folder_path: folder_path[..folder_path.len() - 1].to_vec(),
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ pub enum LayerDataTypeDiscriminant {
|
||||||
Folder,
|
Folder,
|
||||||
Shape,
|
Shape,
|
||||||
Text,
|
Text,
|
||||||
|
Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for LayerDataTypeDiscriminant {
|
impl fmt::Display for LayerDataTypeDiscriminant {
|
||||||
|
|
@ -108,6 +109,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
|
||||||
LayerDataTypeDiscriminant::Folder => "Folder",
|
LayerDataTypeDiscriminant::Folder => "Folder",
|
||||||
LayerDataTypeDiscriminant::Shape => "Shape",
|
LayerDataTypeDiscriminant::Shape => "Shape",
|
||||||
LayerDataTypeDiscriminant::Text => "Text",
|
LayerDataTypeDiscriminant::Text => "Text",
|
||||||
|
LayerDataTypeDiscriminant::Image => "Image",
|
||||||
};
|
};
|
||||||
|
|
||||||
formatter.write_str(name)
|
formatter.write_str(name)
|
||||||
|
|
@ -122,6 +124,7 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
|
||||||
Folder(_) => LayerDataTypeDiscriminant::Folder,
|
Folder(_) => LayerDataTypeDiscriminant::Folder,
|
||||||
Shape(_) => LayerDataTypeDiscriminant::Shape,
|
Shape(_) => LayerDataTypeDiscriminant::Shape,
|
||||||
Text(_) => LayerDataTypeDiscriminant::Text,
|
Text(_) => LayerDataTypeDiscriminant::Text,
|
||||||
|
Image(_) => LayerDataTypeDiscriminant::Image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ pub enum PortfolioMessage {
|
||||||
folder_path: Vec<LayerId>,
|
folder_path: Vec<LayerId>,
|
||||||
insert_index: isize,
|
insert_index: isize,
|
||||||
},
|
},
|
||||||
|
PasteSerializedData {
|
||||||
|
data: String,
|
||||||
|
},
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
RequestAboutGraphiteDialog,
|
RequestAboutGraphiteDialog,
|
||||||
SelectDocument {
|
SelectDocument {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::clipboards::{CopyBufferEntry, CLIPBOARD_COUNT};
|
use super::clipboards::{CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
|
||||||
use super::DocumentMessageHandler;
|
use super::DocumentMessageHandler;
|
||||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
|
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
|
||||||
use crate::frontend::utility_types::FrontendDocumentDetails;
|
use crate::frontend::utility_types::FrontendDocumentDetails;
|
||||||
|
|
@ -17,7 +17,7 @@ pub struct PortfolioMessageHandler {
|
||||||
documents: HashMap<u64, DocumentMessageHandler>,
|
documents: HashMap<u64, DocumentMessageHandler>,
|
||||||
document_ids: Vec<u64>,
|
document_ids: Vec<u64>,
|
||||||
active_document_id: u64,
|
active_document_id: u64,
|
||||||
copy_buffer: [Vec<CopyBufferEntry>; CLIPBOARD_COUNT as usize],
|
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PortfolioMessageHandler {
|
impl PortfolioMessageHandler {
|
||||||
|
|
@ -78,6 +78,8 @@ impl PortfolioMessageHandler {
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new());
|
||||||
|
|
||||||
self.documents.insert(document_id, new_document);
|
self.documents.insert(document_id, new_document);
|
||||||
|
|
||||||
// Send the new list of document tab names
|
// Send the new list of document tab names
|
||||||
|
|
@ -119,7 +121,7 @@ impl Default for PortfolioMessageHandler {
|
||||||
Self {
|
Self {
|
||||||
documents: documents_map,
|
documents: documents_map,
|
||||||
document_ids: vec![starting_key],
|
document_ids: vec![starting_key],
|
||||||
copy_buffer: [EMPTY_VEC; CLIPBOARD_COUNT as usize],
|
copy_buffer: [EMPTY_VEC; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||||
active_document_id: starting_key,
|
active_document_id: starting_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -228,16 +230,28 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||||
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
|
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
|
||||||
let active_document = self.documents.get(&self.active_document_id).unwrap();
|
let active_document = self.documents.get(&self.active_document_id).unwrap();
|
||||||
|
|
||||||
let copy_buffer = &mut self.copy_buffer;
|
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
|
||||||
copy_buffer[clipboard as usize].clear();
|
for layer_path in active_document.selected_layers_without_children() {
|
||||||
|
match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) {
|
||||||
for layer_path in active_document.selected_layers_without_children() {
|
(Ok(layer), layer_metadata) => {
|
||||||
match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) {
|
buffer.push(CopyBufferEntry { layer, layer_metadata });
|
||||||
(Ok(layer), layer_metadata) => {
|
}
|
||||||
copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_metadata });
|
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
|
||||||
}
|
}
|
||||||
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if clipboard == Clipboard::Device {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
copy_val(&mut buffer);
|
||||||
|
let mut copy_text = String::from("graphite/layer: ");
|
||||||
|
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste");
|
||||||
|
|
||||||
|
responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into());
|
||||||
|
} else {
|
||||||
|
let copy_buffer = &mut self.copy_buffer;
|
||||||
|
copy_buffer[clipboard as usize].clear();
|
||||||
|
copy_val(&mut copy_buffer[clipboard as usize]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Cut { clipboard } => {
|
Cut { clipboard } => {
|
||||||
|
|
@ -331,6 +345,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone());
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
DocumentOperation::InsertLayer {
|
DocumentOperation::InsertLayer {
|
||||||
layer: entry.layer.clone(),
|
layer: entry.layer.clone(),
|
||||||
|
|
@ -351,6 +366,40 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PasteSerializedData { data } => {
|
||||||
|
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
|
||||||
|
let document = self.active_document();
|
||||||
|
let shallowest_common_folder = document
|
||||||
|
.graphene_document
|
||||||
|
.shallowest_common_folder(document.selected_layers())
|
||||||
|
.expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||||
|
responses.push_back(DeselectAllLayers.into());
|
||||||
|
responses.push_back(StartTransaction.into());
|
||||||
|
|
||||||
|
for entry in data {
|
||||||
|
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
|
||||||
|
|
||||||
|
responses.push_front(
|
||||||
|
DocumentMessage::UpdateLayerMetadata {
|
||||||
|
layer_path: destination_path.clone(),
|
||||||
|
layer_metadata: entry.layer_metadata,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone());
|
||||||
|
responses.push_front(
|
||||||
|
DocumentOperation::InsertLayer {
|
||||||
|
layer: entry.layer.clone(),
|
||||||
|
destination_path,
|
||||||
|
insert_index: -1,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.push_back(CommitTransaction.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
PrevDocument => {
|
PrevDocument => {
|
||||||
let len = self.document_ids.len();
|
let len = self.document_ids.len();
|
||||||
let current_index = self.document_index(self.active_document_id);
|
let current_index = self.document_index(self.active_document_id);
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,10 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||||
icon: "NodeText".into(),
|
icon: "NodeText".into(),
|
||||||
gap_after: true,
|
gap_after: true,
|
||||||
})),
|
})),
|
||||||
|
LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||||
|
icon: "NodeImage".into(),
|
||||||
|
gap_after: true,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
|
|
@ -260,9 +264,6 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let properties_body = match &layer.data {
|
let properties_body = match &layer.data {
|
||||||
LayerDataType::Folder(_) => {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
LayerDataType::Shape(shape) => {
|
LayerDataType::Shape(shape) => {
|
||||||
if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
|
if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
|
||||||
vec![node_section_transform(layer), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
vec![node_section_transform(layer), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||||
|
|
@ -277,6 +278,12 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||||
node_section_stroke(&text.style.stroke().unwrap_or_default()),
|
node_section_stroke(&text.style.stroke().unwrap_or_default()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
LayerDataType::Image(_) => {
|
||||||
|
vec![node_section_transform(layer)]
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
|
use super::utility_types::{FrontendDocumentDetails, FrontendImageData, MouseCursorIcon};
|
||||||
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
|
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
|
||||||
use crate::layout::layout_message::LayoutTarget;
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
use crate::layout::widgets::SubLayout;
|
use crate::layout::widgets::SubLayout;
|
||||||
|
|
@ -29,6 +29,7 @@ pub enum FrontendMessage {
|
||||||
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
||||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
|
TriggerTextCopy { copy_text: String },
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
|
|
||||||
// Update prefix: give the frontend a new value or state for it to use
|
// Update prefix: give the frontend a new value or state for it to use
|
||||||
|
|
@ -43,6 +44,7 @@ pub enum FrontendMessage {
|
||||||
UpdateDocumentOverlays { svg: String },
|
UpdateDocumentOverlays { svg: String },
|
||||||
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
||||||
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
||||||
|
UpdateImageData { image_data: Vec<FrontendImageData> },
|
||||||
UpdateInputHints { hint_data: HintData },
|
UpdateInputHints { hint_data: HintData },
|
||||||
UpdateMouseCursor { cursor: MouseCursorIcon },
|
UpdateMouseCursor { cursor: MouseCursorIcon },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use graphene::LayerId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||||
|
|
@ -7,6 +8,13 @@ pub struct FrontendDocumentDetails {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||||
|
pub struct FrontendImageData {
|
||||||
|
pub path: Vec<LayerId>,
|
||||||
|
pub mime: String,
|
||||||
|
pub image_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)]
|
||||||
pub enum MouseCursorIcon {
|
pub enum MouseCursorIcon {
|
||||||
Default,
|
Default,
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,8 @@ impl Default for Mapping {
|
||||||
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
||||||
entry! {action=PortfolioMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
entry! {action=PortfolioMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||||
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::Paste { clipboard: Clipboard::User }, key_down=KeyV, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::User }, key_down=KeyC, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::User }, key_down=KeyX, modifiers=[KeyControl]},
|
|
||||||
// Nudging
|
// Nudging
|
||||||
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
|
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
|
||||||
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
|
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,23 @@
|
||||||
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" ref="rulerVertical" />
|
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" ref="rulerVertical" />
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="canvas-area">
|
<LayoutCol class="canvas-area">
|
||||||
<div class="canvas" data-canvas ref="canvas" :style="{ cursor: canvasCursor }" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)">
|
<div
|
||||||
|
class="canvas"
|
||||||
|
data-canvas
|
||||||
|
ref="canvas"
|
||||||
|
:style="{ cursor: canvasCursor }"
|
||||||
|
@pointerdown="(e: PointerEvent) => canvasPointerDown(e)"
|
||||||
|
@dragover="(e) => e.preventDefault()"
|
||||||
|
@drop="(e) => pasteFile(e)"
|
||||||
|
>
|
||||||
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
||||||
<svg class="artwork" v-html="artworkSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
<svg
|
||||||
|
class="artwork"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
v-html="artworkSvg"
|
||||||
|
:style="{ width: canvasSvgWidth, height: canvasSvgHeight }"
|
||||||
|
></svg>
|
||||||
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
|
||||||
</div>
|
</div>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|
@ -267,7 +281,9 @@ import {
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
defaultWidgetLayout,
|
defaultWidgetLayout,
|
||||||
UpdateDocumentBarLayout,
|
UpdateDocumentBarLayout,
|
||||||
|
UpdateImageData,
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
|
TriggerTextCopy,
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
|
|
@ -312,6 +328,22 @@ export default defineComponent({
|
||||||
if (rulerHorizontal) rulerHorizontal.handleResize();
|
if (rulerHorizontal) rulerHorizontal.handleResize();
|
||||||
if (rulerVertical) rulerVertical.handleResize();
|
if (rulerVertical) rulerVertical.handleResize();
|
||||||
},
|
},
|
||||||
|
pasteFile(e: DragEvent) {
|
||||||
|
const { dataTransfer } = e;
|
||||||
|
if (!dataTransfer) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
Array.from(dataTransfer.items).forEach((item) => {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file && file.type.startsWith("image")) {
|
||||||
|
file.arrayBuffer().then((buffer): void => {
|
||||||
|
const u8Array = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
this.editor.instance.paste_image(file.type, u8Array, e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
translateCanvasX(newValue: number) {
|
translateCanvasX(newValue: number) {
|
||||||
const delta = newValue - this.scrollbarPos.x;
|
const delta = newValue - this.scrollbarPos.x;
|
||||||
this.scrollbarPos.x = newValue;
|
this.scrollbarPos.x = newValue;
|
||||||
|
|
@ -421,6 +453,15 @@ export default defineComponent({
|
||||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
|
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
|
||||||
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText));
|
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText));
|
||||||
});
|
});
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => {
|
||||||
|
// Clipboard API supported?
|
||||||
|
if (!navigator.clipboard) return;
|
||||||
|
|
||||||
|
// copy text to clipboard
|
||||||
|
if (navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(triggerTextCopy.copy_text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
|
this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
|
||||||
this.textInput = document.createElement("DIV") as HTMLDivElement;
|
this.textInput = document.createElement("DIV") as HTMLDivElement;
|
||||||
|
|
@ -457,6 +498,19 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
||||||
|
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||||
|
updateImageData.image_data.forEach((element) => {
|
||||||
|
// Using updateImageData.image_data.buffer returns undefined for some reason?
|
||||||
|
const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
createImageBitmap(blob).then((image) => {
|
||||||
|
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||||
// Get initial Document Bar
|
// Get initial Document Bar
|
||||||
this.editor.instance.init_document_bar();
|
this.editor.instance.init_document_bar();
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@
|
||||||
>
|
>
|
||||||
<LayoutRow class="layer-type-icon">
|
<LayoutRow class="layer-type-icon">
|
||||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
|
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
|
||||||
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" title="Path" />
|
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" title="Image" />
|
||||||
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" title="Path" />
|
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" title="Shape" />
|
||||||
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" title="Path" />
|
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" title="Path" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||||
[
|
[
|
||||||
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async (): Promise<void> => editor.instance.cut() },
|
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async (): Promise<void> => editor.instance.cut() },
|
||||||
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async (): Promise<void> => editor.instance.copy() },
|
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async (): Promise<void> => editor.instance.copy() },
|
||||||
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
|
// TODO: Fix this
|
||||||
|
// { label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,10 @@ export class DisplayEditableTextbox extends JsMessage {
|
||||||
readonly color!: Color;
|
readonly color!: Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateImageData extends JsMessage {
|
||||||
|
readonly image_data!: ImageData[];
|
||||||
|
}
|
||||||
|
|
||||||
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||||
|
|
||||||
export class UpdateDocumentLayer extends JsMessage {
|
export class UpdateDocumentLayer extends JsMessage {
|
||||||
|
|
@ -371,6 +375,14 @@ export class LayerMetadata {
|
||||||
|
|
||||||
export type LayerType = "Folder" | "Image" | "Shape" | "Text";
|
export type LayerType = "Folder" | "Image" | "Shape" | "Text";
|
||||||
|
|
||||||
|
export class ImageData {
|
||||||
|
readonly path!: BigUint64Array;
|
||||||
|
|
||||||
|
readonly mime!: string;
|
||||||
|
|
||||||
|
readonly image_data!: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
export class IndexedDbDocumentDetails extends DocumentDetails {
|
export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||||
@Transform(({ value }: { value: BigInt }) => value.toString())
|
@Transform(({ value }: { value: BigInt }) => value.toString())
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
@ -488,6 +500,10 @@ export class DisplayDialogComingSoon extends JsMessage {
|
||||||
|
|
||||||
export class TriggerTextCommit extends JsMessage {}
|
export class TriggerTextCommit extends JsMessage {}
|
||||||
|
|
||||||
|
export class TriggerTextCopy extends JsMessage {
|
||||||
|
readonly copy_text!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class TriggerViewportResize extends JsMessage {}
|
export class TriggerViewportResize extends JsMessage {}
|
||||||
|
|
||||||
// Any is used since the type of the object should be known from the rust side
|
// Any is used since the type of the object should be known from the rust side
|
||||||
|
|
@ -504,12 +520,14 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
||||||
DisplayDialogPanic,
|
DisplayDialogPanic,
|
||||||
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
|
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
|
UpdateImageData,
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
TriggerFileDownload,
|
TriggerFileDownload,
|
||||||
TriggerFileUpload,
|
TriggerFileUpload,
|
||||||
TriggerIndexedDbRemoveDocument,
|
TriggerIndexedDbRemoveDocument,
|
||||||
TriggerIndexedDbWriteDocument,
|
TriggerIndexedDbWriteDocument,
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
|
TriggerTextCopy,
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateActiveTool,
|
UpdateActiveTool,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||||
{ target: window, eventName: "mousedown", action: (e: MouseEvent): void => onMouseDown(e) },
|
{ target: window, eventName: "mousedown", action: (e: MouseEvent): void => onMouseDown(e) },
|
||||||
{ target: window, eventName: "wheel", action: (e: WheelEvent): void => onMouseScroll(e), options: { passive: false } },
|
{ target: window, eventName: "wheel", action: (e: WheelEvent): void => onMouseScroll(e), options: { passive: false } },
|
||||||
{ target: window, eventName: "modifyinputfield", action: (e: CustomEvent): void => onModifyInputField(e) },
|
{ target: window, eventName: "modifyinputfield", action: (e: CustomEvent): void => onModifyInputField(e) },
|
||||||
|
{ target: window.document.body, eventName: "paste", action: (e: ClipboardEvent): void => onPaste(e) },
|
||||||
];
|
];
|
||||||
|
|
||||||
let viewportPointerInteractionOngoing = false;
|
let viewportPointerInteractionOngoing = false;
|
||||||
|
|
@ -45,6 +46,9 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||||
if (key !== "escape" && !(key === "enter" && e.ctrlKey) && target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable))
|
if (key !== "escape" && !(key === "enter" && e.ctrlKey) && target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Don't redirect paste
|
||||||
|
if (key === "v" && e.ctrlKey) return false;
|
||||||
|
|
||||||
// Don't redirect a fullscreen request
|
// Don't redirect a fullscreen request
|
||||||
if (key === "f11" && e.type === "keydown" && !e.repeat) {
|
if (key === "f11" && e.type === "keydown" && !e.repeat) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -208,6 +212,31 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onPaste = (e: ClipboardEvent): void => {
|
||||||
|
const dataTransfer = e.clipboardData;
|
||||||
|
if (!dataTransfer) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
Array.from(dataTransfer.items).forEach((item) => {
|
||||||
|
if (item.type === "text/plain") {
|
||||||
|
item.getAsString((text) => {
|
||||||
|
if (text.startsWith("graphite/layer: ")) {
|
||||||
|
editor.instance.paste_serialized_data(text.substring(16, text.length));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file && file.type.startsWith("image")) {
|
||||||
|
file.arrayBuffer().then((buffer): void => {
|
||||||
|
const u8Array = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
editor.instance.paste_image(file.type, u8Array, undefined, undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Event bindings
|
// Event bindings
|
||||||
|
|
||||||
const addListeners = (): void => {
|
const addListeners = (): void => {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use editor::viewport_tools::tools;
|
||||||
use editor::Color;
|
use editor::Color;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use editor::LayerId;
|
use editor::LayerId;
|
||||||
|
use graphene::Operation;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_wasm_bindgen::{self, from_value};
|
use serde_wasm_bindgen::{self, from_value};
|
||||||
|
|
@ -384,19 +385,19 @@ impl JsEditorHandle {
|
||||||
|
|
||||||
/// Cut selected layers
|
/// Cut selected layers
|
||||||
pub fn cut(&self) {
|
pub fn cut(&self) {
|
||||||
let message = PortfolioMessage::Cut { clipboard: Clipboard::User };
|
let message = PortfolioMessage::Cut { clipboard: Clipboard::Device };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy selected layers
|
/// Copy selected layers
|
||||||
pub fn copy(&self) {
|
pub fn copy(&self) {
|
||||||
let message = PortfolioMessage::Copy { clipboard: Clipboard::User };
|
let message = PortfolioMessage::Copy { clipboard: Clipboard::Device };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paste selected layers
|
/// Paste layers from a serialized json representation
|
||||||
pub fn paste(&self) {
|
pub fn paste_serialized_data(&self, data: String) {
|
||||||
let message = PortfolioMessage::Paste { clipboard: Clipboard::User };
|
let message = PortfolioMessage::PasteSerializedData { data };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,6 +491,20 @@ impl JsEditorHandle {
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends the blob url generated by js
|
||||||
|
pub fn set_image_blob_url(&self, path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
||||||
|
let dimensions = (width, height);
|
||||||
|
let message = Operation::SetImageBlobUrl { path, blob_url, dimensions };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pastes an image
|
||||||
|
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {
|
||||||
|
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
|
||||||
|
let message = DocumentMessage::PasteImage { mime, image_data, mouse };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle visibility of a layer from the layer list
|
/// Toggle visibility of a layer from the layer list
|
||||||
pub fn toggle_layer_visibility(&self, layer_path: Vec<LayerId>) {
|
pub fn toggle_layer_visibility(&self, layer_path: Vec<LayerId>) {
|
||||||
let message = DocumentMessage::ToggleLayerVisibility { layer_path };
|
let message = DocumentMessage::ToggleLayerVisibility { layer_path };
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::boolean_ops::boolean_operation;
|
||||||
use crate::intersection::Quad;
|
use crate::intersection::Quad;
|
||||||
use crate::layers;
|
use crate::layers;
|
||||||
use crate::layers::folder_layer::FolderLayer;
|
use crate::layers::folder_layer::FolderLayer;
|
||||||
|
use crate::layers::image_layer::ImageLayer;
|
||||||
use crate::layers::layer_info::{Layer, LayerData, LayerDataType};
|
use crate::layers::layer_info::{Layer, LayerData, LayerDataType};
|
||||||
use crate::layers::shape_layer::ShapeLayer;
|
use crate::layers::shape_layer::ShapeLayer;
|
||||||
use crate::layers::style::ViewMode;
|
use crate::layers::style::ViewMode;
|
||||||
|
|
@ -268,23 +269,17 @@ impl Document {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visit each layer recursively, applies modify_shape to each non-overlay Shape
|
/// Visit each layer recursively, marks all children as dirty
|
||||||
pub fn visit_all_shapes<F: FnMut(&mut ShapeLayer)>(layer: &mut Layer, modify_shape: &mut F) -> bool {
|
pub fn mark_children_as_dirty(layer: &mut Layer) -> bool {
|
||||||
match layer.data {
|
match layer.data {
|
||||||
LayerDataType::Shape(ref mut shape) => {
|
|
||||||
modify_shape(shape);
|
|
||||||
|
|
||||||
// This layer should be updated on next render pass
|
|
||||||
layer.cache_dirty = true;
|
|
||||||
}
|
|
||||||
LayerDataType::Folder(ref mut folder) => {
|
LayerDataType::Folder(ref mut folder) => {
|
||||||
for sub_layer in folder.layers_mut() {
|
for sub_layer in folder.layers_mut() {
|
||||||
if Document::visit_all_shapes(sub_layer, modify_shape) {
|
if Document::mark_children_as_dirty(sub_layer) {
|
||||||
layer.cache_dirty = true;
|
layer.cache_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LayerDataType::Text(_) => layer.cache_dirty = true,
|
_ => layer.cache_dirty = true,
|
||||||
}
|
}
|
||||||
layer.cache_dirty
|
layer.cache_dirty
|
||||||
}
|
}
|
||||||
|
|
@ -502,6 +497,19 @@ impl Document {
|
||||||
|
|
||||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||||
}
|
}
|
||||||
|
Operation::AddImage {
|
||||||
|
path,
|
||||||
|
transform,
|
||||||
|
insert_index,
|
||||||
|
image_data,
|
||||||
|
mime,
|
||||||
|
} => {
|
||||||
|
let layer = Layer::new(LayerDataType::Image(ImageLayer::new(mime, image_data)), transform);
|
||||||
|
|
||||||
|
self.set_layer(&path, layer, insert_index)?;
|
||||||
|
|
||||||
|
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||||
|
}
|
||||||
Operation::SetTextEditability { path, editable } => {
|
Operation::SetTextEditability { path, editable } => {
|
||||||
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
||||||
self.mark_as_dirty(&path)?;
|
self.mark_as_dirty(&path)?;
|
||||||
|
|
@ -690,6 +698,14 @@ impl Document {
|
||||||
self.mark_as_dirty(&path)?;
|
self.mark_as_dirty(&path)?;
|
||||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||||
}
|
}
|
||||||
|
Operation::SetImageBlobUrl { path, blob_url, dimensions } => {
|
||||||
|
let image = self.layer_mut(&path).expect("Blob url for invalid layer").as_image_mut().unwrap();
|
||||||
|
image.blob_url = Some(blob_url);
|
||||||
|
image.dimensions = dimensions.into();
|
||||||
|
self.mark_as_dirty(&path)?;
|
||||||
|
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||||
|
}
|
||||||
|
|
||||||
Operation::SetLayerTransformInViewport { path, transform } => {
|
Operation::SetLayerTransformInViewport { path, transform } => {
|
||||||
let transform = DAffine2::from_cols_array(&transform);
|
let transform = DAffine2::from_cols_array(&transform);
|
||||||
self.set_transform_relative_to_viewport(&path, transform)?;
|
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub enum DocumentError {
|
||||||
NonReorderableSelection,
|
NonReorderableSelection,
|
||||||
NotAShape,
|
NotAShape,
|
||||||
NotText,
|
NotText,
|
||||||
|
NotAnImage,
|
||||||
InvalidFile(String),
|
InvalidFile(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
use super::layer_info::LayerData;
|
||||||
|
use super::style::ViewMode;
|
||||||
|
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||||
|
use crate::LayerId;
|
||||||
|
|
||||||
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
|
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||||
|
Affine::new(transform.to_cols_array())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct ImageLayer {
|
||||||
|
pub mime: String,
|
||||||
|
pub image_data: Vec<u8>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub blob_url: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dimensions: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerData for ImageLayer {
|
||||||
|
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||||
|
let transform = self.transform(transforms, view_mode);
|
||||||
|
let inverse = transform.inverse();
|
||||||
|
|
||||||
|
if !inverse.is_finite() {
|
||||||
|
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||||
|
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||||
|
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||||
|
});
|
||||||
|
let _ = svg.write_str(r#")">"#);
|
||||||
|
|
||||||
|
let svg_transform = transform
|
||||||
|
.to_cols_array()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
|
||||||
|
.collect::<String>();
|
||||||
|
let _ = write!(
|
||||||
|
svg,
|
||||||
|
r#"<image width="{}" height="{}" transform="matrix({})" xlink:href="{}" />"#,
|
||||||
|
self.dimensions.x,
|
||||||
|
self.dimensions.y,
|
||||||
|
svg_transform,
|
||||||
|
self.blob_url.as_ref().unwrap_or(&String::new())
|
||||||
|
);
|
||||||
|
let _ = svg.write_str("</g>");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||||
|
let mut path = self.bounds();
|
||||||
|
|
||||||
|
if transform.matrix2 == DMat2::ZERO {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
path.apply_affine(glam_to_kurbo(transform));
|
||||||
|
|
||||||
|
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||||
|
Some([(x0, y0).into(), (x1, y1).into()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||||
|
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||||
|
intersections.push(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageLayer {
|
||||||
|
pub fn new(mime: String, image_data: Vec<u8>) -> Self {
|
||||||
|
let blob_url = None;
|
||||||
|
let dimensions = DVec2::ONE;
|
||||||
|
Self {
|
||||||
|
mime,
|
||||||
|
image_data,
|
||||||
|
blob_url,
|
||||||
|
dimensions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||||
|
let start = match mode {
|
||||||
|
ViewMode::Outline => 0,
|
||||||
|
_ => (transforms.len() as i32 - 1).max(0) as usize,
|
||||||
|
};
|
||||||
|
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds(&self) -> BezPath {
|
||||||
|
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(self.dimensions.x, self.dimensions.y)).to_path(0.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::blend_mode::BlendMode;
|
use super::blend_mode::BlendMode;
|
||||||
use super::folder_layer::FolderLayer;
|
use super::folder_layer::FolderLayer;
|
||||||
|
use super::image_layer::ImageLayer;
|
||||||
use super::shape_layer::ShapeLayer;
|
use super::shape_layer::ShapeLayer;
|
||||||
use super::style::{PathStyle, ViewMode};
|
use super::style::{PathStyle, ViewMode};
|
||||||
use super::text_layer::TextLayer;
|
use super::text_layer::TextLayer;
|
||||||
|
|
@ -16,6 +17,7 @@ pub enum LayerDataType {
|
||||||
Folder(FolderLayer),
|
Folder(FolderLayer),
|
||||||
Shape(ShapeLayer),
|
Shape(ShapeLayer),
|
||||||
Text(TextLayer),
|
Text(TextLayer),
|
||||||
|
Image(ImageLayer),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayerDataType {
|
impl LayerDataType {
|
||||||
|
|
@ -24,6 +26,7 @@ impl LayerDataType {
|
||||||
LayerDataType::Shape(s) => s,
|
LayerDataType::Shape(s) => s,
|
||||||
LayerDataType::Folder(f) => f,
|
LayerDataType::Folder(f) => f,
|
||||||
LayerDataType::Text(t) => t,
|
LayerDataType::Text(t) => t,
|
||||||
|
LayerDataType::Image(i) => i,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +35,7 @@ impl LayerDataType {
|
||||||
LayerDataType::Shape(s) => s,
|
LayerDataType::Shape(s) => s,
|
||||||
LayerDataType::Folder(f) => f,
|
LayerDataType::Folder(f) => f,
|
||||||
LayerDataType::Text(t) => t,
|
LayerDataType::Text(t) => t,
|
||||||
|
LayerDataType::Image(i) => i,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +197,13 @@ impl Layer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_image_mut(&mut self) -> Result<&mut ImageLayer, DocumentError> {
|
||||||
|
match &mut self.data {
|
||||||
|
LayerDataType::Image(img) => Ok(img),
|
||||||
|
_ => Err(DocumentError::NotAnImage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||||
match &self.data {
|
match &self.data {
|
||||||
LayerDataType::Shape(s) => Ok(&s.style),
|
LayerDataType::Shape(s) => Ok(&s.style),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod blend_mode;
|
pub mod blend_mode;
|
||||||
pub mod folder_layer;
|
pub mod folder_layer;
|
||||||
|
pub mod image_layer;
|
||||||
pub mod layer_info;
|
pub mod layer_info;
|
||||||
pub mod shape_layer;
|
pub mod shape_layer;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,18 @@ pub enum Operation {
|
||||||
style: style::PathStyle,
|
style: style::PathStyle,
|
||||||
size: f64,
|
size: f64,
|
||||||
},
|
},
|
||||||
|
AddImage {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
transform: [f64; 6],
|
||||||
|
insert_index: isize,
|
||||||
|
mime: String,
|
||||||
|
image_data: Vec<u8>,
|
||||||
|
},
|
||||||
|
SetImageBlobUrl {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
blob_url: String,
|
||||||
|
dimensions: (f64, f64),
|
||||||
|
},
|
||||||
SetTextEditability {
|
SetTextEditability {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
editable: bool,
|
editable: bool,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue