mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-09-11 23:46:18 +00:00
Hook up layer tree structure with frontend (#372)
* Hook up layer tree structure with frontend (decoding and Vue are WIP) * Fix off by one error * Avoid leaking memory * Parse layer structure into list of layers * Fix thumbnail updates * Correctly popagate deletions * Fix selection state in layer tree * Respect expansion during root serialization * Allow expanding of subfolders * Fix arrow direction Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
c5f44a8c1d
commit
225b46300d
19 changed files with 15777 additions and 336 deletions
|
@ -21,7 +21,7 @@ const GROUP_MESSAGES: &[MessageDiscriminant] = &[
|
||||||
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
||||||
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::ExpandFolder),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayFolderTreeStructure),
|
||||||
MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged),
|
MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -124,10 +124,10 @@ mod test {
|
||||||
init_logger();
|
init_logger();
|
||||||
let mut editor = create_editor_with_three_layers();
|
let mut editor = create_editor_with_three_layers();
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
editor.handle_message(DocumentsMessage::Copy);
|
editor.handle_message(DocumentsMessage::Copy);
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||||
|
@ -154,14 +154,14 @@ mod test {
|
||||||
init_logger();
|
init_logger();
|
||||||
let mut editor = create_editor_with_three_layers();
|
let mut editor = create_editor_with_three_layers();
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
||||||
|
|
||||||
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]]));
|
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]]));
|
||||||
editor.handle_message(DocumentsMessage::Copy);
|
editor.handle_message(DocumentsMessage::Copy);
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
|
|
||||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||||
|
@ -193,7 +193,7 @@ mod test {
|
||||||
|
|
||||||
editor.handle_message(DocumentMessage::CreateFolder(vec![]));
|
editor.handle_message(DocumentMessage::CreateFolder(vec![]));
|
||||||
|
|
||||||
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
|
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
|
||||||
|
|
||||||
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
|
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
|
||||||
|
@ -215,14 +215,14 @@ mod test {
|
||||||
|
|
||||||
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![folder_id]]));
|
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![folder_id]]));
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
editor.handle_message(DocumentsMessage::Copy);
|
editor.handle_message(DocumentsMessage::Copy);
|
||||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
|
|
||||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||||
|
@ -273,7 +273,7 @@ mod test {
|
||||||
const SHAPE_INDEX: usize = 1;
|
const SHAPE_INDEX: usize = 1;
|
||||||
const RECT_INDEX: usize = 0;
|
const RECT_INDEX: usize = 0;
|
||||||
|
|
||||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
|
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
|
||||||
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
||||||
|
|
||||||
|
@ -284,7 +284,7 @@ mod test {
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
|
||||||
|
|
||||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
|
||||||
|
|
||||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
pub use super::layer_panel::*;
|
|
||||||
use crate::{
|
|
||||||
consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING},
|
|
||||||
EditorError,
|
|
||||||
};
|
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use graphene::{document::Document as InternalDocument, layers::LayerDataType, DocumentError, LayerId};
|
|
||||||
use kurbo::PathSeg;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::input::InputPreprocessor;
|
|
||||||
use crate::message_prelude::*;
|
|
||||||
use graphene::layers::BlendMode;
|
|
||||||
use graphene::{DocumentResponse, Operation as DocumentOperation};
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
pub use super::layer_panel::*;
|
||||||
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
||||||
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
||||||
|
|
||||||
type DocumentSave = (InternalDocument, HashMap<Vec<LayerId>, LayerData>);
|
use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
|
||||||
|
use crate::input::InputPreprocessor;
|
||||||
|
use crate::message_prelude::*;
|
||||||
|
use crate::EditorError;
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
use graphene::layers::Folder;
|
||||||
|
use kurbo::PathSeg;
|
||||||
|
use log::warn;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use graphene::layers::BlendMode;
|
||||||
|
use graphene::{document::Document as GrapheneDocument, layers::LayerDataType, DocumentError, LayerId};
|
||||||
|
use graphene::{DocumentResponse, Operation as DocumentOperation};
|
||||||
|
|
||||||
|
type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerData>);
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||||
pub enum FlipAxis {
|
pub enum FlipAxis {
|
||||||
|
@ -58,7 +58,7 @@ pub struct VectorManipulatorShape {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DocumentMessageHandler {
|
pub struct DocumentMessageHandler {
|
||||||
pub document: InternalDocument,
|
pub graphene_document: GrapheneDocument,
|
||||||
pub document_history: Vec<DocumentSave>,
|
pub document_history: Vec<DocumentSave>,
|
||||||
pub document_redo_history: Vec<DocumentSave>,
|
pub document_redo_history: Vec<DocumentSave>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -70,7 +70,7 @@ pub struct DocumentMessageHandler {
|
||||||
impl Default for DocumentMessageHandler {
|
impl Default for DocumentMessageHandler {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
document: InternalDocument::default(),
|
graphene_document: GrapheneDocument::default(),
|
||||||
document_history: Vec::new(),
|
document_history: Vec::new(),
|
||||||
document_redo_history: Vec::new(),
|
document_redo_history: Vec::new(),
|
||||||
name: String::from("Untitled Document"),
|
name: String::from("Untitled Document"),
|
||||||
|
@ -105,6 +105,8 @@ pub enum DocumentMessage {
|
||||||
FlipSelectedLayers(FlipAxis),
|
FlipSelectedLayers(FlipAxis),
|
||||||
ToggleLayerExpansion(Vec<LayerId>),
|
ToggleLayerExpansion(Vec<LayerId>),
|
||||||
FolderChanged(Vec<LayerId>),
|
FolderChanged(Vec<LayerId>),
|
||||||
|
LayerChanged(Vec<LayerId>),
|
||||||
|
DocumentStructureChanged,
|
||||||
StartTransaction,
|
StartTransaction,
|
||||||
RollbackTransaction,
|
RollbackTransaction,
|
||||||
GroupSelectedLayers,
|
GroupSelectedLayers,
|
||||||
|
@ -140,39 +142,26 @@ impl From<DocumentOperation> for Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentMessageHandler {
|
impl DocumentMessageHandler {
|
||||||
pub fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
|
|
||||||
let _ = self.document.render_root();
|
|
||||||
self.layer_data(&path).expanded.then(|| {
|
|
||||||
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
|
|
||||||
FrontendMessage::ExpandFolder { path: path.into(), children }.into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_selection(&mut self) {
|
|
||||||
self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||||
if self.document.layer(path).ok()?.overlay {
|
if self.graphene_document.layer(path).ok()?.overlay {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.layer_data(path).selected = true;
|
self.layer_data(path).selected = true;
|
||||||
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
||||||
// TODO: Add deduplication
|
|
||||||
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into())
|
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||||
let paths = self.selected_layers().map(|vec| &vec[..]);
|
let paths = self.selected_layers();
|
||||||
self.document.combined_viewport_bounding_box(paths)
|
self.graphene_document.combined_viewport_bounding_box(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Consider moving this to some kind of overlay manager in the future
|
// TODO: Consider moving this to some kind of overlay manager in the future
|
||||||
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
|
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
|
||||||
let shapes = self.selected_layers().filter_map(|path_to_shape| {
|
let shapes = self.selected_layers().filter_map(|path_to_shape| {
|
||||||
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
|
let viewport_transform = self.graphene_document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
|
||||||
|
|
||||||
let shape = match &self.document.layer(path_to_shape).ok()?.data {
|
let shape = match &self.graphene_document.layer(path_to_shape).ok()?.data {
|
||||||
LayerDataType::Shape(shape) => Some(shape),
|
LayerDataType::Shape(shape) => Some(shape),
|
||||||
LayerDataType::Folder(_) => None,
|
LayerDataType::Folder(_) => None,
|
||||||
}?;
|
}?;
|
||||||
|
@ -214,6 +203,59 @@ impl DocumentMessageHandler {
|
||||||
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
|
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_structure(&self, folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
|
||||||
|
let mut space = 0;
|
||||||
|
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||||
|
data.push(*id);
|
||||||
|
space += 1;
|
||||||
|
match layer.data {
|
||||||
|
LayerDataType::Shape(_) => (),
|
||||||
|
LayerDataType::Folder(ref folder) => {
|
||||||
|
path.push(*id);
|
||||||
|
if self.layerdata(path).expanded {
|
||||||
|
structure.push(space);
|
||||||
|
self.serialize_structure(folder, structure, data, path);
|
||||||
|
space = 0;
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
structure.push(space | 1 << 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes the layer structure into a compressed 1d structure
|
||||||
|
///
|
||||||
|
/// It is a string of numbers broken into three sections:
|
||||||
|
/// (4),(2,1,-2,-0),(16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186) <- Example encoded data
|
||||||
|
/// L = 4 = structure.len() <- First value in the encoding: L, the length of the structure section
|
||||||
|
/// structure = 2,1,-2,-0 <- Subsequent L values: structure section
|
||||||
|
/// data = 16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186 <- Remaining values: data section (layer IDs)
|
||||||
|
///
|
||||||
|
/// The data section lists the layer IDs for all folders/layers in the tree as read from top to bottom.
|
||||||
|
/// The structure section lists signed numbers. The sign indicates a folder indentation change (+ is down a level, - is up a level).
|
||||||
|
/// the numbers in the structure block encode the indentation,
|
||||||
|
/// 2 mean read two element from the data section, then place a [
|
||||||
|
/// -x means read x elements from the data section and then insert a ]
|
||||||
|
///
|
||||||
|
/// 2 V 1 V -2 A -0 A
|
||||||
|
/// 16533113728871998040,3427872634365736244, 18115028555707261608, 15878401910454357952,449479075714955186
|
||||||
|
/// 16533113728871998040,3427872634365736244,[ 18115028555707261608,[15878401910454357952,449479075714955186] ]
|
||||||
|
///
|
||||||
|
/// resulting layer panel:
|
||||||
|
/// 16533113728871998040
|
||||||
|
/// 3427872634365736244
|
||||||
|
/// [3427872634365736244,18115028555707261608]
|
||||||
|
/// [3427872634365736244,18115028555707261608,15878401910454357952]
|
||||||
|
/// [3427872634365736244,18115028555707261608,449479075714955186]
|
||||||
|
pub fn serialize_root(&self) -> Vec<u64> {
|
||||||
|
let (mut structure, mut data) = (vec![0], Vec::new());
|
||||||
|
self.serialize_structure(self.graphene_document.root.as_folder().unwrap(), &mut structure, &mut data, &mut vec![]);
|
||||||
|
structure[0] = structure.len() as u64 - 1;
|
||||||
|
structure.extend(data);
|
||||||
|
structure
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
|
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
|
||||||
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
|
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
|
||||||
// Compute the indices for each layer to be able to sort them
|
// Compute the indices for each layer to be able to sort them
|
||||||
|
@ -227,7 +269,7 @@ impl DocumentMessageHandler {
|
||||||
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
|
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
|
||||||
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
||||||
// Once this problem is solved this code can be simplified
|
// Once this problem is solved this code can be simplified
|
||||||
match self.document.indices_for_path(&path) {
|
match self.graphene_document.indices_for_path(&path) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
|
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
|
||||||
None
|
None
|
||||||
|
@ -259,7 +301,7 @@ impl DocumentMessageHandler {
|
||||||
|
|
||||||
pub fn with_name(name: String) -> Self {
|
pub fn with_name(name: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
document: InternalDocument::default(),
|
graphene_document: GrapheneDocument::default(),
|
||||||
document_history: Vec::new(),
|
document_history: Vec::new(),
|
||||||
document_redo_history: Vec::new(),
|
document_redo_history: Vec::new(),
|
||||||
name,
|
name,
|
||||||
|
@ -271,10 +313,10 @@ impl DocumentMessageHandler {
|
||||||
|
|
||||||
pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> {
|
pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> {
|
||||||
let mut document = Self::with_name(name);
|
let mut document = Self::with_name(name);
|
||||||
let internal_document = InternalDocument::with_content(&serialized_content);
|
let internal_document = GrapheneDocument::with_content(&serialized_content);
|
||||||
match internal_document {
|
match internal_document {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
document.document = handle;
|
document.graphene_document = handle;
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)),
|
Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)),
|
||||||
|
@ -291,9 +333,9 @@ impl DocumentMessageHandler {
|
||||||
let new_layer_data = self
|
let new_layer_data = self
|
||||||
.layer_data
|
.layer_data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
.filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||||
.collect();
|
.collect();
|
||||||
self.document_history.push((self.document.clone_without_overlays(), new_layer_data))
|
self.document_history.push((self.graphene_document.clone_without_overlays(), new_layer_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rollback(&mut self) -> Result<(), EditorError> {
|
pub fn rollback(&mut self) -> Result<(), EditorError> {
|
||||||
|
@ -304,7 +346,7 @@ impl DocumentMessageHandler {
|
||||||
pub fn undo(&mut self) -> Result<(), EditorError> {
|
pub fn undo(&mut self) -> Result<(), EditorError> {
|
||||||
match self.document_history.pop() {
|
match self.document_history.pop() {
|
||||||
Some((document, layer_data)) => {
|
Some((document, layer_data)) => {
|
||||||
let document = std::mem::replace(&mut self.document, document);
|
let document = std::mem::replace(&mut self.graphene_document, document);
|
||||||
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
||||||
self.document_redo_history.push((document, layer_data));
|
self.document_redo_history.push((document, layer_data));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -316,11 +358,11 @@ impl DocumentMessageHandler {
|
||||||
pub fn redo(&mut self) -> Result<(), EditorError> {
|
pub fn redo(&mut self) -> Result<(), EditorError> {
|
||||||
match self.document_redo_history.pop() {
|
match self.document_redo_history.pop() {
|
||||||
Some((document, layer_data)) => {
|
Some((document, layer_data)) => {
|
||||||
let document = std::mem::replace(&mut self.document, document);
|
let document = std::mem::replace(&mut self.graphene_document, document);
|
||||||
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
||||||
let new_layer_data = layer_data
|
let new_layer_data = layer_data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
.filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||||
.collect();
|
.collect();
|
||||||
self.document_history.push((document.clone_without_overlays(), new_layer_data));
|
self.document_history.push((document.clone_without_overlays(), new_layer_data));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -331,18 +373,18 @@ impl DocumentMessageHandler {
|
||||||
|
|
||||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||||
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
||||||
let layer = self.document.layer(&path)?;
|
let layer = self.graphene_document.layer(&path)?;
|
||||||
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path)?, layer, path);
|
let entry = layer_panel_entry(&data, self.graphene_document.multiply_transforms(&path)?, layer, path);
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
|
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
|
||||||
/// any actual data, but rather attributes such as visibility and names of the layers.
|
/// any actual data, but rather attributes such as visibility and names of the layers.
|
||||||
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
|
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
|
||||||
let folder = self.document.folder(path)?;
|
let folder = self.graphene_document.folder(path)?;
|
||||||
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
|
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
|
||||||
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
|
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
|
||||||
let folder = self.document.folder(path)?;
|
let folder = self.graphene_document.folder(path)?;
|
||||||
let entries = folder
|
let entries = folder
|
||||||
.layers()
|
.layers()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -352,7 +394,9 @@ impl DocumentMessageHandler {
|
||||||
.map(|(layer, (path, data))| {
|
.map(|(layer, (path, data))| {
|
||||||
layer_panel_entry(
|
layer_panel_entry(
|
||||||
&data,
|
&data,
|
||||||
self.document.generate_transform_across_scope(path, Some(self.document.root.transform.inverse())).unwrap(),
|
self.graphene_document
|
||||||
|
.generate_transform_across_scope(path, Some(self.graphene_document.root.transform.inverse()))
|
||||||
|
.unwrap(),
|
||||||
layer,
|
layer,
|
||||||
path.to_vec(),
|
path.to_vec(),
|
||||||
)
|
)
|
||||||
|
@ -366,21 +410,25 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
|
||||||
use DocumentMessage::*;
|
use DocumentMessage::*;
|
||||||
match message {
|
match message {
|
||||||
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
|
Movement(message) => self
|
||||||
TransformLayers(message) => self.transform_layer_handler.process_action(message, (&mut self.layer_data, &mut self.document, ipp), responses),
|
.movement_handler
|
||||||
|
.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.graphene_document, ipp), responses),
|
||||||
|
TransformLayers(message) => self
|
||||||
|
.transform_layer_handler
|
||||||
|
.process_action(message, (&mut self.layer_data, &mut self.graphene_document, ipp), responses),
|
||||||
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
|
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
|
||||||
StartTransaction => self.backup(),
|
StartTransaction => self.backup(),
|
||||||
RollbackTransaction => {
|
RollbackTransaction => {
|
||||||
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
|
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
|
||||||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||||
}
|
}
|
||||||
AbortTransaction => {
|
AbortTransaction => {
|
||||||
self.undo().unwrap_or_else(|e| log::warn!("{}", e));
|
self.undo().unwrap_or_else(|e| log::warn!("{}", e));
|
||||||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||||
}
|
}
|
||||||
CommitTransaction => (),
|
CommitTransaction => (),
|
||||||
ExportDocument => {
|
ExportDocument => {
|
||||||
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
|
let bbox = self.graphene_document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
|
||||||
let size = bbox[1] - bbox[0];
|
let size = bbox[1] - bbox[0];
|
||||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||||
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
|
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
|
||||||
|
@ -395,7 +443,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
size.x,
|
size.x,
|
||||||
size.y,
|
size.y,
|
||||||
"\n",
|
"\n",
|
||||||
self.document.render_root()
|
self.graphene_document.render_root()
|
||||||
),
|
),
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
|
@ -409,7 +457,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
};
|
};
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::SaveDocument {
|
FrontendMessage::SaveDocument {
|
||||||
document: self.document.serialize_document(),
|
document: self.graphene_document.serialize_document(),
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
@ -422,7 +470,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
responses.push_back(DocumentOperation::CreateFolder { path }.into())
|
responses.push_back(DocumentOperation::CreateFolder { path }.into())
|
||||||
}
|
}
|
||||||
GroupSelectedLayers => {
|
GroupSelectedLayers => {
|
||||||
let common_prefix = self.document.common_prefix(self.selected_layers());
|
let common_prefix = self.graphene_document.common_prefix(self.selected_layers());
|
||||||
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
|
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
|
||||||
|
|
||||||
let mut new_folder_path = common_prefix.to_vec();
|
let mut new_folder_path = common_prefix.to_vec();
|
||||||
|
@ -460,11 +508,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
ToggleLayerExpansion(path) => {
|
ToggleLayerExpansion(path) => {
|
||||||
self.layer_data(&path).expanded ^= true;
|
self.layer_data(&path).expanded ^= true;
|
||||||
match self.layer_data(&path).expanded {
|
responses.push_back(DocumentStructureChanged.into());
|
||||||
true => responses.push_back(FolderChanged(path.clone()).into()),
|
responses.push_back(LayerChanged(path).into())
|
||||||
false => responses.push_back(FrontendMessage::CollapseFolder { path: path.clone().into() }.into()),
|
|
||||||
}
|
|
||||||
responses.extend(self.layer_panel_entry(path.clone()).ok().map(|data| FrontendMessage::UpdateLayer { path: path.into(), data }.into()));
|
|
||||||
}
|
}
|
||||||
SelectionChanged => {
|
SelectionChanged => {
|
||||||
// TODO: Hoist this duplicated code into wider system
|
// TODO: Hoist this duplicated code into wider system
|
||||||
|
@ -479,7 +524,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
ClearOverlays => {
|
ClearOverlays => {
|
||||||
responses.push_back(ToolMessage::SelectedLayersChanged.into());
|
responses.push_back(ToolMessage::SelectedLayersChanged.into());
|
||||||
for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() {
|
for path in self.layer_data.keys().filter(|path| self.graphene_document.layer(path).unwrap().overlay).cloned() {
|
||||||
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
|
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -490,7 +535,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetSelectedLayers(paths) => {
|
SetSelectedLayers(paths) => {
|
||||||
self.clear_selection();
|
self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
|
||||||
|
layer_data.selected = false;
|
||||||
|
responses.push_back(LayerChanged(path.clone()).into())
|
||||||
|
});
|
||||||
|
|
||||||
responses.push_front(AddSelectedLayers(paths).into());
|
responses.push_front(AddSelectedLayers(paths).into());
|
||||||
}
|
}
|
||||||
AddSelectedLayers(paths) => {
|
AddSelectedLayers(paths) => {
|
||||||
|
@ -505,7 +554,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
let all_layer_paths = self
|
let all_layer_paths = self
|
||||||
.layer_data
|
.layer_data
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
|
.filter(|path| !path.is_empty() && !self.graphene_document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
responses.push_front(SetSelectedLayers(all_layer_paths).into());
|
responses.push_front(SetSelectedLayers(all_layer_paths).into());
|
||||||
|
@ -527,30 +576,41 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
responses.push_back(RenderDocument.into());
|
responses.push_back(RenderDocument.into());
|
||||||
responses.push_back(FolderChanged(vec![]).into());
|
responses.push_back(FolderChanged(vec![]).into());
|
||||||
}
|
}
|
||||||
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
|
FolderChanged(path) => {
|
||||||
DispatchOperation(op) => match self.document.handle_operation(&op) {
|
let _ = self.graphene_document.render_root();
|
||||||
|
responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]);
|
||||||
|
}
|
||||||
|
DocumentStructureChanged => {
|
||||||
|
let data_buffer: RawBuffer = self.serialize_root().into();
|
||||||
|
responses.push_back(FrontendMessage::DisplayFolderTreeStructure { data_buffer }.into())
|
||||||
|
}
|
||||||
|
LayerChanged(path) => {
|
||||||
|
responses.extend(self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
|
||||||
|
let overlay = self.graphene_document.layer(&path).unwrap().overlay;
|
||||||
|
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchOperation(op) => match self.graphene_document.handle_operation(&op) {
|
||||||
Ok(Some(document_responses)) => {
|
Ok(Some(document_responses)) => {
|
||||||
responses.extend(
|
for response in document_responses {
|
||||||
document_responses
|
match response {
|
||||||
.into_iter()
|
DocumentResponse::FolderChanged { path } => responses.push_back(FolderChanged(path).into()),
|
||||||
.map(|response| match response {
|
|
||||||
DocumentResponse::FolderChanged { path } => Some(FolderChanged(path).into()),
|
|
||||||
DocumentResponse::DeletedLayer { path } => {
|
DocumentResponse::DeletedLayer { path } => {
|
||||||
self.layer_data.remove(&path);
|
self.layer_data.remove(&path);
|
||||||
Some(ToolMessage::SelectedLayersChanged.into())
|
responses.push_back(ToolMessage::SelectedLayersChanged.into())
|
||||||
}
|
}
|
||||||
DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
|
DocumentResponse::LayerChanged { path } => responses.push_back(LayerChanged(path).into()),
|
||||||
let overlay = self.document.layer(&path).unwrap().overlay;
|
|
||||||
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
|
|
||||||
}),
|
|
||||||
DocumentResponse::CreatedLayer { path } => {
|
DocumentResponse::CreatedLayer { path } => {
|
||||||
self.layer_data.insert(path.clone(), LayerData::new(false));
|
self.layer_data.insert(path.clone(), LayerData::new(false));
|
||||||
(!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into())
|
responses.push_back(LayerChanged(path.clone()).into());
|
||||||
|
if !self.graphene_document.layer(&path).unwrap().overlay {
|
||||||
|
responses.push_back(SetSelectedLayers(vec![path]).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
DocumentResponse::DocumentChanged => Some(RenderDocument.into()),
|
|
||||||
})
|
|
||||||
.flatten(),
|
|
||||||
);
|
|
||||||
// log::debug!("LayerPanel: {:?}", self.layer_data.keys());
|
// log::debug!("LayerPanel: {:?}", self.layer_data.keys());
|
||||||
}
|
}
|
||||||
Err(e) => log::error!("DocumentError: {:?}", e),
|
Err(e) => log::error!("DocumentError: {:?}", e),
|
||||||
|
@ -559,7 +619,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
RenderDocument => {
|
RenderDocument => {
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::UpdateCanvas {
|
FrontendMessage::UpdateCanvas {
|
||||||
document: self.document.render_root(),
|
document: self.graphene_document.render_root(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
@ -567,7 +627,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
let scale = 0.5 + ASYMPTOTIC_EFFECT + self.layerdata(&[]).scale * SCALE_EFFECT;
|
let scale = 0.5 + ASYMPTOTIC_EFFECT + self.layerdata(&[]).scale * SCALE_EFFECT;
|
||||||
let viewport_size = ipp.viewport_bounds.size();
|
let viewport_size = ipp.viewport_bounds.size();
|
||||||
let viewport_mid = ipp.viewport_bounds.center();
|
let viewport_mid = ipp.viewport_bounds.center();
|
||||||
let [bounds1, bounds2] = self.document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]);
|
let [bounds1, bounds2] = self.graphene_document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]);
|
||||||
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
|
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
|
||||||
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
|
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
|
||||||
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
|
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
|
||||||
|
@ -619,7 +679,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
let insert = all_layer_paths.get(insert_pos);
|
let insert = all_layer_paths.get(insert_pos);
|
||||||
if let Some(insert_path) = insert {
|
if let Some(insert_path) = insert {
|
||||||
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
||||||
if let Some(folder) = self.document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
|
if let Some(folder) = self.graphene_document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
|
||||||
let selected: Vec<_> = selected_layers
|
let selected: Vec<_> = selected_layers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
||||||
|
@ -641,7 +701,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
FlipAxis::X => DVec2::new(-1., 1.),
|
FlipAxis::X => DVec2::new(-1., 1.),
|
||||||
FlipAxis::Y => DVec2::new(1., -1.),
|
FlipAxis::Y => DVec2::new(1., -1.),
|
||||||
};
|
};
|
||||||
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
||||||
let center = (max + min) / 2.;
|
let center = (max + min) / 2.;
|
||||||
let bbox_trans = DAffine2::from_translation(-center);
|
let bbox_trans = DAffine2::from_translation(-center);
|
||||||
for path in self.selected_layers() {
|
for path in self.selected_layers() {
|
||||||
|
@ -659,14 +719,17 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
}
|
}
|
||||||
AlignSelectedLayers(axis, aggregate) => {
|
AlignSelectedLayers(axis, aggregate) => {
|
||||||
self.backup();
|
self.backup();
|
||||||
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
|
let (paths, boxes): (Vec<_>, Vec<_>) = self
|
||||||
|
.selected_layers()
|
||||||
|
.filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b)))
|
||||||
|
.unzip();
|
||||||
|
|
||||||
let axis = match axis {
|
let axis = match axis {
|
||||||
AlignAxis::X => DVec2::X,
|
AlignAxis::X => DVec2::X,
|
||||||
AlignAxis::Y => DVec2::Y,
|
AlignAxis::Y => DVec2::Y,
|
||||||
};
|
};
|
||||||
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
|
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
|
||||||
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
||||||
let aggregated = match aggregate {
|
let aggregated = match aggregate {
|
||||||
AlignAggregate::Min => combined_box[0],
|
AlignAggregate::Min => combined_box[0],
|
||||||
AlignAggregate::Max => combined_box[1],
|
AlignAggregate::Max => combined_box[1],
|
||||||
|
|
|
@ -78,14 +78,12 @@ impl DocumentsMessageHandler {
|
||||||
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
|
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
|
||||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||||
|
|
||||||
responses.push_back(
|
|
||||||
FrontendMessage::ExpandFolder {
|
|
||||||
path: Vec::new().into(),
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into());
|
responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into());
|
||||||
|
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||||
|
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||||
|
for layer in self.active_document().layer_data.keys() {
|
||||||
|
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +113,10 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
responses.push_back(RenderDocument.into());
|
responses.push_back(RenderDocument.into());
|
||||||
responses.extend(self.active_document_mut().handle_folder_changed(vec![]));
|
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||||
|
for layer in self.active_document().layer_data.keys() {
|
||||||
|
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CloseActiveDocumentWithConfirmation => {
|
CloseActiveDocumentWithConfirmation => {
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
|
@ -156,14 +157,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
self.active_document_index -= 1;
|
self.active_document_index -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lp = self.active_document_mut().layer_panel(&[]).expect("Could not get panel for active doc");
|
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||||
responses.push_back(
|
|
||||||
FrontendMessage::ExpandFolder {
|
|
||||||
path: Vec::new().into(),
|
|
||||||
children: lp,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::SetActiveDocument {
|
FrontendMessage::SetActiveDocument {
|
||||||
document_index: self.active_document_index,
|
document_index: self.active_document_index,
|
||||||
|
@ -172,7 +166,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
);
|
);
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::UpdateCanvas {
|
FrontendMessage::UpdateCanvas {
|
||||||
document: self.active_document_mut().document.render_root(),
|
document: self.active_document_mut().graphene_document.render_root(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
@ -228,7 +222,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
let paths = self.active_document().selected_layers_sorted();
|
let paths = self.active_document().selected_layers_sorted();
|
||||||
self.copy_buffer.clear();
|
self.copy_buffer.clear();
|
||||||
for path in paths {
|
for path in paths {
|
||||||
match self.active_document().document.layer(&path).map(|t| t.clone()) {
|
match self.active_document().graphene_document.layer(&path).map(|t| t.clone()) {
|
||||||
Ok(layer) => {
|
Ok(layer) => {
|
||||||
self.copy_buffer.push(layer);
|
self.copy_buffer.push(layer);
|
||||||
}
|
}
|
||||||
|
@ -239,7 +233,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
||||||
Paste => {
|
Paste => {
|
||||||
let document = self.active_document();
|
let document = self.active_document();
|
||||||
let shallowest_common_folder = document
|
let shallowest_common_folder = document
|
||||||
.document
|
.graphene_document
|
||||||
.deepest_common_folder(document.selected_layers())
|
.deepest_common_folder(document.selected_layers())
|
||||||
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@ use graphene::{
|
||||||
layers::{Layer, LayerData as DocumentLayerData},
|
layers::{Layer, LayerData as DocumentLayerData},
|
||||||
LayerId,
|
LayerId,
|
||||||
};
|
};
|
||||||
use serde::{ser::SerializeSeq, Deserialize, Serialize};
|
use serde::{
|
||||||
|
ser::{SerializeSeq, SerializeStruct},
|
||||||
|
Deserialize, Serialize,
|
||||||
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
@ -117,6 +120,33 @@ impl Serialize for Path {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct RawBuffer(Vec<u8>);
|
||||||
|
|
||||||
|
impl From<Vec<u64>> for RawBuffer {
|
||||||
|
fn from(iter: Vec<u64>) -> Self {
|
||||||
|
// https://github.com/rust-lang/rust-clippy/issues/4484
|
||||||
|
let v_from_raw: Vec<u8> = unsafe {
|
||||||
|
// prepare for an auto-forget of the initial vec:
|
||||||
|
let v_orig: &mut Vec<_> = &mut *std::mem::ManuallyDrop::new(iter);
|
||||||
|
Vec::from_raw_parts(v_orig.as_mut_ptr() as *mut u8, v_orig.len() * 8, v_orig.capacity() * 8)
|
||||||
|
// v_orig is never used again, so no aliasing issue
|
||||||
|
};
|
||||||
|
Self(v_from_raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Serialize for RawBuffer {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut buffer = serializer.serialize_struct("Buffer", 2)?;
|
||||||
|
buffer.serialize_field("ptr", &(self.0.as_ptr() as usize))?;
|
||||||
|
buffer.serialize_field("len", &(self.0.len()))?;
|
||||||
|
buffer.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct LayerPanelEntry {
|
pub struct LayerPanelEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::document::layer_panel::{LayerPanelEntry, Path};
|
use crate::document::layer_panel::{LayerPanelEntry, Path, RawBuffer};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
use crate::tool::tool_options::ToolOptions;
|
use crate::tool::tool_options::ToolOptions;
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
|
@ -7,8 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[impl_message(Message, Frontend)]
|
#[impl_message(Message, Frontend)]
|
||||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||||
pub enum FrontendMessage {
|
pub enum FrontendMessage {
|
||||||
CollapseFolder { path: Path },
|
DisplayFolderTreeStructure { data_buffer: RawBuffer },
|
||||||
ExpandFolder { path: Path, children: Vec<LayerPanelEntry> },
|
|
||||||
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
||||||
SetActiveDocument { document_index: usize },
|
SetActiveDocument { document_index: usize },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
||||||
|
@ -16,9 +15,9 @@ pub enum FrontendMessage {
|
||||||
DisplayPanic { panic_info: String, title: String, description: String },
|
DisplayPanic { panic_info: String, title: String, description: String },
|
||||||
DisplayConfirmationToCloseDocument { document_index: usize },
|
DisplayConfirmationToCloseDocument { document_index: usize },
|
||||||
DisplayConfirmationToCloseAllDocuments,
|
DisplayConfirmationToCloseAllDocuments,
|
||||||
|
UpdateLayer { path: Path, data: LayerPanelEntry },
|
||||||
UpdateCanvas { document: String },
|
UpdateCanvas { document: String },
|
||||||
UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
||||||
UpdateLayer { path: Path, data: LayerPanelEntry },
|
|
||||||
ExportDocument { document: String, name: String },
|
ExportDocument { document: String, name: String },
|
||||||
SaveDocument { document: String, name: String },
|
SaveDocument { document: String, name: String },
|
||||||
OpenDocumentBrowse,
|
OpenDocumentBrowse,
|
||||||
|
|
|
@ -22,8 +22,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
|
||||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||||
|
|
||||||
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
|
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
|
||||||
if let Ok(layer) = data.0.document.layer(path) {
|
if let Ok(layer) = data.0.graphene_document.layer(path) {
|
||||||
if let LayerDataType::Shape(s) = &layer.data {
|
if let LayerDataType::Shape(s) = &layer.data {
|
||||||
s.style.fill().and_then(|fill| {
|
s.style.fill().and_then(|fill| {
|
||||||
fill.color().map(|color| match action {
|
fill.color().map(|color| match action {
|
||||||
|
|
|
@ -20,7 +20,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
|
||||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
||||||
|
|
||||||
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
|
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
Operation::SetLayerFill {
|
Operation::SetLayerFill {
|
||||||
path: path.to_vec(),
|
path: path.to_vec(),
|
||||||
|
|
|
@ -66,7 +66,7 @@ impl Fsm for PenToolFsmState {
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let transform = document.document.root.transform;
|
let transform = document.graphene_document.root.transform;
|
||||||
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position);
|
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position);
|
||||||
|
|
||||||
use PenMessage::*;
|
use PenMessage::*;
|
||||||
|
|
|
@ -151,7 +151,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect();
|
let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect();
|
||||||
let quad = data.selection_quad();
|
let quad = data.selection_quad();
|
||||||
let intersection = document.document.intersects_quad_root(quad);
|
let intersection = document.graphene_document.intersects_quad_root(quad);
|
||||||
// If no layer is currently selected and the user clicks on a shape, select that.
|
// If no layer is currently selected and the user clicks on a shape, select that.
|
||||||
if selected.is_empty() {
|
if selected.is_empty() {
|
||||||
if let Some(layer) = intersection.last() {
|
if let Some(layer) = intersection.last() {
|
||||||
|
@ -214,7 +214,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
}
|
}
|
||||||
(DrawingBox, DragStop) => {
|
(DrawingBox, DragStop) => {
|
||||||
let quad = data.selection_quad();
|
let quad = data.selection_quad();
|
||||||
responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
|
responses.push_front(DocumentMessage::AddSelectedLayers(document.graphene_document.intersects_quad_root(quad)).into());
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
Operation::DeleteLayer {
|
Operation::DeleteLayer {
|
||||||
path: data.drag_box_id.take().unwrap(),
|
path: data.drag_box_id.take().unwrap(),
|
||||||
|
|
|
@ -62,6 +62,7 @@ module.exports = {
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
"no-param-reassign": ["error", { props: false }],
|
"no-param-reassign": ["error", { props: false }],
|
||||||
|
"no-bitwise": "off",
|
||||||
|
|
||||||
// TypeScript plugin config
|
// TypeScript plugin config
|
||||||
"@typescript-eslint/camelcase": "off",
|
"@typescript-eslint/camelcase": "off",
|
||||||
|
|
15422
frontend/package-lock.json
generated
15422
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -110,18 +110,18 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
top: 2px;
|
top: 3px;
|
||||||
left: 3px;
|
left: 4px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 0 3px 6px 3px;
|
border-width: 3px 0 3px 6px;
|
||||||
border-color: transparent transparent var(--color-2-mildblack) transparent;
|
border-color: transparent transparent transparent var(--color-2-mildblack);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded::after {
|
&.expanded::after {
|
||||||
top: 3px;
|
top: 4px;
|
||||||
left: 4px;
|
left: 3px;
|
||||||
border-width: 3px 0 3px 6px;
|
border-width: 6px 3px 0 3px;
|
||||||
border-color: transparent transparent transparent var(--color-2-mildblack);
|
border-color: var(--color-2-mildblack) transparent transparent transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, CollapseFolder, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
|
import { ResponseType, registerResponseHandler, Response, BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
|
||||||
import { panicProxy } from "@/utilities/panic-proxy";
|
import { panicProxy } from "@/utilities/panic-proxy";
|
||||||
import { SeparatorType } from "@/components/widgets/widgets";
|
import { SeparatorType } from "@/components/widgets/widgets";
|
||||||
|
|
||||||
|
@ -238,7 +238,24 @@ const blendModeEntries: SectionsOfMenuListEntries = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {},
|
data() {
|
||||||
|
return {
|
||||||
|
blendModeEntries,
|
||||||
|
blendModeSelectedIndex: 0,
|
||||||
|
blendModeDropdownDisabled: true,
|
||||||
|
opacityNumberInputDisabled: true,
|
||||||
|
// TODO: replace with BigUint64Array as index
|
||||||
|
layerCache: new Map() as Map<string, LayerPanelEntry>,
|
||||||
|
layers: [] as Array<LayerPanelEntry>,
|
||||||
|
layerDepths: [] as Array<number>,
|
||||||
|
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||||
|
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||||
|
opacity: 100,
|
||||||
|
MenuDirection,
|
||||||
|
SeparatorType,
|
||||||
|
LayerType,
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
layerIndent(layer: LayerPanelEntry): string {
|
layerIndent(layer: LayerPanelEntry): string {
|
||||||
return `${(layer.path.length - 1) * 16}px`;
|
return `${(layer.path.length - 1) * 16}px`;
|
||||||
|
@ -325,7 +342,6 @@ export default defineComponent({
|
||||||
output.set(path, i);
|
output.set(path, i);
|
||||||
i += path.length;
|
i += path.length;
|
||||||
if (index < paths.length) {
|
if (index < paths.length) {
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
output[i] = (1n << 64n) - 1n;
|
output[i] = (1n << 64n) - 1n;
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
|
@ -374,99 +390,40 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
registerResponseHandler(ResponseType.ExpandFolder, (responseData: Response) => {
|
registerResponseHandler(ResponseType.DisplayFolderTreeStructure, (responseData: Response) => {
|
||||||
const expandData = responseData as ExpandFolder;
|
const expandData = responseData as DisplayFolderTreeStructure;
|
||||||
if (expandData) {
|
if (!expandData) return;
|
||||||
const responsePath = expandData.path;
|
console.log(expandData);
|
||||||
const responseLayers = expandData.children as Array<LayerPanelEntry>;
|
|
||||||
// TODO: @Keavon Refactor this function
|
|
||||||
if (responseLayers.length === 0) return;
|
|
||||||
|
|
||||||
const mergeIntoExisting = (elements: Array<LayerPanelEntry>, layers: Array<LayerPanelEntry>) => {
|
const path = [] as Array<bigint>;
|
||||||
let lastInsertion = layers.findIndex((layer: LayerPanelEntry) => {
|
this.layers = [] as Array<LayerPanelEntry>;
|
||||||
const pathLengthsEqual = elements[0].path.length - 1 === layer.path.length;
|
function recurse(folder: DisplayFolderTreeStructure, layers: Array<LayerPanelEntry>, cache: Map<string, LayerPanelEntry>) {
|
||||||
return pathLengthsEqual && elements[0].path.slice(0, -1).every((layerId, i) => layerId === layer.path[i]);
|
folder.children.forEach((item) => {
|
||||||
|
// TODO: fix toString
|
||||||
|
path.push(BigInt(item.layerId.toString()));
|
||||||
|
const mapping = cache.get(path.toString());
|
||||||
|
if (mapping) layers.push(mapping);
|
||||||
|
if (item.children.length > 1) recurse(item, layers, cache);
|
||||||
|
path.pop();
|
||||||
});
|
});
|
||||||
elements.forEach((nlayer) => {
|
|
||||||
const index = layers.findIndex((layer: LayerPanelEntry) => {
|
|
||||||
const pathLengthsEqual = nlayer.path.length === layer.path.length;
|
|
||||||
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
|
|
||||||
});
|
|
||||||
if (index >= 0) {
|
|
||||||
lastInsertion = index;
|
|
||||||
layers[index] = nlayer;
|
|
||||||
} else {
|
|
||||||
lastInsertion += 1;
|
|
||||||
layers.splice(lastInsertion, 0, nlayer);
|
|
||||||
}
|
}
|
||||||
|
recurse(expandData, this.layers, this.layerCache);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
mergeIntoExisting(responseLayers, this.layers);
|
|
||||||
const newLayers: Array<LayerPanelEntry> = [];
|
|
||||||
this.layers.forEach((layer) => {
|
|
||||||
const index = responseLayers.findIndex((nlayer: LayerPanelEntry) => {
|
|
||||||
const pathLengthsEqual = responsePath.length + 1 === layer.path.length;
|
|
||||||
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
|
|
||||||
});
|
|
||||||
if (index >= 0 || layer.path.length !== responsePath.length + 1) {
|
|
||||||
newLayers.push(layer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.layers = newLayers;
|
|
||||||
|
|
||||||
this.setBlendModeForSelectedLayers();
|
|
||||||
this.setOpacityForSelectedLayers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerResponseHandler(ResponseType.CollapseFolder, (responseData) => {
|
|
||||||
const collapseData = responseData as CollapseFolder;
|
|
||||||
if (collapseData) {
|
|
||||||
const responsePath = collapseData.path;
|
|
||||||
|
|
||||||
const newLayers: Array<LayerPanelEntry> = [];
|
|
||||||
this.layers.forEach((layer) => {
|
|
||||||
if (responsePath.length >= layer.path.length || !responsePath.every((layerId, i) => layerId === layer.path[i])) {
|
|
||||||
newLayers.push(layer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.layers = newLayers;
|
|
||||||
|
|
||||||
this.setBlendModeForSelectedLayers();
|
|
||||||
this.setOpacityForSelectedLayers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerResponseHandler(ResponseType.UpdateLayer, (responseData) => {
|
registerResponseHandler(ResponseType.UpdateLayer, (responseData) => {
|
||||||
const updateData = responseData as UpdateLayer;
|
const updateData = responseData as UpdateLayer;
|
||||||
if (updateData) {
|
if (updateData) {
|
||||||
const responsePath = updateData.path;
|
const responsePath = updateData.path;
|
||||||
const responseLayer = updateData.data;
|
const responseLayer = updateData.data;
|
||||||
|
|
||||||
const index = this.layers.findIndex((layer: LayerPanelEntry) => {
|
const layer = this.layerCache.get(responsePath.toString());
|
||||||
const pathLengthsEqual = responsePath.length === layer.path.length;
|
if (layer) Object.assign(this.layerCache.get(responsePath.toString()), responseLayer);
|
||||||
return pathLengthsEqual && responsePath.every((layerId, i) => layerId === layer.path[i]);
|
else this.layerCache.set(responsePath.toString(), responseLayer);
|
||||||
});
|
|
||||||
if (index >= 0) this.layers[index] = responseLayer;
|
|
||||||
|
|
||||||
this.setBlendModeForSelectedLayers();
|
this.setBlendModeForSelectedLayers();
|
||||||
this.setOpacityForSelectedLayers();
|
this.setOpacityForSelectedLayers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
blendModeEntries,
|
|
||||||
blendModeSelectedIndex: 0,
|
|
||||||
blendModeDropdownDisabled: true,
|
|
||||||
opacityNumberInputDisabled: true,
|
|
||||||
layers: [] as Array<LayerPanelEntry>,
|
|
||||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
|
||||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
|
||||||
opacity: 100,
|
|
||||||
MenuDirection,
|
|
||||||
SeparatorType,
|
|
||||||
LayerType,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
LayoutRow,
|
LayoutRow,
|
||||||
LayoutCol,
|
LayoutCol,
|
||||||
|
|
|
@ -293,7 +293,6 @@ export default defineComponent({
|
||||||
this.setClosed();
|
this.setClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
const eventIncludesLmb = Boolean(e.buttons & 1);
|
const eventIncludesLmb = Boolean(e.buttons & 1);
|
||||||
|
|
||||||
// Clean up any messes from lost mouseup events
|
// Clean up any messes from lost mouseup events
|
||||||
|
|
|
@ -4,22 +4,32 @@ import { fullscreenModeChanged } from "@/utilities/fullscreen";
|
||||||
import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input";
|
import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input";
|
||||||
import "@/utilities/errors";
|
import "@/utilities/errors";
|
||||||
import App from "@/App.vue";
|
import App from "@/App.vue";
|
||||||
|
import { panicProxy } from "@/utilities/panic-proxy";
|
||||||
|
|
||||||
// Bind global browser events
|
const wasm = import("@/../wasm/pkg").then(panicProxy);
|
||||||
window.addEventListener("resize", onWindowResize);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
window.addEventListener("DOMContentLoaded", onWindowResize);
|
(window as any).wasmMemory = undefined;
|
||||||
|
|
||||||
document.addEventListener("contextmenu", (e) => e.preventDefault());
|
(async () => {
|
||||||
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(window as any).wasmMemory = (await wasm).wasm_memory;
|
||||||
|
|
||||||
window.addEventListener("keyup", onKeyUp);
|
// Initialize the Vue application
|
||||||
window.addEventListener("keydown", onKeyDown);
|
createApp(App).mount("#app");
|
||||||
|
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
// Bind global browser events
|
||||||
window.addEventListener("mousedown", onMouseDown);
|
window.addEventListener("resize", onWindowResize);
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
onWindowResize();
|
||||||
|
|
||||||
window.addEventListener("wheel", onMouseScroll, { passive: false });
|
document.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||||
|
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
|
||||||
|
|
||||||
// Initialize the Vue application
|
window.addEventListener("keyup", onKeyUp);
|
||||||
createApp(App).mount("#app");
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mousedown", onMouseDown);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
|
||||||
|
window.addEventListener("wheel", onMouseScroll, { passive: false });
|
||||||
|
})();
|
||||||
|
|
|
@ -126,6 +126,5 @@ export async function onWindowResize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
|
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
|
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,7 @@ export enum ResponseType {
|
||||||
ExportDocument = "ExportDocument",
|
ExportDocument = "ExportDocument",
|
||||||
SaveDocument = "SaveDocument",
|
SaveDocument = "SaveDocument",
|
||||||
OpenDocumentBrowse = "OpenDocumentBrowse",
|
OpenDocumentBrowse = "OpenDocumentBrowse",
|
||||||
ExpandFolder = "ExpandFolder",
|
DisplayFolderTreeStructure = "DisplayFolderTreeStructure",
|
||||||
CollapseFolder = "CollapseFolder",
|
|
||||||
UpdateLayer = "UpdateLayer",
|
UpdateLayer = "UpdateLayer",
|
||||||
SetActiveTool = "SetActiveTool",
|
SetActiveTool = "SetActiveTool",
|
||||||
SetActiveDocument = "SetActiveDocument",
|
SetActiveDocument = "SetActiveDocument",
|
||||||
|
@ -56,10 +55,8 @@ function parseResponse(responseType: string, data: any): Response {
|
||||||
switch (responseType) {
|
switch (responseType) {
|
||||||
case "DocumentChanged":
|
case "DocumentChanged":
|
||||||
return newDocumentChanged(data.DocumentChanged);
|
return newDocumentChanged(data.DocumentChanged);
|
||||||
case "CollapseFolder":
|
case "DisplayFolderTreeStructure":
|
||||||
return newCollapseFolder(data.CollapseFolder);
|
return newDisplayFolderTreeStructure(data.DisplayFolderTreeStructure);
|
||||||
case "ExpandFolder":
|
|
||||||
return newExpandFolder(data.ExpandFolder);
|
|
||||||
case "SetActiveTool":
|
case "SetActiveTool":
|
||||||
return newSetActiveTool(data.SetActiveTool);
|
return newSetActiveTool(data.SetActiveTool);
|
||||||
case "SetActiveDocument":
|
case "SetActiveDocument":
|
||||||
|
@ -97,7 +94,7 @@ function parseResponse(responseType: string, data: any): Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
|
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | UpdateLayer | DocumentChanged | DisplayFolderTreeStructure | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
|
||||||
|
|
||||||
export interface UpdateOpenDocumentsList {
|
export interface UpdateOpenDocumentsList {
|
||||||
open_documents: Array<string>;
|
open_documents: Array<string>;
|
||||||
|
@ -239,13 +236,61 @@ function newDocumentChanged(_: any): DocumentChanged {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapseFolder {
|
export interface DisplayFolderTreeStructure {
|
||||||
path: BigUint64Array;
|
layerId: BigInt;
|
||||||
|
children: DisplayFolderTreeStructure[];
|
||||||
}
|
}
|
||||||
function newCollapseFolder(input: any): CollapseFolder {
|
function newDisplayFolderTreeStructure(input: any): DisplayFolderTreeStructure {
|
||||||
return {
|
const { ptr, len } = input.data_buffer;
|
||||||
path: newPath(input.path),
|
const wasmMemoryBuffer = (window as any).wasmMemory().buffer;
|
||||||
};
|
|
||||||
|
// Decode the folder structure encoding
|
||||||
|
const encoding = new DataView(wasmMemoryBuffer, ptr, len);
|
||||||
|
|
||||||
|
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
|
||||||
|
const structureSectionLength = Number(encoding.getBigUint64(0, true));
|
||||||
|
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, ptr + 8, structureSectionLength * 8);
|
||||||
|
|
||||||
|
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
|
||||||
|
const layerIdsSection = new DataView(wasmMemoryBuffer, ptr + 8 + structureSectionLength * 8);
|
||||||
|
|
||||||
|
let layersEncountered = 0;
|
||||||
|
let currentFolder: DisplayFolderTreeStructure = { layerId: BigInt(-1), children: [] };
|
||||||
|
const currentFolderStack = [currentFolder];
|
||||||
|
|
||||||
|
for (let i = 0; i < structureSectionLength; i += 1) {
|
||||||
|
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
|
||||||
|
const msbMask = BigInt(1) << BigInt(63);
|
||||||
|
|
||||||
|
// Set the MSB to 0 to clear the sign and then read the number as usual
|
||||||
|
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
|
||||||
|
|
||||||
|
// Store child folders in the current folder (until we are interrupted by an indent)
|
||||||
|
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
|
||||||
|
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
|
||||||
|
layersEncountered += 1;
|
||||||
|
|
||||||
|
const childLayer = { layerId, children: [] };
|
||||||
|
currentFolder.children.push(childLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the sign of the MSB, where a 1 is a negative (outward) indent
|
||||||
|
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
|
||||||
|
// debugger;
|
||||||
|
// Inward
|
||||||
|
if (subsequentDirectionOfDepthChange) {
|
||||||
|
currentFolderStack.push(currentFolder);
|
||||||
|
currentFolder = currentFolder.children[currentFolder.children.length - 1];
|
||||||
|
}
|
||||||
|
// Outward
|
||||||
|
else {
|
||||||
|
const popped = currentFolderStack.pop();
|
||||||
|
if (!popped) throw Error("Too many negative indents in the folder structure");
|
||||||
|
if (popped) currentFolder = popped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateLayer {
|
export interface UpdateLayer {
|
||||||
|
@ -259,17 +304,6 @@ function newUpdateLayer(input: any): UpdateLayer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpandFolder {
|
|
||||||
path: BigUint64Array;
|
|
||||||
children: Array<LayerPanelEntry>;
|
|
||||||
}
|
|
||||||
function newExpandFolder(input: any): ExpandFolder {
|
|
||||||
return {
|
|
||||||
path: newPath(input.path),
|
|
||||||
children: input.children.map((child: any) => newLayerPanelEntry(child)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SetCanvasZoom {
|
export interface SetCanvasZoom {
|
||||||
new_zoom: number;
|
new_zoom: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,7 +186,6 @@ function htmlDecode(input) {
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-cond-assign
|
// eslint-disable-next-line no-cond-assign
|
||||||
if ((match = entityCode.match(/^#(\d+)$/))) {
|
if ((match = entityCode.match(/^#(\d+)$/))) {
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
return String.fromCharCode(~~match[1]);
|
return String.fromCharCode(~~match[1]);
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
|
|
|
@ -20,6 +20,11 @@ pub fn intentional_panic() {
|
||||||
panic!();
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn wasm_memory() -> JsValue {
|
||||||
|
wasm_bindgen::memory()
|
||||||
|
}
|
||||||
|
|
||||||
/// Modify the currently selected tool in the document state store
|
/// Modify the currently selected tool in the document state store
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn select_tool(tool: String) -> Result<(), JsValue> {
|
pub fn select_tool(tool: String) -> Result<(), JsValue> {
|
||||||
|
|
|
@ -117,49 +117,6 @@ impl Document {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_structure(folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>) {
|
|
||||||
let mut space = 0;
|
|
||||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
|
||||||
data.push(*id);
|
|
||||||
match layer.data {
|
|
||||||
LayerDataType::Shape(_) => space += 1,
|
|
||||||
LayerDataType::Folder(ref folder) => {
|
|
||||||
structure.push(space);
|
|
||||||
Document::serialize_structure(folder, structure, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
structure.push(space | 1 << 63);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serializes the layer structure into a compressed 1d structure
|
|
||||||
/// 4,2,1,-2-0,10,12,13,14,15 <- input data
|
|
||||||
/// l = 4 = structure.len() <- length of the structure section
|
|
||||||
/// structure = 2,1,-2,-0 <- structure section
|
|
||||||
/// data = 10,12,13,14,15 <- data section
|
|
||||||
///
|
|
||||||
/// the numbers in the structure block encode the indentation,
|
|
||||||
/// 2 mean read two element from the data section, then place a [
|
|
||||||
/// -x means read x elements from the date section an then insert a ]
|
|
||||||
///
|
|
||||||
/// 2 V 1 V -2 A -0 A
|
|
||||||
/// 10,12, 13, 14,15
|
|
||||||
/// 10,12,[ 13,[14,15] ]
|
|
||||||
///
|
|
||||||
/// resulting layer panel:
|
|
||||||
/// 10
|
|
||||||
/// 12
|
|
||||||
/// [12,13]
|
|
||||||
/// [12,13,14]
|
|
||||||
/// [12,13,15]
|
|
||||||
pub fn serialize_root(&self) -> Vec<LayerId> {
|
|
||||||
let (mut structure, mut data) = (vec![0], Vec::new());
|
|
||||||
Document::serialize_structure(self.root.as_folder().unwrap(), &mut structure, &mut data);
|
|
||||||
structure[0] = structure.len() as u64 - 1;
|
|
||||||
structure.extend(data);
|
|
||||||
structure
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a path to a layer, returns a vector of the indices in the layer tree
|
/// Given a path to a layer, returns a vector of the indices in the layer tree
|
||||||
/// These indices can be used to order a list of layers
|
/// These indices can be used to order a list of layers
|
||||||
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
||||||
|
@ -446,10 +403,24 @@ 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::DeleteLayer { path } => {
|
Operation::DeleteLayer { path } => {
|
||||||
|
fn aggregate_deletions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
||||||
|
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||||
|
path.push(*id);
|
||||||
|
responses.push(DocumentResponse::DeletedLayer { path: path.clone() });
|
||||||
|
if let LayerDataType::Folder(f) = &layer.data {
|
||||||
|
aggregate_deletions(f, path, responses);
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
if let Ok(folder) = self.folder(path) {
|
||||||
|
aggregate_deletions(folder, &mut path.clone(), &mut responses)
|
||||||
|
};
|
||||||
self.delete(path)?;
|
self.delete(path)?;
|
||||||
|
|
||||||
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||||
let mut responses = vec![DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }];
|
responses.extend([DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]);
|
||||||
responses.extend(update_thumbnails_upstream(folder));
|
responses.extend(update_thumbnails_upstream(folder));
|
||||||
Some(responses)
|
Some(responses)
|
||||||
}
|
}
|
||||||
|
@ -458,8 +429,22 @@ impl Document {
|
||||||
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||||
let full_path = [path.clone(), vec![id]].concat();
|
let full_path = [path.clone(), vec![id]].concat();
|
||||||
self.mark_as_dirty(&full_path)?;
|
self.mark_as_dirty(&full_path)?;
|
||||||
|
fn aggregate_insertions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
||||||
|
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||||
|
path.push(*id);
|
||||||
|
responses.push(DocumentResponse::CreatedLayer { path: path.clone() });
|
||||||
|
if let LayerDataType::Folder(f) = &layer.data {
|
||||||
|
aggregate_insertions(f, path, responses);
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
if let Ok(folder) = self.folder(&full_path) {
|
||||||
|
aggregate_insertions(folder, &mut full_path.clone(), &mut responses)
|
||||||
|
};
|
||||||
|
|
||||||
let mut responses = vec![DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }];
|
responses.extend([DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }]);
|
||||||
responses.extend(update_thumbnails_upstream(path));
|
responses.extend(update_thumbnails_upstream(path));
|
||||||
Some(responses)
|
Some(responses)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue