Add folders to frontend and folder creation to backend (#315)
* Add folders to frontend and folder creation to backend Closes #149 * Add Group keybind * Add logic to handle expanding of folders * Send all paths as (u32, u32) * Add custom serialization for path * Merge two layer_panel files * Refactor frontend layer merging * Fix JS linting * Update upstream thumbnail changes * Add paste into selected folder + fix thumbnail dirtification * Implement CollapseFolder function * Skip folders on a different indentation level during reorder * Only reorder within the same folder * Add folder node icon for folder layers * Add expand/collapse folder button; partly implement new layer tree design * Update terminology in the docs * Add number labels to ruler marks * Replace promise with await in MenuList.vue * Miscellaneous minor code cleanup * Disallow snake_case variable names in frontend * Add support for saving and opening files (#325) * Add support for saving a document This is similar to the "export" functionality, except that we store all metadata needed to open the file again. Currently we store the internal representation of the layer which is probably pretty fragile. * Add support for opening a saved document User can select a file using the browser's file input selector. We parse it as JSON and load it into the internal representation. Concerns: - The file format is fragile - Loading data directly into internal data structures usually creates security vulnerabilities - Error handling: The user is not informed of errors * Serialize Document and skip "cache" fields in Layer Instead of serializing the root layer, we serialize the Document struct directly. Additionally, we mark the "cache" fields in layer as "skip" fields so they don't get serialized. * Opened files use the filename as the tab title * Split "new document" and "open document" handling Open document needs name and content to be provided so having a different interface is cleaner. Also did some refactoring to reuse code. * Show error to user when a file fails to open * Clean up code: better variable naming and structure * Use document name for saved and exported files We pass through the document name in the export and save messages. Additionally, we check if the appropriate file suffixes (.graphite and .svg) need to be added before passing it to the frontend. * Refactor document name generation * Don't assign a default of 1 to Documents that start with something other than DEFAULT_DOCUMENT_NAME * Improve runtime complexity by using binary instead of linear search * Update Layer panel upon document selection * Add File>Open/Ctrl+O; File>Save (As)/Ctrl+(Shift)+S; browse filters extension; split out download()/upload() into files.ts; change unsaved close dialog text Co-authored-by: Dennis Kobert <dennis@kobert.dev> Co-authored-by: Keavon Chambers <keavon@keavon.com> * Refactor ViewportPosition from u32 (UVec2) to f64 (DVec2) (#345) * Refactor ViewportPosition from u32 (UVec2) to f64 (DVec2) * Fix pseudo_hash call * Replace hash function with proper function for uuid generation * Cargo fmt Co-authored-by: Dennis Kobert <dennis@kobert.dev> * Improve Frontend -> Backend user input system (#348) Includes refactor that sends coordinates of the document viewports to the backend so input is sent relative to the application window Closes #124 Fixes #291 * Improve Frontend -> Backend user input system * Code review changes * More code review changes * Fix TS error * Update the readme * Make scrollbars interactable (#328) * Make scrollbars interactable * Add watcher for position change * Fix case of data * Fix updateHandlePosition capitalization * Clean up class name thing * Scroll bars between 0 and 1 * Allow width to be 100% * Scrollbars reflect backend * Include viewport in scrollbar * Add half viewport padding for scrollbars * Refactor scrollbar using lerp * Send messages to backend * Refactor * Use glam::DVec2 * Remove glam:: * Remove unnecessary abs * Add TrueDoctor's change * Add missing minus * Fix vue issues * Fix viewport size * Remove unnecessary log * Linear dragging * Improve scrollbar behavior (#351) * Change scrollbar behavior * Leave space at the end of the scrollbar * Change mid to center * Use shorter array initialization * Add space around scrollbar * Fix scrollbar spacing * Smooth end of scrollbars * Add page up and down * Page up and down on click in scrollbar track * Add shift pageup to translate horizontally * Implement bounding box for selected layers (#349) * Implement bounding box for selected layers * Add shift modifier for multi selection * Fix collapsing of folders * Add have pixel offset to selection bounding box * Don't panic on Ctrl + A * Rename to camel case * Add todo comment for Keavon * Apply @Hypercubes review suggestions * Fix many panics, improve behavior of copy/paste and grouping (but grouping still can panic) Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
e75714330c
commit
6b274b3f1c
|
|
@ -126,8 +126,8 @@ mod test {
|
|||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::Copy).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
|
|
@ -159,8 +159,8 @@ mod test {
|
|||
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
||||
|
||||
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]])).unwrap();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::Copy).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ mod test {
|
|||
const LINE_INDEX: usize = 0;
|
||||
const PEN_INDEX: usize = 1;
|
||||
|
||||
editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
|
||||
editor.handle_message(DocumentMessage::CreateFolder(vec![])).unwrap();
|
||||
|
||||
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
|
||||
|
|
@ -222,10 +222,10 @@ mod test {
|
|||
|
||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::Copy).unwrap();
|
||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
|
|
@ -283,11 +283,11 @@ mod test {
|
|||
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
||||
|
||||
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::Copy).unwrap();
|
||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
|
||||
editor.draw_rect(0., 800., 12., 200.);
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
pub use super::layer_panel::*;
|
||||
use crate::{
|
||||
consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING},
|
||||
frontend::layer_panel::*,
|
||||
EditorError,
|
||||
};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -93,9 +92,9 @@ pub enum DocumentMessage {
|
|||
DeleteLayer(Vec<LayerId>),
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
CreateFolder(Vec<LayerId>),
|
||||
SetBlendModeForSelectedLayers(BlendMode),
|
||||
SetOpacityForSelectedLayers(f64),
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
FlipSelectedLayers(FlipAxis),
|
||||
|
|
@ -103,6 +102,7 @@ pub enum DocumentMessage {
|
|||
FolderChanged(Vec<LayerId>),
|
||||
StartTransaction,
|
||||
RollbackTransaction,
|
||||
GroupSelectedLayers,
|
||||
AbortTransaction,
|
||||
CommitTransaction,
|
||||
ExportDocument,
|
||||
|
|
@ -139,7 +139,7 @@ impl DocumentMessageHandler {
|
|||
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, children }.into()
|
||||
FrontendMessage::ExpandFolder { path: path.into(), children }.into()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ impl DocumentMessageHandler {
|
|||
self.layer_data(path).selected = true;
|
||||
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
||||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), 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]> {
|
||||
|
|
@ -165,9 +165,9 @@ impl DocumentMessageHandler {
|
|||
// TODO: Consider moving this to some kind of overlay manager in the future
|
||||
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
|
||||
let shapes = self.selected_layers().filter_map(|path_to_shape| {
|
||||
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape.as_slice()).ok()?;
|
||||
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
|
||||
|
||||
let shape = match &self.document.layer(path_to_shape.as_slice()).ok()?.data {
|
||||
let shape = match &self.document.layer(path_to_shape).ok()?.data {
|
||||
LayerDataType::Shape(shape) => Some(shape),
|
||||
LayerDataType::Folder(_) => None,
|
||||
}?;
|
||||
|
|
@ -205,8 +205,8 @@ impl DocumentMessageHandler {
|
|||
self.layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
|
||||
}
|
||||
|
||||
pub fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
|
||||
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
|
||||
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
|
||||
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
|
||||
|
|
@ -326,7 +326,7 @@ impl DocumentMessageHandler {
|
|||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
||||
let layer = self.document.layer(&path)?;
|
||||
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path).unwrap(), layer, path);
|
||||
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path)?, layer, path);
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
|
|
@ -362,7 +362,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
match message {
|
||||
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
|
||||
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
|
||||
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
|
||||
StartTransaction => self.backup(),
|
||||
RollbackTransaction => {
|
||||
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
|
|
@ -409,6 +408,32 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
.into(),
|
||||
)
|
||||
}
|
||||
CreateFolder(mut path) => {
|
||||
let id = generate_uuid();
|
||||
path.push(id);
|
||||
self.layerdata_mut(&path).expanded = true;
|
||||
responses.push_back(DocumentOperation::CreateFolder { path }.into())
|
||||
}
|
||||
GroupSelectedLayers => {
|
||||
let common_prefix = self.document.common_prefix(self.selected_layers());
|
||||
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
|
||||
|
||||
let mut new_folder_path = common_prefix.to_vec();
|
||||
new_folder_path.push(generate_uuid());
|
||||
|
||||
responses.push_back(DocumentsMessage::Copy.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
|
||||
responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into());
|
||||
responses.push_back(
|
||||
DocumentsMessage::PasteIntoFolder {
|
||||
path: new_folder_path.clone(),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
|
||||
}
|
||||
SetBlendModeForSelectedLayers(blend_mode) => {
|
||||
self.backup();
|
||||
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
|
|
@ -419,7 +444,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
self.backup();
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
||||
for path in self.selected_layers().cloned() {
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
|
||||
}
|
||||
}
|
||||
|
|
@ -428,7 +453,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
ToggleLayerExpansion(path) => {
|
||||
self.layer_data(&path).expanded ^= true;
|
||||
responses.push_back(FolderChanged(path).into());
|
||||
match self.layer_data(&path).expanded {
|
||||
true => responses.push_back(FolderChanged(path.clone()).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 => {
|
||||
// TODO: Hoist this duplicated code into wider system
|
||||
|
|
@ -437,7 +466,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
DeleteSelectedLayers => {
|
||||
self.backup();
|
||||
responses.push_front(ToolMessage::SelectedLayersChanged.into());
|
||||
for path in self.selected_layers().cloned() {
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
|
||||
}
|
||||
}
|
||||
|
|
@ -469,14 +498,12 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
let all_layer_paths = self
|
||||
.layer_data
|
||||
.keys()
|
||||
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
|
||||
.filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
responses.push_front(SetSelectedLayers(all_layer_paths).into());
|
||||
}
|
||||
DeselectAllLayers => {
|
||||
responses.push_front(SetSelectedLayers(vec![]).into());
|
||||
}
|
||||
DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()),
|
||||
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
Undo => {
|
||||
|
|
@ -505,18 +532,19 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
self.layer_data.remove(&path);
|
||||
Some(ToolMessage::SelectedLayersChanged.into())
|
||||
}
|
||||
DocumentResponse::LayerChanged { path } => (!self.document.layer(&path).unwrap().overlay).then(|| {
|
||||
FrontendMessage::UpdateLayer {
|
||||
path: path.clone(),
|
||||
data: self.layer_panel_entry(path).unwrap(),
|
||||
}
|
||||
.into()
|
||||
DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
|
||||
let overlay = self.document.layer(&path).unwrap().overlay;
|
||||
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
|
||||
}),
|
||||
DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()),
|
||||
DocumentResponse::CreatedLayer { path } => {
|
||||
self.layer_data.insert(path.clone(), LayerData::new(false));
|
||||
(!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into())
|
||||
}
|
||||
DocumentResponse::DocumentChanged => Some(RenderDocument.into()),
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
log::debug!("LayerPanel: {:?}", self.layer_data.keys());
|
||||
}
|
||||
Err(e) => log::error!("DocumentError: {:?}", e),
|
||||
Ok(_) => (),
|
||||
|
|
@ -551,7 +579,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
self.backup();
|
||||
for path in self.selected_layers().cloned() {
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
let operation = DocumentOperation::TransformLayerInViewport {
|
||||
path,
|
||||
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
|
||||
|
|
@ -561,9 +589,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(ToolMessage::SelectedLayersChanged.into());
|
||||
}
|
||||
MoveSelectedLayersTo { path, insert_index } => {
|
||||
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
|
||||
responses.push_back(DocumentsMessage::Copy.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
|
||||
responses.push_back(DocumentsMessage::PasteIntoFolder { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_position) => {
|
||||
self.backup();
|
||||
|
|
@ -574,7 +602,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
1 => selected_layers.last(),
|
||||
_ => unreachable!(),
|
||||
} {
|
||||
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
|
||||
let all_layer_paths: Vec<_> = all_layer_paths
|
||||
.iter()
|
||||
.filter(|layer| layer.starts_with(&pivot[0..pivot.len() - 1]) && pivot.len() == layer.len())
|
||||
.collect();
|
||||
if let Some(pos) = all_layer_paths.iter().position(|path| *path == pivot) {
|
||||
let max = all_layer_paths.len() as i64 - 1;
|
||||
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
|
||||
let insert = all_layer_paths.get(insert_pos);
|
||||
|
|
@ -602,13 +634,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
FlipAxis::X => 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.as_slice())) {
|
||||
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
||||
let center = (max + min) / 2.;
|
||||
let bbox_trans = DAffine2::from_translation(-center);
|
||||
for path in self.selected_layers() {
|
||||
responses.push_back(
|
||||
DocumentOperation::TransformLayerInScope {
|
||||
path: path.clone(),
|
||||
path: path.to_vec(),
|
||||
transform: DAffine2::from_scale(scale).to_cols_array(),
|
||||
scope: bbox_trans.to_cols_array(),
|
||||
}
|
||||
|
|
@ -627,7 +659,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
AlignAxis::Y => DVec2::Y,
|
||||
};
|
||||
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.as_slice())) {
|
||||
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
|
||||
let aggregated = match aggregate {
|
||||
AlignAggregate::Min => combined_box[0],
|
||||
AlignAggregate::Max => combined_box[1],
|
||||
|
|
@ -643,7 +675,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
let translation = (aggregated - center) * axis;
|
||||
responses.push_back(
|
||||
DocumentOperation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
path: path.to_vec(),
|
||||
transform: DAffine2::from_translation(translation).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
|
|
@ -673,6 +705,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
DuplicateSelectedLayers,
|
||||
NudgeSelectedLayers,
|
||||
ReorderSelectedLayers,
|
||||
GroupSelectedLayers,
|
||||
);
|
||||
common.extend(select);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::input::InputPreprocessor;
|
||||
use crate::message_prelude::*;
|
||||
use graphene::layers::Layer;
|
||||
use graphene::layers::{Layer, LayerDataType};
|
||||
use graphene::{LayerId, Operation as DocumentOperation};
|
||||
use log::warn;
|
||||
|
||||
|
|
@ -13,11 +13,12 @@ use crate::consts::DEFAULT_DOCUMENT_NAME;
|
|||
#[impl_message(Message, Documents)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DocumentsMessage {
|
||||
CopySelectedLayers,
|
||||
PasteLayers {
|
||||
Copy,
|
||||
PasteIntoFolder {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
Paste,
|
||||
SelectDocument(usize),
|
||||
CloseDocument(usize),
|
||||
#[child]
|
||||
|
|
@ -79,7 +80,7 @@ impl DocumentsMessageHandler {
|
|||
|
||||
responses.push_back(
|
||||
FrontendMessage::ExpandFolder {
|
||||
path: Vec::new(),
|
||||
path: Vec::new().into(),
|
||||
children: Vec::new(),
|
||||
}
|
||||
.into(),
|
||||
|
|
@ -156,7 +157,13 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
}
|
||||
|
||||
let lp = self.active_document_mut().layer_panel(&[]).expect("Could not get panel for active doc");
|
||||
responses.push_back(FrontendMessage::ExpandFolder { path: Vec::new(), children: lp }.into());
|
||||
responses.push_back(
|
||||
FrontendMessage::ExpandFolder {
|
||||
path: Vec::new().into(),
|
||||
children: lp,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(
|
||||
FrontendMessage::SetActiveDocument {
|
||||
document_index: self.active_document_index,
|
||||
|
|
@ -211,7 +218,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
let id = (self.active_document_index + self.documents.len() - 1) % self.documents.len();
|
||||
responses.push_back(SelectDocument(id).into());
|
||||
}
|
||||
CopySelectedLayers => {
|
||||
Copy => {
|
||||
let paths = self.active_document().selected_layers_sorted();
|
||||
self.copy_buffer.clear();
|
||||
for path in paths {
|
||||
|
|
@ -223,9 +230,24 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
}
|
||||
}
|
||||
}
|
||||
PasteLayers { path, insert_index } => {
|
||||
Paste => {
|
||||
let document = self.active_document();
|
||||
let shallowest_common_folder = document
|
||||
.document
|
||||
.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");
|
||||
|
||||
responses.push_back(
|
||||
PasteIntoFolder {
|
||||
path: shallowest_common_folder.to_vec(),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
PasteIntoFolder { path, insert_index } => {
|
||||
let paste = |layer: &Layer, responses: &mut VecDeque<_>| {
|
||||
log::trace!("pasting into folder {:?} as index: {}", path, insert_index);
|
||||
log::trace!("Pasting into folder {:?} as index: {}", path, insert_index);
|
||||
responses.push_back(
|
||||
DocumentOperation::PasteLayer {
|
||||
layer: layer.clone(),
|
||||
|
|
@ -255,12 +277,13 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
CloseAllDocuments,
|
||||
NextDocument,
|
||||
PrevDocument,
|
||||
PasteLayers,
|
||||
PasteIntoFolder,
|
||||
Paste,
|
||||
);
|
||||
|
||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||
let select = actions!(DocumentsMessageDiscriminant;
|
||||
CopySelectedLayers,
|
||||
Copy,
|
||||
);
|
||||
common.extend(select);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use crate::{consts::VIEWPORT_ROTATE_SNAP_INTERVAL, frontend::layer_panel::*};
|
||||
use crate::consts::VIEWPORT_ROTATE_SNAP_INTERVAL;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene::layers::{BlendMode, LayerDataType};
|
||||
use graphene::{
|
||||
layers::{Layer, LayerData as DocumentLayerData},
|
||||
LayerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ser::SerializeSeq, Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
|
||||
pub struct LayerData {
|
||||
|
|
@ -49,10 +51,7 @@ impl LayerData {
|
|||
}
|
||||
|
||||
pub fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[LayerId]) -> &'a mut LayerData {
|
||||
if !layer_data.contains_key(path) {
|
||||
layer_data.insert(path.to_vec(), LayerData::new(false));
|
||||
}
|
||||
layer_data.get_mut(path).unwrap()
|
||||
layer_data.get_mut(path).expect(&format!("Layer data cannot be found because the path {:?} does not exist", path))
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||
|
|
@ -78,9 +77,6 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La
|
|||
String::new()
|
||||
};
|
||||
|
||||
// LayerIds are sent as (u32, u32) because jsond does not support u64s
|
||||
let path = path.iter().map(|id| ((id >> 32) as u32, (id << 32 >> 32) as u32)).collect::<Vec<_>>();
|
||||
|
||||
LayerPanelEntry {
|
||||
name,
|
||||
visible: layer.visible,
|
||||
|
|
@ -88,7 +84,74 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La
|
|||
opacity: layer.opacity,
|
||||
layer_type: (&layer.data).into(),
|
||||
layer_data: *layer_data,
|
||||
path,
|
||||
path: path.into(),
|
||||
thumbnail,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct Path(Vec<LayerId>);
|
||||
|
||||
impl From<Vec<LayerId>> for Path {
|
||||
fn from(iter: Vec<LayerId>) -> Self {
|
||||
Self(iter)
|
||||
}
|
||||
}
|
||||
impl Serialize for Path {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for e in self.0.iter() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// LayerIds are sent as (u32, u32) because json does not support u64s
|
||||
let id = ((e >> 32) as u32, (e << 32 >> 32) as u32);
|
||||
seq.serialize_element(&id)?;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
seq.serialize_element(e)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LayerPanelEntry {
|
||||
pub name: String,
|
||||
pub visible: bool,
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f64,
|
||||
pub layer_type: LayerType,
|
||||
pub layer_data: LayerData,
|
||||
pub path: crate::document::layer_panel::Path,
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum LayerType {
|
||||
Folder,
|
||||
Shape,
|
||||
}
|
||||
|
||||
impl fmt::Display for LayerType {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
let name = match self {
|
||||
LayerType::Folder => "Folder",
|
||||
LayerType::Shape => "Shape",
|
||||
};
|
||||
|
||||
formatter.write_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LayerDataType> for LayerType {
|
||||
fn from(data: &LayerDataType) -> Self {
|
||||
use LayerDataType::*;
|
||||
match data {
|
||||
Folder(_) => LayerType::Folder,
|
||||
Shape(_) => LayerType::Shape,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
mod document_file;
|
||||
mod document_message_handler;
|
||||
mod layer_panel;
|
||||
pub mod layer_panel;
|
||||
mod movement_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::frontend::layer_panel::LayerPanelEntry;
|
||||
use crate::document::layer_panel::{LayerPanelEntry, Path};
|
||||
use crate::message_prelude::*;
|
||||
use crate::tool::tool_options::ToolOptions;
|
||||
use crate::Color;
|
||||
|
|
@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize};
|
|||
#[impl_message(Message, Frontend)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||
pub enum FrontendMessage {
|
||||
CollapseFolder { path: Vec<LayerId> },
|
||||
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
|
||||
CollapseFolder { path: Path },
|
||||
ExpandFolder { path: Path, children: Vec<LayerPanelEntry> },
|
||||
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
||||
SetActiveDocument { document_index: usize },
|
||||
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
||||
|
|
@ -17,7 +17,7 @@ pub enum FrontendMessage {
|
|||
DisplayConfirmationToCloseAllDocuments,
|
||||
UpdateCanvas { document: String },
|
||||
UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
|
||||
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
|
||||
UpdateLayer { path: Path, data: LayerPanelEntry },
|
||||
ExportDocument { document: String, name: String },
|
||||
SaveDocument { document: String, name: String },
|
||||
OpenDocumentBrowse,
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
use crate::document::LayerData;
|
||||
use graphene::layers::{BlendMode, LayerDataType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LayerPanelEntry {
|
||||
pub name: String,
|
||||
pub visible: bool,
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f64,
|
||||
pub layer_type: LayerType,
|
||||
pub layer_data: LayerData,
|
||||
// TODO: Instead of turning the u64 into (u32, u32)s here, do that in the WASM translation layer
|
||||
pub path: Vec<(u32, u32)>,
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum LayerType {
|
||||
Folder,
|
||||
Shape,
|
||||
}
|
||||
|
||||
impl fmt::Display for LayerType {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
let name = match self {
|
||||
LayerType::Folder => "Folder",
|
||||
LayerType::Shape => "Shape",
|
||||
};
|
||||
|
||||
formatter.write_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LayerDataType> for LayerType {
|
||||
fn from(data: &LayerDataType) -> Self {
|
||||
use LayerDataType::*;
|
||||
match data {
|
||||
Folder(_) => LayerType::Folder,
|
||||
Shape(_) => LayerType::Shape,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod frontend_message_handler;
|
||||
pub mod layer_panel;
|
||||
|
||||
pub use frontend_message_handler::{FrontendMessage, FrontendMessageDiscriminant};
|
||||
|
|
|
|||
|
|
@ -126,10 +126,11 @@ macro_rules! mapping {
|
|||
impl Default for Mapping {
|
||||
fn default() -> Self {
|
||||
use Key::*;
|
||||
// WARNING!
|
||||
// If a new mapping isn't being handled (and perhaps another lower-precedence one is instead), make sure to advertise
|
||||
// it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`)
|
||||
let mappings = mapping![
|
||||
entry! {action=DocumentsMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::EnableSnapping, key_down=KeyShift},
|
||||
entry! {action=MovementMessage::DisableSnapping, key_up=KeyShift},
|
||||
// Higher priority than entries in sections below
|
||||
// Select
|
||||
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=SelectMessage::DragStart{add_to_selection: KeyShift}, key_down=Lmb},
|
||||
|
|
@ -188,10 +189,12 @@ impl Default for Mapping {
|
|||
// Editor Actions
|
||||
entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]},
|
||||
// Document Actions
|
||||
entry! {action=DocumentsMessage::Paste, key_down=KeyV, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::CreateFolder(vec![]), key_down=KeyN, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||
|
|
@ -225,7 +228,8 @@ impl Default for Mapping {
|
|||
entry! {action=DocumentsMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=DocumentsMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentsMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentsMessage::Copy, key_down=KeyC, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::GroupSelectedLayers, key_down=KeyG},
|
||||
// Nudging
|
||||
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
|
||||
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ impl ToolType {
|
|||
ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New },
|
||||
ToolType::Pen => ToolOptions::Pen { weight: 5 },
|
||||
ToolType::Line => ToolOptions::Line { weight: 5 },
|
||||
ToolType::Ellipse => ToolOptions::Ellipse,
|
||||
ToolType::Ellipse => ToolOptions::Ellipse {},
|
||||
ToolType::Shape => ToolOptions::Shape {
|
||||
shape_type: ShapeType::Polygon { vertices: 6 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum ToolOptions {
|
||||
Select { append_mode: SelectAppendMode },
|
||||
Ellipse,
|
||||
Ellipse {},
|
||||
Shape { shape_type: ShapeType },
|
||||
Line { weight: u32 },
|
||||
Pen { weight: u32 },
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ impl Fsm for PathToolFsmState {
|
|||
shape_i += 1;
|
||||
|
||||
for segment in &shape_to_draw.segments {
|
||||
// TODO: We draw each anchor point twice because segment has it on both ends, fix this
|
||||
let (anchors, handles, anchor_handle_lines) = match segment {
|
||||
VectorManipulatorSegment::Line(a1, a2) => (vec![*a1, *a2], vec![], vec![]),
|
||||
VectorManipulatorSegment::Quad(a1, h1, a2) => (vec![*a1, *a2], vec![*h1], vec![(*h1, *a1)]),
|
||||
|
|
|
|||
|
|
@ -129,8 +129,14 @@ impl Fsm for SelectToolFsmState {
|
|||
(None, Some(path)) => Operation::DeleteLayer { path }.into(),
|
||||
(Some([pos1, pos2]), path) => {
|
||||
let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer));
|
||||
|
||||
data.bounding_box_path = Some(path.clone());
|
||||
|
||||
let half_pixel_offset = DVec2::splat(0.5);
|
||||
let pos1 = pos1 + half_pixel_offset;
|
||||
let pos2 = pos2 - half_pixel_offset;
|
||||
let transform = transform_from_box(pos1, pos2);
|
||||
|
||||
Operation::SetLayerTransformInViewport { path, transform }.into()
|
||||
}
|
||||
(_, _) => Message::NoOp,
|
||||
|
|
@ -143,7 +149,7 @@ impl Fsm for SelectToolFsmState {
|
|||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
let mut buffer = Vec::new();
|
||||
let mut selected: Vec<_> = document.selected_layers().cloned().collect();
|
||||
let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect();
|
||||
let quad = data.selection_quad();
|
||||
let intersection = document.document.intersects_quad_root(quad);
|
||||
// If no layer is currently selected and the user clicks on a shape, select that.
|
||||
|
|
@ -185,7 +191,7 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
(DrawingBox, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
let half_pixel_offset = DVec2::new(0.5, 0.5);
|
||||
let half_pixel_offset = DVec2::splat(0.5);
|
||||
let start = data.drag_start + half_pixel_offset;
|
||||
let size = data.drag_current - start + half_pixel_offset;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(7 7)">
|
||||
<path style="fill:#FFFFFF" d="M15.22-2.74l-9-4c-0.78-0.34-1.66-0.34-2.44,0l-9,4C-6.3-2.26-7-1.19-7,0v10c0,1.19,0.7,2.26,1.78,2.74l9,4C4.17,16.91,4.58,17,5,17s0.83-0.09,1.22-0.26l9-4C16.3,12.26,17,11.19,17,10V0C17-1.19,16.3-2.26,15.22-2.74z" />
|
||||
<path style="fill:#DD83FF" d="M13.38,8.93L6,5.47c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,13.81c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,9.96,14.21,9.32,13.38,8.93z" />
|
||||
<path style="opacity:0.4; fill:url(#bottom)" d="M13.38,8.93L6,5.47c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,13.81c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,9.96,14.21,9.32,13.38,8.93z" />
|
||||
<path style="fill:#EAC800" d="M13.38,4.29L6,0.83c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,9.17c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,5.32,14.21,4.68,13.38,4.29z" />
|
||||
<path style="opacity:0.4; fill:url(#middle)" d="M13.38,4.29L6,0.83c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,9.17c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,5.32,14.21,4.68,13.38,4.29z" />
|
||||
<path style="fill:#6EEBFF" d="M13.38-0.35L6-3.81c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,4.53c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,0.68,14.21,0.04,13.38-0.35z" />
|
||||
<path style="opacity:0.4; fill:url(#top)" d="M13.38-0.35L6-3.81c-0.64-0.26-1.36-0.26-2,0l-7.38,3.46c-0.83,0.39-0.83,1.02,0,1.41L4,4.53c0.64,0.26,1.36,0.26,2.01,0l7.38-3.46C14.21,0.68,14.21,0.04,13.38-0.35z" />
|
||||
</g>
|
||||
<linearGradient id="top" gradientUnits="userSpaceOnUse" x1="10.5551" y1="-26.454" x2="10.5551" y2="-25.454" gradientTransform="matrix(18.0005 0 0 -8.7219 -184.997 -226.0052)">
|
||||
<stop offset="0" style="stop-color:#000000; stop-opacity:0" />
|
||||
<stop offset="0.91" style="stop-color:#000000; stop-opacity:0.796" />
|
||||
<stop offset="1" style="stop-color:#000000" />
|
||||
</linearGradient>
|
||||
<linearGradient id="middle" gradientUnits="userSpaceOnUse" x1="10.5551" y1="-26.459" x2="10.5551" y2="-25.459" gradientTransform="matrix(18.0005 0 0 -8.7209 -184.997 -221.3842)">
|
||||
<stop offset="0" style="stop-color:#000000; stop-opacity:0" />
|
||||
<stop offset="0.91" style="stop-color:#000000; stop-opacity:0.796" />
|
||||
<stop offset="1" style="stop-color:#000000" />
|
||||
</linearGradient>
|
||||
<linearGradient id="bottom" gradientUnits="userSpaceOnUse" x1="10.5551" y1="-26.454" x2="10.5551" y2="-25.454" gradientTransform="matrix(18.0005 0 0 -8.7219 -184.997 -216.7282)">
|
||||
<stop offset="0" style="stop-color:#000000; stop-opacity:0" />
|
||||
<stop offset="0.91" style="stop-color:#000000; stop-opacity:0.796" />
|
||||
<stop offset="1" style="stop-color:#000000" />
|
||||
</linearGradient>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path style="fill:#FFFFFF;" d="M23.34,4.06c0.14-0.94-0.18-1.88-0.85-2.55s-1.62-0.98-2.55-0.85c-5.27,0.77-10.61,0.77-15.88,0C3.12,0.53,2.18,0.84,1.51,1.51S0.53,3.13,0.66,4.06c0.77,5.27,0.77,10.61,0,15.88c-0.14,0.94,0.18,1.88,0.85,2.55s1.62,0.98,2.55,0.85c5.27-0.77,10.61-0.77,15.88,0c0.14,0.02,0.29,0.03,0.43,0.03c0.79,0,1.55-0.31,2.12-0.88c0.67-0.67,0.98-1.62,0.85-2.55C22.57,14.67,22.57,9.33,23.34,4.06z" />
|
||||
<path style="fill:#65BBE5;" d="M19.89,9.01c-0.17,0.02-2.18,0.26-4.89,1.01V9H9v3.28c-1.6,0.79-3.2,1.75-4.64,2.95c-0.42,0.35-0.48,0.98-0.13,1.41C4.43,16.88,4.71,17,5,17c0.23,0,0.45-0.08,0.64-0.23C6.68,15.9,7.83,15.16,9,14.53V15h6v-2.9c2.88-0.84,5.07-1.1,5.11-1.11c0.55-0.06,0.94-0.56,0.88-1.11C20.93,9.34,20.43,8.95,19.89,9.01z M13,13h-2v-2h2V13z" />
|
||||
<path style="fill:#FFFFFF" d="M23.34,4.06c0.14-0.94-0.18-1.88-0.85-2.55s-1.62-0.98-2.55-0.85c-5.27,0.77-10.61,0.77-15.88,0C3.12,0.53,2.18,0.84,1.51,1.51S0.53,3.13,0.66,4.06c0.77,5.27,0.77,10.61,0,15.88c-0.14,0.94,0.18,1.88,0.85,2.55s1.62,0.98,2.55,0.85c5.27-0.77,10.61-0.77,15.88,0c0.14,0.02,0.29,0.03,0.43,0.03c0.79,0,1.55-0.31,2.12-0.88c0.67-0.67,0.98-1.62,0.85-2.55C22.57,14.67,22.57,9.33,23.34,4.06z" />
|
||||
<path style="fill:#65BBE5" d="M19.89,9.01c-0.17,0.02-2.18,0.26-4.89,1.01V9H9v3.28c-1.6,0.79-3.2,1.75-4.64,2.95c-0.42,0.35-0.48,0.98-0.13,1.41C4.43,16.88,4.71,17,5,17c0.23,0,0.45-0.08,0.64-0.23C6.68,15.9,7.83,15.16,9,14.53V15h6v-2.9c2.88-0.84,5.07-1.1,5.11-1.11c0.55-0.06,0.94-0.56,0.88-1.11C20.93,9.34,20.43,8.95,19.89,9.01z M13,13h-2v-2h2V13z" />
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 826 B |
|
|
@ -59,6 +59,9 @@
|
|||
--color-accent-hover-rgb: 73, 165, 226;
|
||||
--color-accent-disabled: #416277;
|
||||
--color-accent-disabled-rgb: 65, 98, 119;
|
||||
|
||||
--color-data-raster: #e4bb72;
|
||||
--color-data-raster-rgb: 228, 187, 114;
|
||||
}
|
||||
|
||||
html,
|
||||
|
|
|
|||
|
|
@ -25,9 +25,17 @@
|
|||
:title="layer.visible ? 'Visible' : 'Hidden'"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="layer.layer_type === LayerType.Folder"
|
||||
class="node-connector"
|
||||
:class="{ expanded: layer.layer_data.expanded }"
|
||||
@click.stop="handleNodeConnectorClick(layer.path)"
|
||||
></button>
|
||||
<div v-else class="node-connector-missing"></div>
|
||||
<div
|
||||
class="layer"
|
||||
:class="{ selected: layer.layer_data.selected }"
|
||||
:style="{ marginLeft: layerIndent(layer) }"
|
||||
@click.shift.exact.stop="handleShiftClick(layer)"
|
||||
@click.ctrl.exact.stop="handleControlClick(layer)"
|
||||
@click.alt.exact.stop="handleControlClick(layer)"
|
||||
|
|
@ -35,12 +43,14 @@
|
|||
>
|
||||
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
|
||||
<div class="layer-type-icon">
|
||||
<IconLabel :icon="'NodeTypePath'" title="Path" />
|
||||
<IconLabel v-if="layer.layer_type === LayerType.Folder" :icon="'NodeTypeFolder'" title="Folder" />
|
||||
<IconLabel v-else :icon="'NodeTypePath'" title="Path" />
|
||||
</div>
|
||||
<div class="layer-name">
|
||||
<span>{{ layer.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="glue" :style="{ marginLeft: layerIndent(layer) }"></div> -->
|
||||
</div>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
|
@ -72,42 +82,96 @@
|
|||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
margin: 0 8px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--color-5-dullgray);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
.selected {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
position: relative;
|
||||
|
||||
& + .layer-row {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.layer-thumbnail {
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
background: white;
|
||||
.layer-visibility {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
.node-connector {
|
||||
flex: 0 0 auto;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 2px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-data-raster);
|
||||
outline: none;
|
||||
border: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
border-style: solid;
|
||||
border-width: 0 3px 6px 3px;
|
||||
border-color: transparent transparent var(--color-2-mildblack) transparent;
|
||||
}
|
||||
|
||||
&.expanded::after {
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
border-width: 3px 0 3px 6px;
|
||||
border-color: transparent transparent transparent var(--color-2-mildblack);
|
||||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
margin: 0 8px;
|
||||
.node-connector-missing {
|
||||
width: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
background: var(--color-5-dullgray);
|
||||
margin-right: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
&.selected {
|
||||
background: var(--color-7-middlegray);
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
.layer-thumbnail {
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.glue {
|
||||
position: absolute;
|
||||
background: var(--color-data-raster);
|
||||
height: 6px;
|
||||
bottom: -4px;
|
||||
left: 44px;
|
||||
right: 16px;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +181,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, UpdateLayer, LayerPanelEntry } from "@/utilities/response-handler";
|
||||
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, CollapseFolder, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
|
||||
import { SeparatorType } from "@/components/widgets/widgets";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
|
@ -175,9 +239,15 @@ const blendModeEntries: SectionsOfMenuListEntries = [
|
|||
export default defineComponent({
|
||||
props: {},
|
||||
methods: {
|
||||
layerIndent(layer: LayerPanelEntry): string {
|
||||
return `${(layer.path.length - 1) * 16}px`;
|
||||
},
|
||||
async toggleLayerVisibility(path: BigUint64Array) {
|
||||
(await wasm).toggle_layer_visibility(path);
|
||||
},
|
||||
async handleNodeConnectorClick(path: BigUint64Array) {
|
||||
(await wasm).toggle_layer_expansion(path);
|
||||
},
|
||||
async setLayerBlendMode() {
|
||||
const blendMode = this.blendModeEntries.flat()[this.blendModeSelectedIndex].value as BlendMode;
|
||||
if (blendMode) {
|
||||
|
|
@ -308,16 +378,61 @@ export default defineComponent({
|
|||
if (expandData) {
|
||||
const responsePath = expandData.path;
|
||||
const responseLayers = expandData.children as Array<LayerPanelEntry>;
|
||||
if (responsePath.length > 0) console.error("Non root paths are currently not implemented");
|
||||
// TODO: @Keavon Refactor this function
|
||||
if (responseLayers.length === 0) return;
|
||||
|
||||
this.layers = responseLayers;
|
||||
const mergeIntoExisting = (elements: Array<LayerPanelEntry>, layers: Array<LayerPanelEntry>) => {
|
||||
let lastInsertion = layers.findIndex((layer: LayerPanelEntry) => {
|
||||
const pathLengthsEqual = elements[0].path.length - 1 === layer.path.length;
|
||||
return pathLengthsEqual && elements[0].path.slice(0, -1).every((layerId, i) => layerId === layer.path[i]);
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
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) => {
|
||||
console.log("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) => {
|
||||
const updateData = responseData as UpdateLayer;
|
||||
|
|
@ -348,6 +463,7 @@ export default defineComponent({
|
|||
opacity: 100,
|
||||
MenuDirection,
|
||||
SeparatorType,
|
||||
LayerType,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ import MouseHintRMBDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg"
|
|||
import MouseHintMMBDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg";
|
||||
|
||||
import NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg";
|
||||
import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg";
|
||||
|
||||
const icons = {
|
||||
LayoutSelectTool: { component: LayoutSelectTool, size: 24 },
|
||||
|
|
@ -192,6 +193,7 @@ const icons = {
|
|||
MouseHintRMBDrag: { component: MouseHintRMBDrag, size: 16 },
|
||||
MouseHintMMBDrag: { component: MouseHintMMBDrag, size: 16 },
|
||||
NodeTypePath: { component: NodeTypePath, size: 24 },
|
||||
NodeTypeFolder: { component: NodeTypeFolder, size: 24 },
|
||||
};
|
||||
|
||||
const components = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component]));
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ function newSetCanvasRotation(input: any): SetCanvasRotation {
|
|||
|
||||
function newPath(input: any): BigUint64Array {
|
||||
// eslint-disable-next-line
|
||||
const u32CombinedPairs = input.map((n: Array<bigint>) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1])));
|
||||
const u32CombinedPairs = input.map((n: Array<number>) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1])));
|
||||
return new BigUint64Array(u32CombinedPairs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ pub fn on_mouse_up(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Result<(),
|
|||
pub fn on_key_down(name: String, modifiers: u8) -> Result<(), JsValue> {
|
||||
let key = translate_key(&name);
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
log::trace!("key down {:?}, name: {}, modifiers: {:?}", key, name, mods);
|
||||
log::trace!("Key down {:?}, name: {}, modifiers: {:?}", key, name, mods);
|
||||
let ev = InputPreprocessorMessage::KeyDown(key, mods);
|
||||
dispatch(ev)
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ pub fn on_key_down(name: String, modifiers: u8) -> Result<(), JsValue> {
|
|||
pub fn on_key_up(name: String, modifiers: u8) -> Result<(), JsValue> {
|
||||
let key = translate_key(&name);
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
log::trace!("key up {:?}, name: {}, modifiers: {:?}", key, name, mods);
|
||||
log::trace!("Key up {:?}, name: {}, modifiers: {:?}", key, name, mods);
|
||||
let ev = InputPreprocessorMessage::KeyUp(key, mods);
|
||||
dispatch(ev)
|
||||
}
|
||||
|
|
@ -353,5 +353,5 @@ pub fn delete_layer(path: Vec<LayerId>) -> Result<(), JsValue> {
|
|||
/// Requests the backend to add a layer to the layer list
|
||||
#[wasm_bindgen]
|
||||
pub fn add_folder(path: Vec<LayerId>) -> Result<(), JsValue> {
|
||||
dispatch(DocumentMessage::AddFolder(path))
|
||||
dispatch(DocumentMessage::CreateFolder(path))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ pub fn translate_append_mode(name: &str) -> Option<SelectAppendMode> {
|
|||
}
|
||||
|
||||
pub fn translate_key(name: &str) -> Key {
|
||||
log::trace!("pressed key: {}", name);
|
||||
log::trace!("Key event received: {}", name);
|
||||
use Key::*;
|
||||
match name.to_lowercase().as_str() {
|
||||
"a" => KeyA,
|
||||
|
|
|
|||
|
|
@ -27,11 +27,6 @@ impl Default for Document {
|
|||
}
|
||||
}
|
||||
|
||||
fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> {
|
||||
let (id, path) = path.split_last().ok_or(DocumentError::InvalidPath)?;
|
||||
Ok((path, *id))
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn with_content(serialized_content: &str) -> Result<Self, DocumentError> {
|
||||
serde_json::from_str(serialized_content).map_err(|e| DocumentError::InvalidFile(e.to_string()))
|
||||
|
|
@ -78,7 +73,7 @@ impl Document {
|
|||
/// Returns a mutable reference to the requested folder. Fails if the path does not exist,
|
||||
/// or if the requested layer is not of type folder.
|
||||
/// If you manually edit the folder you have to set the cache_dirty flag yourself.
|
||||
pub fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Folder, DocumentError> {
|
||||
fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Folder, DocumentError> {
|
||||
let mut root = &mut self.root;
|
||||
for id in path {
|
||||
root = root.as_folder_mut()?.layer_mut(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
|
|
@ -96,7 +91,7 @@ impl Document {
|
|||
}
|
||||
|
||||
/// Returns a mutable reference to the layer or folder at the path.
|
||||
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
if path.is_empty() {
|
||||
return Ok(&mut self.root);
|
||||
}
|
||||
|
|
@ -104,6 +99,24 @@ impl Document {
|
|||
self.folder_mut(path)?.layer_mut(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
pub fn deepest_common_folder<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> Result<&'a [LayerId], DocumentError> {
|
||||
let common_prefix_of_path = self.common_prefix(layers);
|
||||
|
||||
Ok(match self.layer(common_prefix_of_path)?.data {
|
||||
LayerDataType::Folder(_) => common_prefix_of_path,
|
||||
LayerDataType::Shape(_) => &common_prefix_of_path[..common_prefix_of_path.len() - 1],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn common_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
|
||||
layers
|
||||
.reduce(|a, b| {
|
||||
let number_of_uncommon_ids_in_a = (0..a.len()).position(|i| b.starts_with(&a[..a.len() - i])).unwrap_or_default();
|
||||
&a[..(a.len() - number_of_uncommon_ids_in_a)]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
||||
|
|
@ -297,6 +310,7 @@ impl Document {
|
|||
/// reaction from the frontend, responses may be returned.
|
||||
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
operation.pseudo_hash().hash(&mut self.hasher);
|
||||
use DocumentResponse::*;
|
||||
|
||||
let responses = match &operation {
|
||||
Operation::AddEllipse { path, insert_index, transform, style } => {
|
||||
|
|
@ -304,7 +318,7 @@ impl Document {
|
|||
|
||||
self.set_layer(path, layer, *insert_index)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddOverlayEllipse { path, transform, style } => {
|
||||
let mut ellipse = Shape::ellipse(*style);
|
||||
|
|
@ -315,14 +329,14 @@ impl Document {
|
|||
|
||||
self.set_layer(path, layer, -1)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||
}
|
||||
Operation::AddRect { path, insert_index, transform, style } => {
|
||||
let layer = Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform);
|
||||
|
||||
self.set_layer(path, layer, *insert_index)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddOverlayRect { path, transform, style } => {
|
||||
let mut rect = Shape::rectangle(*style);
|
||||
|
|
@ -333,14 +347,14 @@ impl Document {
|
|||
|
||||
self.set_layer(path, layer, -1)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||
}
|
||||
Operation::AddLine { path, insert_index, transform, style } => {
|
||||
let layer = Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform);
|
||||
|
||||
self.set_layer(path, layer, *insert_index)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddOverlayLine { path, transform, style } => {
|
||||
let mut line = Shape::line(*style);
|
||||
|
|
@ -351,7 +365,7 @@ impl Document {
|
|||
|
||||
self.set_layer(path, layer, -1)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||
}
|
||||
Operation::AddNgon {
|
||||
path,
|
||||
|
|
@ -360,9 +374,11 @@ impl Document {
|
|||
style,
|
||||
sides,
|
||||
} => {
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ngon(*sides, *style)), *transform), *insert_index)?;
|
||||
let layer = Layer::new(LayerDataType::Shape(Shape::ngon(*sides, *style)), *transform);
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
self.set_layer(path, layer, *insert_index)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::AddOverlayShape { path, style, bez_path } => {
|
||||
let mut shape = Shape::from_bez_path(bez_path.clone(), *style, false);
|
||||
|
|
@ -373,7 +389,7 @@ impl Document {
|
|||
|
||||
self.set_layer(path, layer, -1)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||
}
|
||||
Operation::AddPen {
|
||||
path,
|
||||
|
|
@ -384,17 +400,15 @@ impl Document {
|
|||
} => {
|
||||
let points: Vec<glam::DVec2> = points.iter().map(|&it| it.into()).collect();
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::poly_line(points, *style)), *transform), *insert_index)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::DeleteLayer { path } => {
|
||||
self.delete(path)?;
|
||||
|
||||
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||
Some(vec![
|
||||
DocumentResponse::DocumentChanged,
|
||||
DocumentResponse::DeletedLayer { path: path.clone() },
|
||||
DocumentResponse::FolderChanged { path: folder.to_vec() },
|
||||
])
|
||||
let mut responses = vec![DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }];
|
||||
responses.extend(update_thumbnails_upstream(folder));
|
||||
Some(responses)
|
||||
}
|
||||
Operation::PasteLayer { path, layer, insert_index } => {
|
||||
let folder = self.folder_mut(path)?;
|
||||
|
|
@ -402,11 +416,9 @@ impl Document {
|
|||
let full_path = [path.clone(), vec![id]].concat();
|
||||
self.mark_as_dirty(&full_path)?;
|
||||
|
||||
Some(vec![
|
||||
DocumentResponse::DocumentChanged,
|
||||
DocumentResponse::CreatedLayer { path: full_path },
|
||||
DocumentResponse::FolderChanged { path: path.clone() },
|
||||
])
|
||||
let mut responses = vec![DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }];
|
||||
responses.extend(update_thumbnails_upstream(path));
|
||||
Some(responses)
|
||||
}
|
||||
Operation::DuplicateLayer { path } => {
|
||||
let layer = self.layer(path)?.clone();
|
||||
|
|
@ -414,35 +426,36 @@ impl Document {
|
|||
let folder = self.folder_mut(folder_path)?;
|
||||
folder.add_layer(layer, None, -1).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
self.mark_as_dirty(&path[..path.len() - 1])?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: folder_path.to_vec() }])
|
||||
Some(vec![DocumentChanged, FolderChanged { path: folder_path.to_vec() }])
|
||||
}
|
||||
Operation::RenameLayer { path, name } => {
|
||||
self.layer_mut(path)?.name = Some(name.clone());
|
||||
Some(vec![DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some(vec![LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::AddFolder { path } => {
|
||||
Operation::CreateFolder { path } => {
|
||||
self.set_layer(path, Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()), -1)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
|
||||
Some(vec![DocumentChanged, CreatedLayer { path: path.clone() }])
|
||||
}
|
||||
Operation::TransformLayer { path, transform } => {
|
||||
let layer = self.layer_mut(path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(transform) * layer.transform;
|
||||
layer.transform = transform;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentChanged])
|
||||
}
|
||||
Operation::TransformLayerInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
self.apply_transform_relative_to_viewport(path, transform)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransformInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
self.set_transform_relative_to_viewport(path, transform)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetShapePathInViewport { path, bez_path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
|
|
@ -455,52 +468,52 @@ impl Document {
|
|||
}
|
||||
LayerDataType::Folder(_) => (),
|
||||
}
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::TransformLayerInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
let scope = DAffine2::from_cols_array(scope);
|
||||
self.transform_relative_to_scope(path, Some(scope), transform)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransformInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
let scope = DAffine2::from_cols_array(scope);
|
||||
self.set_transform_relative_to_scope(path, Some(scope), transform)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransform { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
let layer = self.layer_mut(path)?;
|
||||
layer.transform = transform;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::ToggleLayerVisibility { path } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
let layer = self.layer_mut(path)?;
|
||||
layer.visible = !layer.visible;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerVisibility { path, visible } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
let layer = self.layer_mut(path)?;
|
||||
layer.visible = *visible;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerBlendMode { path, blend_mode } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
self.layer_mut(path)?.blend_mode = *blend_mode;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerOpacity { path, opacity } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
self.layer_mut(path)?.opacity = *opacity;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerStyle { path, style } => {
|
||||
let layer = self.layer_mut(path)?;
|
||||
|
|
@ -509,7 +522,7 @@ impl Document {
|
|||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some(vec![DocumentChanged, LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::SetLayerFill { path, color } => {
|
||||
let layer = self.layer_mut(path)?;
|
||||
|
|
@ -518,9 +531,23 @@ impl Document {
|
|||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
};
|
||||
Ok(responses)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> {
|
||||
let (id, path) = path.split_last().ok_or(DocumentError::InvalidPath)?;
|
||||
Ok((path, *id))
|
||||
}
|
||||
|
||||
fn update_thumbnails_upstream(path: &[LayerId]) -> Vec<DocumentResponse> {
|
||||
let length = path.len();
|
||||
let mut responses = Vec::with_capacity(length);
|
||||
for i in 0..length {
|
||||
responses.push(DocumentResponse::LayerChanged { path: path[0..(length - i)].to_vec() });
|
||||
}
|
||||
responses
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
AddFolder {
|
||||
CreateFolder {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
TransformLayer {
|
||||
|
|
|
|||
Loading…
Reference in New Issue