mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 17:08:11 +08:00
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.
Example document:
```json
{
"nodes": {},
"root": {
"blend_mode": "Normal",
"cache": "...",
"cache_dirty": false,
"data": {
"Folder": {
"layer_ids": [
3902938778642561358
],
"layers": [
{
"blend_mode": "Normal",
"cache": "...",
"cache_dirty": false,
"data": {
"Shape": {
"path": [
{
"MoveTo": {
"x": 0.0,
"y": 0.0
}
},
{
"LineTo": {
"x": 1.0,
"y": 0.0
}
},
{
"LineTo": {
"x": 1.0,
"y": 1.0
}
},
{
"LineTo": {
"x": 0.0,
"y": 1.0
}
},
"ClosePath"
],
"render_index": 1,
"solid": true,
"style": {
"fill": {
"color": {
"alpha": 1.0,
"blue": 0.0,
"green": 0.0,
"red": 0.0
}
},
"stroke": null
}
}
},
"name": null,
"opacity": 1.0,
"thumbnail_cache": "...",
"transform": {
"matrix2": [
223.0,
0.0,
-0.0,
348.0
],
"translation": [
-188.0,
-334.0
]
},
"visible": true
}
],
"next_assignment_id": 3902938778642561359
}
},
"name": null,
"opacity": 1.0,
"thumbnail_cache": "...",
"transform": {
"matrix2": [
1.0,
0.0,
0.0,
1.0
],
"translation": [
479.0,
563.0
]
},
"visible": true
},
"version": 0
}
```
* 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>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Dennis Kobert
Keavon Chambers
parent
8551121793
commit
d4674e5856
Generated
+1
@@ -110,6 +110,7 @@ dependencies = [
|
|||||||
"kurbo",
|
"kurbo",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
|||||||
|
|
||||||
// SELECT TOOL
|
// SELECT TOOL
|
||||||
pub const SELECTION_TOLERANCE: f64 = 1.0;
|
pub const SELECTION_TOLERANCE: f64 = 1.0;
|
||||||
|
|
||||||
|
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||||
|
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||||
|
pub const FILE_EXPORT_SUFFIX: &str = ".svg";
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
pub use super::layer_panel::*;
|
pub use super::layer_panel::*;
|
||||||
use crate::{frontend::layer_panel::*, EditorError};
|
use crate::{
|
||||||
|
consts::{FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX},
|
||||||
|
frontend::layer_panel::*,
|
||||||
|
EditorError,
|
||||||
|
};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene::{document::Document as InternalDocument, LayerId};
|
use graphene::{document::Document as InternalDocument, DocumentError, LayerId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -81,6 +85,7 @@ pub enum DocumentMessage {
|
|||||||
AbortTransaction,
|
AbortTransaction,
|
||||||
CommitTransaction,
|
CommitTransaction,
|
||||||
ExportDocument,
|
ExportDocument,
|
||||||
|
SaveDocument,
|
||||||
RenderDocument,
|
RenderDocument,
|
||||||
Undo,
|
Undo,
|
||||||
NudgeSelectedLayers(f64, f64),
|
NudgeSelectedLayers(f64, f64),
|
||||||
@@ -115,7 +120,7 @@ impl DocumentMessageHandler {
|
|||||||
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
|
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
|
||||||
document_responses.len() != len
|
document_responses.len() != len
|
||||||
}
|
}
|
||||||
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
|
pub fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
|
||||||
let _ = self.document.render_root();
|
let _ = self.document.render_root();
|
||||||
self.layer_data(&path).expanded.then(|| {
|
self.layer_data(&path).expanded.then(|| {
|
||||||
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
|
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
|
||||||
@@ -192,6 +197,18 @@ impl DocumentMessageHandler {
|
|||||||
movement_handler: MovementMessageHandler::default(),
|
movement_handler: MovementMessageHandler::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> {
|
||||||
|
let mut document = Self::with_name(name);
|
||||||
|
let internal_document = InternalDocument::with_content(&serialized_content);
|
||||||
|
match internal_document {
|
||||||
|
Ok(handle) => {
|
||||||
|
document.document = handle;
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
|
Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)),
|
||||||
|
_ => Err(EditorError::Document(String::from("Failed to open file"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn layer_data(&mut self, path: &[LayerId]) -> &mut LayerData {
|
pub fn layer_data(&mut self, path: &[LayerId]) -> &mut LayerData {
|
||||||
layer_data(&mut self.layer_data, path)
|
layer_data(&mut self.layer_data, path)
|
||||||
@@ -269,6 +286,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||||||
ExportDocument => {
|
ExportDocument => {
|
||||||
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_size.as_f64()]);
|
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_size.as_f64()]);
|
||||||
let size = bbox[1] - bbox[0];
|
let size = bbox[1] - bbox[0];
|
||||||
|
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||||
|
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
|
||||||
|
false => self.name.clone() + FILE_EXPORT_SUFFIX,
|
||||||
|
};
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
FrontendMessage::ExportDocument {
|
FrontendMessage::ExportDocument {
|
||||||
document: format!(
|
document: format!(
|
||||||
@@ -280,6 +301,20 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||||||
"\n",
|
"\n",
|
||||||
self.document.render_root()
|
self.document.render_root()
|
||||||
),
|
),
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SaveDocument => {
|
||||||
|
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||||
|
true => self.name.clone(),
|
||||||
|
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
||||||
|
};
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::SaveDocument {
|
||||||
|
document: self.document.serialize_document(),
|
||||||
|
name,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
@@ -484,6 +519,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||||||
DeselectAllLayers,
|
DeselectAllLayers,
|
||||||
RenderDocument,
|
RenderDocument,
|
||||||
ExportDocument,
|
ExportDocument,
|
||||||
|
SaveDocument,
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.layer_data.values().any(|data| data.selected) {
|
if self.layer_data.values().any(|data| data.selected) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use log::warn;
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use super::DocumentMessageHandler;
|
use super::DocumentMessageHandler;
|
||||||
|
use crate::consts::DEFAULT_DOCUMENT_NAME;
|
||||||
|
|
||||||
#[impl_message(Message, Documents)]
|
#[impl_message(Message, Documents)]
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
@@ -24,6 +25,8 @@ pub enum DocumentsMessage {
|
|||||||
CloseAllDocumentsWithConfirmation,
|
CloseAllDocumentsWithConfirmation,
|
||||||
CloseAllDocuments,
|
CloseAllDocuments,
|
||||||
NewDocument,
|
NewDocument,
|
||||||
|
OpenDocument,
|
||||||
|
OpenDocumentFile(String, String),
|
||||||
GetOpenDocumentsList,
|
GetOpenDocumentsList,
|
||||||
NextDocument,
|
NextDocument,
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
@@ -43,6 +46,45 @@ impl DocumentsMessageHandler {
|
|||||||
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
||||||
&mut self.documents[self.active_document_index]
|
&mut self.documents[self.active_document_index]
|
||||||
}
|
}
|
||||||
|
fn generate_new_document_name(&self) -> String {
|
||||||
|
let mut doc_title_numbers = self
|
||||||
|
.documents
|
||||||
|
.iter()
|
||||||
|
.filter_map(|d| {
|
||||||
|
d.name
|
||||||
|
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
||||||
|
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
|
||||||
|
})
|
||||||
|
.collect::<Vec<isize>>();
|
||||||
|
doc_title_numbers.sort_unstable();
|
||||||
|
doc_title_numbers.iter_mut().enumerate().for_each(|(i, number)| *number = *number - i as isize - 2);
|
||||||
|
// Uses binary search to find the index of the element where number is bigger than i
|
||||||
|
let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1;
|
||||||
|
|
||||||
|
let name = match new_doc_title_num {
|
||||||
|
1 => DEFAULT_DOCUMENT_NAME.to_string(),
|
||||||
|
_ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num),
|
||||||
|
};
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
self.active_document_index = self.documents.len();
|
||||||
|
self.documents.push(new_document);
|
||||||
|
|
||||||
|
// Send the new list of document tab names
|
||||||
|
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
|
||||||
|
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::ExpandFolder {
|
||||||
|
path: Vec::new(),
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DocumentsMessageHandler {
|
impl Default for DocumentsMessageHandler {
|
||||||
@@ -71,6 +113,7 @@ 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![]));
|
||||||
}
|
}
|
||||||
CloseActiveDocumentWithConfirmation => {
|
CloseActiveDocumentWithConfirmation => {
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
@@ -138,48 +181,21 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
NewDocument => {
|
NewDocument => {
|
||||||
let digits = ('0'..='9').collect::<Vec<char>>();
|
let name = self.generate_new_document_name();
|
||||||
let mut doc_title_numbers = self
|
|
||||||
.documents
|
|
||||||
.iter()
|
|
||||||
.map(|d| {
|
|
||||||
if d.name.ends_with(digits.as_slice()) {
|
|
||||||
let (_, number) = d.name.split_at(17);
|
|
||||||
number.trim().parse::<usize>().unwrap()
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<usize>>();
|
|
||||||
doc_title_numbers.sort_unstable();
|
|
||||||
let mut new_doc_title_num = 1;
|
|
||||||
while new_doc_title_num <= self.documents.len() {
|
|
||||||
if new_doc_title_num != doc_title_numbers[new_doc_title_num - 1] {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
new_doc_title_num += 1;
|
|
||||||
}
|
|
||||||
let name = match new_doc_title_num {
|
|
||||||
1 => "Untitled Document".to_string(),
|
|
||||||
_ => format!("Untitled Document {}", new_doc_title_num),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.active_document_index = self.documents.len();
|
|
||||||
let new_document = DocumentMessageHandler::with_name(name);
|
let new_document = DocumentMessageHandler::with_name(name);
|
||||||
self.documents.push(new_document);
|
self.load_document(new_document, responses);
|
||||||
|
}
|
||||||
// Send the new list of document tab names
|
OpenDocument => {
|
||||||
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
|
responses.push_back(FrontendMessage::OpenDocumentBrowse.into());
|
||||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
}
|
||||||
|
OpenDocumentFile(name, serialized_contents) => {
|
||||||
responses.push_back(
|
let document = DocumentMessageHandler::with_name_and_content(name, serialized_contents);
|
||||||
FrontendMessage::ExpandFolder {
|
match document {
|
||||||
path: Vec::new(),
|
Ok(document) => {
|
||||||
children: Vec::new(),
|
self.load_document(document, responses);
|
||||||
}
|
}
|
||||||
.into(),
|
Err(e) => responses.push_back(FrontendMessage::DisplayError { description: e.to_string() }.into()),
|
||||||
);
|
}
|
||||||
responses.push_back(SelectDocument(self.active_document_index).into());
|
|
||||||
}
|
}
|
||||||
GetOpenDocumentsList => {
|
GetOpenDocumentsList => {
|
||||||
// Send the list of document tab names
|
// Send the list of document tab names
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ pub enum FrontendMessage {
|
|||||||
DisplayConfirmationToCloseAllDocuments,
|
DisplayConfirmationToCloseAllDocuments,
|
||||||
UpdateCanvas { document: String },
|
UpdateCanvas { document: String },
|
||||||
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
|
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
|
||||||
ExportDocument { document: String },
|
ExportDocument { document: String, name: String },
|
||||||
|
SaveDocument { document: String, name: String },
|
||||||
|
OpenDocumentBrowse,
|
||||||
EnableTextInput,
|
EnableTextInput,
|
||||||
DisableTextInput,
|
DisableTextInput,
|
||||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||||
@@ -52,5 +54,6 @@ impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
|
|||||||
DisableTextInput,
|
DisableTextInput,
|
||||||
SetCanvasZoom,
|
SetCanvasZoom,
|
||||||
SetCanvasRotation,
|
SetCanvasRotation,
|
||||||
|
OpenDocumentBrowse,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ impl Default for Mapping {
|
|||||||
entry! {action=ToolMessage::SelectTool(ToolType::Eyedropper), key_down=KeyI},
|
entry! {action=ToolMessage::SelectTool(ToolType::Eyedropper), key_down=KeyI},
|
||||||
entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]},
|
entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]},
|
||||||
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]},
|
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]},
|
||||||
|
// Editor Actions
|
||||||
|
entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]},
|
||||||
// Document Actions
|
// Document Actions
|
||||||
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
|
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
|
||||||
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
|
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
|
||||||
@@ -188,6 +190,8 @@ impl Default for Mapping {
|
|||||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
||||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||||
|
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl]},
|
||||||
|
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
||||||
entry! {action=MovementMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
entry! {action=MovementMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||||
entry! {action=MovementMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
|
entry! {action=MovementMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
|
||||||
entry! {action=MovementMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
|
entry! {action=MovementMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
|
||||||
|
|||||||
@@ -5,18 +5,23 @@ use thiserror::Error;
|
|||||||
/// The error type used by the Graphite editor.
|
/// The error type used by the Graphite editor.
|
||||||
#[derive(Clone, Debug, Error)]
|
#[derive(Clone, Debug, Error)]
|
||||||
pub enum EditorError {
|
pub enum EditorError {
|
||||||
#[error("Failed to execute operation: {0}")]
|
#[error("Failed to execute operation:\n{0}")]
|
||||||
InvalidOperation(String),
|
InvalidOperation(String),
|
||||||
#[error("{0}")]
|
|
||||||
Misc(String),
|
#[error("Tried to construct an invalid color:\n{0:?}")]
|
||||||
#[error("Tried to construct an invalid color {0:?}")]
|
|
||||||
Color(String),
|
Color(String),
|
||||||
|
|
||||||
#[error("The requested tool does not exist")]
|
#[error("The requested tool does not exist")]
|
||||||
UnknownTool,
|
UnknownTool,
|
||||||
#[error("The operation caused a document error {0:?}")]
|
|
||||||
|
#[error("The operation caused a document error:\n{0:?}")]
|
||||||
Document(String),
|
Document(String),
|
||||||
#[error("A Rollback was initated but no transaction was in progress")]
|
|
||||||
|
#[error("A rollback was initiated but no transaction was in progress")]
|
||||||
NoTransactionInProgress,
|
NoTransactionInProgress,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Misc(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! derive_from {
|
macro_rules! derive_from {
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import { makeModifiersBitfield } from "@/utilities/input";
|
import { makeModifiersBitfield } from "@/utilities/input";
|
||||||
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
|
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
|
||||||
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
|
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
|
||||||
import { comingSoon } from "@/utilities/errors";
|
import { comingSoon } from "@/utilities/errors";
|
||||||
|
|
||||||
@@ -300,37 +300,25 @@ export default defineComponent({
|
|||||||
async resetWorkingColors() {
|
async resetWorkingColors() {
|
||||||
(await wasm).reset_colors();
|
(await wasm).reset_colors();
|
||||||
},
|
},
|
||||||
download(filename: string, fileData: string) {
|
|
||||||
const svgBlob = new Blob([fileData], { type: "image/svg+xml;charset=utf-8" });
|
|
||||||
const svgUrl = URL.createObjectURL(svgBlob);
|
|
||||||
const element = document.createElement("a");
|
|
||||||
|
|
||||||
element.href = svgUrl;
|
|
||||||
element.setAttribute("download", filename);
|
|
||||||
element.style.display = "none";
|
|
||||||
|
|
||||||
element.click();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
|
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
|
||||||
const updateData = responseData as UpdateCanvas;
|
const updateData = responseData as UpdateCanvas;
|
||||||
if (updateData) this.viewportSvg = updateData.document;
|
if (updateData) this.viewportSvg = updateData.document;
|
||||||
});
|
});
|
||||||
registerResponseHandler(ResponseType.ExportDocument, (responseData: Response) => {
|
|
||||||
const updateData = responseData as ExportDocument;
|
|
||||||
if (updateData) this.download("canvas.svg", updateData.document);
|
|
||||||
});
|
|
||||||
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
|
||||||
const toolData = responseData as SetActiveTool;
|
const toolData = responseData as SetActiveTool;
|
||||||
if (toolData) this.activeTool = toolData.tool_name;
|
if (toolData) this.activeTool = toolData.tool_name;
|
||||||
});
|
});
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.SetCanvasZoom, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetCanvasZoom, (responseData: Response) => {
|
||||||
const updateData = responseData as SetCanvasZoom;
|
const updateData = responseData as SetCanvasZoom;
|
||||||
if (updateData) {
|
if (updateData) {
|
||||||
this.documentZoom = updateData.new_zoom * 100;
|
this.documentZoom = updateData.new_zoom * 100;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.SetCanvasRotation, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetCanvasRotation, (responseData: Response) => {
|
||||||
const updateData = responseData as SetCanvasRotation;
|
const updateData = responseData as SetCanvasRotation;
|
||||||
if (updateData) {
|
if (updateData) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const menuEntries: MenuListEntries = [
|
|||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => (await wasm).new_document() },
|
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => (await wasm).new_document() },
|
||||||
{ label: "Open…", shortcut: ["Ctrl", "O"] },
|
{ label: "Open…", shortcut: ["Ctrl", "O"], action: async () => (await wasm).open_document() },
|
||||||
{
|
{
|
||||||
label: "Open Recent",
|
label: "Open Recent",
|
||||||
shortcut: ["Ctrl", "⇧", "O"],
|
shortcut: ["Ctrl", "⇧", "O"],
|
||||||
@@ -90,8 +90,8 @@ const menuEntries: MenuListEntries = [
|
|||||||
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => (await wasm).close_all_documents_with_confirmation() },
|
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => (await wasm).close_all_documents_with_confirmation() },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ label: "Save", shortcut: ["Ctrl", "S"] },
|
{ label: "Save", shortcut: ["Ctrl", "S"], action: async () => (await wasm).save_document() },
|
||||||
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"] },
|
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"], action: async () => (await wasm).save_document() },
|
||||||
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
|
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
|
||||||
{ label: "Auto-Save", checkbox: true, checked: true },
|
{ label: "Auto-Save", checkbox: true, checked: true },
|
||||||
],
|
],
|
||||||
@@ -128,10 +128,10 @@ const menuEntries: MenuListEntries = [
|
|||||||
label: "Order",
|
label: "Order",
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "Raise To Front", shortcut: ["Ctrl", "Shift", "]"], action: async () => (await wasm).reorder_selected_layers(2147483647) },
|
{ label: "Raise To Front", shortcut: ["Ctrl", "Shift", "]"], action: async () => (await wasm).reorder_selected_layers((await wasm).i32_max()) },
|
||||||
{ label: "Raise", shortcut: ["Ctrl", "]"], action: async () => (await wasm).reorder_selected_layers(1) },
|
{ label: "Raise", shortcut: ["Ctrl", "]"], action: async () => (await wasm).reorder_selected_layers(1) },
|
||||||
{ label: "Lower", shortcut: ["Ctrl", "["], action: async () => (await wasm).reorder_selected_layers(-1) },
|
{ label: "Lower", shortcut: ["Ctrl", "["], action: async () => (await wasm).reorder_selected_layers(-1) },
|
||||||
{ label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => (await wasm).reorder_selected_layers(-2147483648) },
|
{ label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => (await wasm).reorder_selected_layers((await wasm).i32_min()) },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { reactive, readonly } from "vue";
|
import { reactive, readonly } from "vue";
|
||||||
|
|
||||||
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
||||||
import { ResponseType, registerResponseHandler, Response, SetActiveDocument, UpdateOpenDocumentsList, DisplayConfirmationToCloseDocument } from "@/utilities/response-handler";
|
import {
|
||||||
|
ResponseType,
|
||||||
|
registerResponseHandler,
|
||||||
|
Response,
|
||||||
|
SetActiveDocument,
|
||||||
|
UpdateOpenDocumentsList,
|
||||||
|
DisplayConfirmationToCloseDocument,
|
||||||
|
ExportDocument,
|
||||||
|
SaveDocument,
|
||||||
|
} from "@/utilities/response-handler";
|
||||||
|
import { download, upload } from "./files";
|
||||||
|
|
||||||
const wasm = import("@/../wasm/pkg");
|
const wasm = import("@/../wasm/pkg");
|
||||||
|
|
||||||
@@ -21,15 +31,14 @@ export async function closeDocumentWithConfirmation(tabIndex: number) {
|
|||||||
|
|
||||||
const tabLabel = state.documents[tabIndex];
|
const tabLabel = state.documents[tabIndex];
|
||||||
|
|
||||||
// TODO: Rename to "Save changes before closing?" when we can actually save documents somewhere, not just export SVGs
|
createDialog("File", "Save changes before closing?", tabLabel, [
|
||||||
createDialog("File", "Close without exporting SVG?", tabLabel, [
|
|
||||||
{
|
{
|
||||||
kind: "TextButton",
|
kind: "TextButton",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
(await wasm).export_document();
|
(await wasm).save_document();
|
||||||
dismissDialog();
|
dismissDialog();
|
||||||
},
|
},
|
||||||
props: { label: "Export", emphasized: true, minWidth: 96 },
|
props: { label: "Save", emphasized: true, minWidth: 96 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "TextButton",
|
kind: "TextButton",
|
||||||
@@ -78,6 +87,7 @@ registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Res
|
|||||||
state.title = state.documents[state.activeDocumentIndex];
|
state.title = state.documents[state.activeDocumentIndex];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
||||||
const documentData = responseData as SetActiveDocument;
|
const documentData = responseData as SetActiveDocument;
|
||||||
if (documentData) {
|
if (documentData) {
|
||||||
@@ -85,12 +95,30 @@ registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response)
|
|||||||
state.title = state.documents[state.activeDocumentIndex];
|
state.title = state.documents[state.activeDocumentIndex];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registerResponseHandler(ResponseType.DisplayConfirmationToCloseDocument, (responseData: Response) => {
|
registerResponseHandler(ResponseType.DisplayConfirmationToCloseDocument, (responseData: Response) => {
|
||||||
const data = responseData as DisplayConfirmationToCloseDocument;
|
const data = responseData as DisplayConfirmationToCloseDocument;
|
||||||
closeDocumentWithConfirmation(data.document_index);
|
closeDocumentWithConfirmation(data.document_index);
|
||||||
});
|
});
|
||||||
registerResponseHandler(ResponseType.DisplayConfirmationToCloseAllDocuments, (_responseData: Response) => {
|
|
||||||
|
registerResponseHandler(ResponseType.DisplayConfirmationToCloseAllDocuments, (_: Response) => {
|
||||||
closeAllDocumentsWithConfirmation();
|
closeAllDocumentsWithConfirmation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerResponseHandler(ResponseType.OpenDocumentBrowse, async (_: Response) => {
|
||||||
|
const extension = (await wasm).file_save_suffix();
|
||||||
|
const data = await upload(extension);
|
||||||
|
(await wasm).open_document_file(data.filename, data.content);
|
||||||
|
});
|
||||||
|
|
||||||
|
registerResponseHandler(ResponseType.ExportDocument, (responseData: Response) => {
|
||||||
|
const updateData = responseData as ExportDocument;
|
||||||
|
if (updateData) download(updateData.name, updateData.document);
|
||||||
|
});
|
||||||
|
|
||||||
|
registerResponseHandler(ResponseType.SaveDocument, (responseData: Response) => {
|
||||||
|
const saveData = responseData as SaveDocument;
|
||||||
|
if (saveData) download(saveData.name, saveData.document);
|
||||||
|
});
|
||||||
|
|
||||||
(async () => (await wasm).get_open_documents_list())();
|
(async () => (await wasm).get_open_documents_list())();
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export function download(filename: string, fileData: string) {
|
||||||
|
let type = "text/plain;charset=utf-8";
|
||||||
|
if (filename.endsWith(".svg")) {
|
||||||
|
type = "image/svg+xml;charset=utf-8";
|
||||||
|
}
|
||||||
|
const blob = new Blob([fileData], { type });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const element = document.createElement("a");
|
||||||
|
|
||||||
|
element.href = url;
|
||||||
|
element.setAttribute("download", filename);
|
||||||
|
element.style.display = "none";
|
||||||
|
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upload(acceptedEextensions: string) {
|
||||||
|
return new Promise<{ filename: string; content: string }>((resolve, _) => {
|
||||||
|
const element = document.createElement("input");
|
||||||
|
element.type = "file";
|
||||||
|
element.style.display = "none";
|
||||||
|
element.accept = acceptedEextensions;
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
"change",
|
||||||
|
async () => {
|
||||||
|
if (!element.files || !element.files.length) return;
|
||||||
|
const file = element.files[0];
|
||||||
|
const filename = file.name;
|
||||||
|
const content = await file.text();
|
||||||
|
|
||||||
|
resolve({ filename, content });
|
||||||
|
},
|
||||||
|
{ capture: false, once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
element.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ const state = reactive({
|
|||||||
export enum ResponseType {
|
export enum ResponseType {
|
||||||
UpdateCanvas = "UpdateCanvas",
|
UpdateCanvas = "UpdateCanvas",
|
||||||
ExportDocument = "ExportDocument",
|
ExportDocument = "ExportDocument",
|
||||||
|
SaveDocument = "SaveDocument",
|
||||||
|
OpenDocumentBrowse = "OpenDocumentBrowse",
|
||||||
ExpandFolder = "ExpandFolder",
|
ExpandFolder = "ExpandFolder",
|
||||||
CollapseFolder = "CollapseFolder",
|
CollapseFolder = "CollapseFolder",
|
||||||
UpdateLayer = "UpdateLayer",
|
UpdateLayer = "UpdateLayer",
|
||||||
@@ -72,6 +74,10 @@ function parseResponse(responseType: string, data: any): Response {
|
|||||||
return newSetCanvasRotation(data.SetCanvasRotation);
|
return newSetCanvasRotation(data.SetCanvasRotation);
|
||||||
case "ExportDocument":
|
case "ExportDocument":
|
||||||
return newExportDocument(data.ExportDocument);
|
return newExportDocument(data.ExportDocument);
|
||||||
|
case "SaveDocument":
|
||||||
|
return newSaveDocument(data.SaveDocument);
|
||||||
|
case "OpenDocumentBrowse":
|
||||||
|
return newOpenDocumentBrowse(data.OpenDocumentBrowse);
|
||||||
case "UpdateWorkingColors":
|
case "UpdateWorkingColors":
|
||||||
return newUpdateWorkingColors(data.UpdateWorkingColors);
|
return newUpdateWorkingColors(data.UpdateWorkingColors);
|
||||||
case "DisplayError":
|
case "DisplayError":
|
||||||
@@ -167,13 +173,31 @@ function newUpdateCanvas(input: any): UpdateCanvas {
|
|||||||
|
|
||||||
export interface ExportDocument {
|
export interface ExportDocument {
|
||||||
document: string;
|
document: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
function newExportDocument(input: any): UpdateCanvas {
|
function newExportDocument(input: any): ExportDocument {
|
||||||
return {
|
return {
|
||||||
document: input.document,
|
document: input.document,
|
||||||
|
name: input.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaveDocument {
|
||||||
|
document: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
function newSaveDocument(input: any): SaveDocument {
|
||||||
|
return {
|
||||||
|
document: input.document,
|
||||||
|
name: input.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenDocumentBrowse = {};
|
||||||
|
function newOpenDocumentBrowse(_: any): OpenDocumentBrowse {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
export type DocumentChanged = {};
|
export type DocumentChanged = {};
|
||||||
function newDocumentChanged(_: any): DocumentChanged {
|
function newDocumentChanged(_: any): DocumentChanged {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ pub fn new_document() -> Result<(), JsValue> {
|
|||||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::NewDocument).map_err(convert_error))
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::NewDocument).map_err(convert_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn open_document() -> Result<(), JsValue> {
|
||||||
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::OpenDocument).map_err(convert_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn open_document_file(name: String, content: String) -> Result<(), JsValue> {
|
||||||
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::OpenDocumentFile(name, content)).map_err(convert_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn save_document() -> Result<(), JsValue> {
|
||||||
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SaveDocument)).map_err(convert_error)
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn close_document(document: usize) -> Result<(), JsValue> {
|
pub fn close_document(document: usize) -> Result<(), JsValue> {
|
||||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseDocument(document)).map_err(convert_error))
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseDocument(document)).map_err(convert_error))
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
use crate::shims::Error;
|
use crate::shims::Error;
|
||||||
|
use editor::consts::FILE_SAVE_SUFFIX;
|
||||||
use editor::input::keyboard::Key;
|
use editor::input::keyboard::Key;
|
||||||
use editor::tool::{SelectAppendMode, ToolType};
|
use editor::tool::{SelectAppendMode, ToolType};
|
||||||
use editor::Color as InnerColor;
|
use editor::Color as InnerColor;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn file_save_suffix() -> String {
|
||||||
|
FILE_SAVE_SUFFIX.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn i32_max() -> i32 {
|
||||||
|
i32::MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn i32_min() -> i32 {
|
||||||
|
i32::MIN
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct Color(InnerColor);
|
pub struct Color(InnerColor);
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
|
|||||||
"serde",
|
"serde",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0" }
|
||||||
glam = { version = "0.17", features = ["serde"] }
|
glam = { version = "0.17", features = ["serde"] }
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
layers::{self, Folder, Layer, LayerData, LayerDataType, Shape},
|
layers::{self, Folder, Layer, LayerData, LayerDataType, Shape},
|
||||||
DocumentError, DocumentResponse, LayerId, Operation, Quad,
|
DocumentError, DocumentResponse, LayerId, Operation, Quad,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
pub root: Layer,
|
pub root: Layer,
|
||||||
|
#[serde(skip)]
|
||||||
pub hasher: DefaultHasher,
|
pub hasher: DefaultHasher,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,10 @@ fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Document {
|
impl Document {
|
||||||
|
pub fn with_content(serialized_content: &String) -> Result<Self, DocumentError> {
|
||||||
|
serde_json::from_str(serialized_content).map_err(|e| DocumentError::InvalidFile(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper around render, that returns the whole document as a Response.
|
/// Wrapper around render, that returns the whole document as a Response.
|
||||||
pub fn render_root(&mut self) -> String {
|
pub fn render_root(&mut self) -> String {
|
||||||
self.root.render(&mut vec![]);
|
self.root.render(&mut vec![]);
|
||||||
@@ -41,6 +47,12 @@ impl Document {
|
|||||||
self.hasher.finish()
|
self.hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serialize_document(&self) -> String {
|
||||||
|
let val = serde_json::to_string(self);
|
||||||
|
// We fully expect the serialization to succeed
|
||||||
|
val.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
|
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
|
||||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||||
self.layer(path).unwrap().intersects_quad(quad, path, intersections);
|
self.layer(path).unwrap().intersects_quad(quad, path, intersections);
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ struct DAffine2Ref {
|
|||||||
pub translation: DVec2,
|
pub translation: DVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn return_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct Layer {
|
pub struct Layer {
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
@@ -71,8 +75,11 @@ pub struct Layer {
|
|||||||
pub data: LayerDataType,
|
pub data: LayerDataType,
|
||||||
#[serde(with = "DAffine2Ref")]
|
#[serde(with = "DAffine2Ref")]
|
||||||
pub transform: glam::DAffine2,
|
pub transform: glam::DAffine2,
|
||||||
|
#[serde(skip)]
|
||||||
pub cache: String,
|
pub cache: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub thumbnail_cache: String,
|
pub thumbnail_cache: String,
|
||||||
|
#[serde(skip, default = "return_true")]
|
||||||
pub cache_dirty: bool,
|
pub cache_dirty: bool,
|
||||||
pub blend_mode: BlendMode,
|
pub blend_mode: BlendMode,
|
||||||
pub opacity: f64,
|
pub opacity: f64,
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,7 @@ pub use response::DocumentResponse;
|
|||||||
|
|
||||||
pub type LayerId = u64;
|
pub type LayerId = u64;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum DocumentError {
|
pub enum DocumentError {
|
||||||
LayerNotFound,
|
LayerNotFound,
|
||||||
InvalidPath,
|
InvalidPath,
|
||||||
@@ -19,4 +19,5 @@ pub enum DocumentError {
|
|||||||
NotAFolder,
|
NotAFolder,
|
||||||
NonReorderableSelection,
|
NonReorderableSelection,
|
||||||
NotAShape,
|
NotAShape,
|
||||||
|
InvalidFile(String),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user