mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Transform API (#301)
* Enforce cache dirtification * Turn all shapes into one struct * Remove working folder * Remove old shapes * Major restructuring * Refactor Ellipse, Rectangle and ShapeTool * Simplify bounding box calculation for folder * Fix panic in select tool * Refactorselect tool * Refactor Align * Refactor flipping layers * Zoom to fit all * Refactor tools to avoid state keeping * Refactor more tools to use state that is passed along * Fix whitespace + change selection box style * Set viewbox of svg export based on the contents
This commit is contained in:
parent
63a4ed64c8
commit
f79e6e378d
55 changed files with 2059 additions and 2251 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -77,9 +77,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.16.0"
|
||||
version = "0.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16"
|
||||
checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
|
@ -168,8 +168,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42"
|
||||
source = "git+https://github.com/linebender/kurbo#0ef68211223b956942c4a5834d66a2625cb8d575"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, LayerPanelEntry } from "@/utilities/response-handler";
|
||||
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, UpdateLayer, LayerPanelEntry } from "@/utilities/response-handler";
|
||||
import { SeparatorType } from "@/components/widgets/widgets";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
|
@ -324,6 +324,22 @@ export default defineComponent({
|
|||
registerResponseHandler(ResponseType.CollapseFolder, (responseData) => {
|
||||
console.log("CollapseFolder: ", responseData);
|
||||
});
|
||||
registerResponseHandler(ResponseType.UpdateLayer, (responseData) => {
|
||||
const updateData = responseData as UpdateLayer;
|
||||
if (updateData) {
|
||||
const responsePath = updateData.path;
|
||||
const responseLayer = updateData.data;
|
||||
|
||||
const index = this.layers.findIndex((layer: LayerPanelEntry) => {
|
||||
const pathLengthsEqual = responsePath.length === layer.path.length;
|
||||
return pathLengthsEqual && responsePath.every((layer_id, i) => layer_id === layer.path[i]);
|
||||
});
|
||||
if (index >= 0) this.layers[index] = responseLayer;
|
||||
|
||||
this.setBlendModeForSelectedLayers();
|
||||
this.setOpacityForSelectedLayers();
|
||||
}
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export enum ResponseType {
|
|||
SetActiveDocument = "SetActiveDocument",
|
||||
UpdateOpenDocumentsList = "UpdateOpenDocumentsList",
|
||||
UpdateWorkingColors = "UpdateWorkingColors",
|
||||
UpdateLayer = "UpdateLayer",
|
||||
SetCanvasZoom = "SetCanvasZoom",
|
||||
SetCanvasRotation = "SetCanvasRotation",
|
||||
DisplayConfirmationToCloseDocument = "DisplayConfirmationToCloseDocument",
|
||||
|
|
@ -61,6 +62,8 @@ function parseResponse(responseType: string, data: any): Response {
|
|||
return newUpdateOpenDocumentsList(data.UpdateOpenDocumentsList);
|
||||
case "UpdateCanvas":
|
||||
return newUpdateCanvas(data.UpdateCanvas);
|
||||
case "UpdateLayer":
|
||||
return newUpdateLayer(data.UpdateLayer);
|
||||
case "SetCanvasZoom":
|
||||
return newSetCanvasZoom(data.SetCanvasZoom);
|
||||
case "SetCanvasRotation":
|
||||
|
|
@ -168,7 +171,18 @@ export interface CollapseFolder {
|
|||
}
|
||||
function newCollapseFolder(input: any): CollapseFolder {
|
||||
return {
|
||||
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
|
||||
path: newPath(input.path),
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateLayer {
|
||||
path: BigUint64Array;
|
||||
data: LayerPanelEntry;
|
||||
}
|
||||
function newUpdateLayer(input: any): UpdateLayer {
|
||||
return {
|
||||
path: newPath(input.data.path),
|
||||
data: newLayerPanelEntry(input.data),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +192,7 @@ export interface ExpandFolder {
|
|||
}
|
||||
function newExpandFolder(input: any): ExpandFolder {
|
||||
return {
|
||||
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
|
||||
path: newPath(input.path),
|
||||
children: input.children.map((child: any) => newLayerPanelEntry(child)),
|
||||
};
|
||||
}
|
||||
|
|
@ -201,6 +215,12 @@ 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])));
|
||||
return new BigUint64Array(u32CombinedPairs);
|
||||
}
|
||||
|
||||
export enum BlendMode {
|
||||
Normal = "normal",
|
||||
Multiply = "multiply",
|
||||
|
|
@ -266,7 +286,7 @@ function newLayerPanelEntry(input: any): LayerPanelEntry {
|
|||
opacity: newOpacity(input.opacity),
|
||||
layer_type: newLayerType(input.layer_type),
|
||||
layer_data: newLayerData(input.layer_data),
|
||||
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
|
||||
path: newPath(input.path),
|
||||
thumbnail: input.thumbnail,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ use editor_core::input::mouse::ScrollDelta;
|
|||
use editor_core::message_prelude::*;
|
||||
use editor_core::misc::EditorError;
|
||||
use editor_core::tool::{tool_options::ToolOptions, tools, ToolType};
|
||||
use editor_core::{
|
||||
input::mouse::{MouseState, ViewportPosition},
|
||||
LayerId,
|
||||
};
|
||||
use editor_core::{input::mouse::MouseState, LayerId};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
fn convert_error(err: editor_core::EditorError) -> JsValue {
|
||||
|
|
@ -59,44 +56,44 @@ pub fn send_tool_message(tool: String, message: &JsValue) -> Result<(), JsValue>
|
|||
|
||||
#[wasm_bindgen]
|
||||
pub fn select_document(document: usize) -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectDocument(document)).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::SelectDocument(document)).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_open_documents_list() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::GetOpenDocumentsList).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::GetOpenDocumentsList).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn new_document() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::NewDocument).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::NewDocument).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn close_document(document: usize) -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::CloseDocument(document)).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseDocument(document)).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn close_all_documents() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::CloseAllDocuments).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseAllDocuments).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn close_active_document_with_confirmation() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::CloseActiveDocumentWithConfirmation).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseActiveDocumentWithConfirmation).map_err(convert_error))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn close_all_documents_with_confirmation() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::CloseAllDocumentsWithConfirmation).map_err(convert_error))
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseAllDocumentsWithConfirmation).map_err(convert_error))
|
||||
}
|
||||
|
||||
// TODO: Call event when the panels are resized
|
||||
/// Viewport resized
|
||||
#[wasm_bindgen]
|
||||
pub fn viewport_resize(new_width: u32, new_height: u32) -> Result<(), JsValue> {
|
||||
let ev = InputPreprocessorMessage::ViewportResize(ViewportPosition { x: new_width, y: new_height });
|
||||
let ev = InputPreprocessorMessage::ViewportResize((new_width, new_height).into());
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +103,7 @@ pub fn viewport_resize(new_width: u32, new_height: u32) -> Result<(), JsValue> {
|
|||
pub fn on_mouse_move(x: u32, y: u32, modifiers: u8) -> Result<(), JsValue> {
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
// TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan
|
||||
let ev = InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }, mods);
|
||||
let ev = InputPreprocessorMessage::MouseMove((x, y).into(), mods);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
|
|
@ -122,18 +119,16 @@ pub fn on_mouse_scroll(delta_x: i32, delta_y: i32, delta_z: i32, modifiers: u8)
|
|||
/// A mouse button depressed within screenspace the bounds of the viewport
|
||||
#[wasm_bindgen]
|
||||
pub fn on_mouse_down(x: u32, y: u32, mouse_keys: u8, modifiers: u8) -> Result<(), JsValue> {
|
||||
let pos = ViewportPosition { x, y };
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
let ev = InputPreprocessorMessage::MouseDown(MouseState::from_u8_pos(mouse_keys, pos), mods);
|
||||
let ev = InputPreprocessorMessage::MouseDown(MouseState::from_u8_pos(mouse_keys, (x, y).into()), mods);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// A mouse button released
|
||||
#[wasm_bindgen]
|
||||
pub fn on_mouse_up(x: u32, y: u32, mouse_keys: u8, modifiers: u8) -> Result<(), JsValue> {
|
||||
let pos = ViewportPosition { x, y };
|
||||
let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys");
|
||||
let ev = InputPreprocessorMessage::MouseUp(MouseState::from_u8_pos(mouse_keys, pos), mods);
|
||||
let ev = InputPreprocessorMessage::MouseUp(MouseState::from_u8_pos(mouse_keys, (x, y).into()), mods);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
|
|
@ -259,14 +254,14 @@ pub fn export_document() -> Result<(), JsValue> {
|
|||
/// Sets the zoom to the value
|
||||
#[wasm_bindgen]
|
||||
pub fn set_zoom(new_zoom: f64) -> Result<(), JsValue> {
|
||||
let ev = DocumentMessage::SetCanvasZoom(new_zoom);
|
||||
let ev = MovementMessage::SetCanvasZoom(new_zoom);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Sets the rotation to the new value (in radians)
|
||||
#[wasm_bindgen]
|
||||
pub fn set_rotation(new_radians: f64) -> Result<(), JsValue> {
|
||||
let ev = DocumentMessage::SetCanvasRotation(new_radians);
|
||||
let ev = MovementMessage::SetCanvasRotation(new_radians);
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ license = "Apache-2.0"
|
|||
[dependencies]
|
||||
log = "0.4"
|
||||
|
||||
kurbo = {version="0.8", features = ["serde"]}
|
||||
kurbo = {git="https://github.com/linebender/kurbo", features = ["serde"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
glam = { version = "0.16", features = ["serde"] }
|
||||
glam = { version = "0.17", features = ["serde"] }
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
use crate::{
|
||||
layers::{self, style::PathStyle, Folder, Layer, LayerDataTypes, Line, PolyLine, Rect, Shape},
|
||||
DocumentError, DocumentResponse, LayerId, Operation,
|
||||
layers::{self, Folder, Layer, LayerData, LayerDataType, Shape},
|
||||
DocumentError, DocumentResponse, LayerId, Operation, Quad,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Document {
|
||||
pub root: Layer,
|
||||
pub work: Layer,
|
||||
pub work_mount_path: Vec<LayerId>,
|
||||
pub work_operations: Vec<Operation>,
|
||||
pub work_mounted: bool,
|
||||
pub hasher: DefaultHasher,
|
||||
}
|
||||
|
||||
impl Default for Document {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()),
|
||||
work: Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()),
|
||||
work_mount_path: Vec::new(),
|
||||
work_operations: Vec::new(),
|
||||
work_mounted: false,
|
||||
root: Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()),
|
||||
hasher: DefaultHasher::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,146 +31,71 @@ fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError>
|
|||
}
|
||||
|
||||
impl Document {
|
||||
/// Renders the layer graph with the root `path` as an SVG string.
|
||||
/// This operation merges currently mounted folder and document_folder
|
||||
/// contents together.
|
||||
pub fn render(&mut self, path: &mut Vec<LayerId>, svg: &mut String) {
|
||||
if !self.work_mount_path.as_slice().starts_with(path) {
|
||||
self.layer_mut(path).unwrap().render();
|
||||
path.pop();
|
||||
return;
|
||||
}
|
||||
if path.as_slice() == self.work_mount_path {
|
||||
// TODO: Handle if mounted in nested folders
|
||||
let transform = self.document_folder(path).unwrap().transform;
|
||||
self.document_folder_mut(path).unwrap().render_as_folder(svg);
|
||||
self.work.transform = transform;
|
||||
self.work.render_as_folder(svg);
|
||||
path.pop();
|
||||
}
|
||||
let ids = self.folder(path).unwrap().layer_ids.clone();
|
||||
for element in ids {
|
||||
path.push(element);
|
||||
self.render(path, svg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around render, that returns the whole document as a Response.
|
||||
pub fn render_root(&mut self) -> String {
|
||||
let mut svg = String::new();
|
||||
self.render(&mut vec![], &mut svg);
|
||||
svg
|
||||
self.root.render(&mut vec![]);
|
||||
self.root.cache.clone()
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> u64 {
|
||||
self.hasher.finish()
|
||||
}
|
||||
|
||||
/// 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: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
self.document_folder(path).unwrap().intersects_quad(quad, path, intersections);
|
||||
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);
|
||||
}
|
||||
|
||||
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
|
||||
pub fn intersects_quad_root(&self, quad: [DVec2; 4]) -> Vec<Vec<LayerId>> {
|
||||
pub fn intersects_quad_root(&self, quad: Quad) -> Vec<Vec<LayerId>> {
|
||||
let mut intersections = Vec::new();
|
||||
self.intersects_quad(quad, &mut vec![], &mut intersections);
|
||||
intersections
|
||||
}
|
||||
|
||||
fn is_mounted(&self, mount_path: &[LayerId], path: &[LayerId]) -> bool {
|
||||
path.starts_with(mount_path) && self.work_mounted
|
||||
}
|
||||
|
||||
/// Returns a reference to the requested folder. Fails if the path does not exist,
|
||||
/// or if the requested layer is not of type folder.
|
||||
/// This function respects mounted folders and will thus not contain the layers already
|
||||
/// present in the document if a temporary folder is mounted on top.
|
||||
pub fn folder(&self, mut path: &[LayerId]) -> Result<&Folder, DocumentError> {
|
||||
let mut root = self.root.as_folder()?;
|
||||
if self.is_mounted(self.work_mount_path.as_slice(), path) {
|
||||
path = &path[self.work_mount_path.len()..];
|
||||
root = self.work.as_folder()?;
|
||||
}
|
||||
for id in path {
|
||||
root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// This function respects mounted folders and will thus not contain the layers already
|
||||
/// present in the document if a temporary folder is mounted on top.
|
||||
/// If you manually edit the folder you have to set the cache_dirty flag yourself.
|
||||
pub fn folder_mut(&mut self, mut path: &[LayerId]) -> Result<&mut Folder, DocumentError> {
|
||||
let mut root = if self.is_mounted(self.work_mount_path.as_slice(), path) {
|
||||
path = &path[self.work_mount_path.len()..];
|
||||
self.work.as_folder_mut()?
|
||||
} else {
|
||||
self.root.as_folder_mut()?
|
||||
};
|
||||
for id in path {
|
||||
root = root.folder_mut(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
/// Returns a reference to the requested folder. Fails if the path does not exist,
|
||||
/// or if the requested layer is not of type folder.
|
||||
/// This function does **not** respect mounted folders and will always return the current
|
||||
/// state of the document, disregarding any temporary modifications.
|
||||
pub fn document_folder(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> {
|
||||
pub fn folder(&self, path: &[LayerId]) -> Result<&Folder, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
for id in path {
|
||||
root = root.as_folder()?.layer(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
Ok(root)
|
||||
root.as_folder()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// This function does **not** respect mounted folders and will always return the current
|
||||
/// state of the document, disregarding any temporary modifications.
|
||||
/// If you manually edit the folder you have to set the cache_dirty flag yourself.
|
||||
pub fn document_folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
pub 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)?;
|
||||
}
|
||||
Ok(root)
|
||||
root.as_folder_mut()
|
||||
}
|
||||
|
||||
/// Returns a reference to the layer or folder at the path. Does not return an error for root
|
||||
pub fn document_layer(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> {
|
||||
/// Returns a reference to the layer or folder at the path.
|
||||
pub fn layer(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> {
|
||||
if path.is_empty() {
|
||||
return Ok(&self.root);
|
||||
}
|
||||
let (path, id) = split_path(path)?;
|
||||
self.document_folder(path)?.as_folder()?.layer(id).ok_or(DocumentError::LayerNotFound)
|
||||
self.folder(path)?.layer(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the layer or folder at the path. Does not return an error for root
|
||||
pub fn document_layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
/// Returns a mutable reference to the layer or folder at the path.
|
||||
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
if path.is_empty() {
|
||||
return Ok(&mut self.root);
|
||||
}
|
||||
let (path, id) = split_path(path)?;
|
||||
self.document_folder_mut(path)?.as_folder_mut()?.layer_mut(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
/// Returns a reference to the layer struct at the specified `path`.
|
||||
pub fn layer(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> {
|
||||
let (path, id) = split_path(path)?;
|
||||
self.folder(path)?.layer(id).ok_or(DocumentError::LayerNotFound)
|
||||
self.folder_mut(path)?.layer_mut(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
/// 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, mut path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
||||
let mut root = if self.is_mounted(self.work_mount_path.as_slice(), path) {
|
||||
path = &path[self.work_mount_path.len()..];
|
||||
&self.work
|
||||
} else {
|
||||
&self.root
|
||||
}
|
||||
.as_folder()?;
|
||||
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
|
||||
let mut root = self.root.as_folder()?;
|
||||
let mut indices = vec![];
|
||||
let (path, layer_id) = split_path(path)?;
|
||||
|
||||
|
|
@ -186,62 +110,72 @@ impl Document {
|
|||
Ok(indices)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the layer struct at the specified `path`.
|
||||
/// If you manually edit the layer you have to set the cache_dirty flag yourself.
|
||||
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
let (path, id) = split_path(path)?;
|
||||
self.folder_mut(path)?.layer_mut(id).ok_or(DocumentError::LayerNotFound)
|
||||
}
|
||||
|
||||
/// Replaces the layer at the specified `path` with `layer`.
|
||||
pub fn set_layer(&mut self, path: &[LayerId], layer: Layer) -> Result<(), DocumentError> {
|
||||
pub fn set_layer(&mut self, path: &[LayerId], layer: Layer, insert_index: isize) -> Result<(), DocumentError> {
|
||||
let mut folder = self.root.as_folder_mut()?;
|
||||
let mut layer_id = None;
|
||||
if let Ok((path, id)) = split_path(path) {
|
||||
self.layer_mut(path)?.cache_dirty = true;
|
||||
layer_id = Some(id);
|
||||
self.mark_as_dirty(path)?;
|
||||
folder = self.folder_mut(path)?;
|
||||
if let Some(folder_layer) = folder.layer_mut(id) {
|
||||
*folder_layer = layer;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
folder.add_layer(layer, -1).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
folder.add_layer(layer, layer_id, insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a new layer to the folder specified by `path`.
|
||||
/// Passing a negative `insert_index` indexes relative to the end.
|
||||
/// -1 is equivalent to adding the layer to the top.
|
||||
pub fn add_layer(&mut self, path: &[LayerId], mut layer: Layer, insert_index: isize) -> Result<LayerId, DocumentError> {
|
||||
layer.render();
|
||||
pub fn add_layer(&mut self, path: &[LayerId], layer: Layer, insert_index: isize) -> Result<LayerId, DocumentError> {
|
||||
let folder = self.folder_mut(path)?;
|
||||
folder.add_layer(layer, insert_index).ok_or(DocumentError::IndexOutOfBounds)
|
||||
folder.add_layer(layer, None, insert_index).ok_or(DocumentError::IndexOutOfBounds)
|
||||
}
|
||||
|
||||
/// Deletes the layer specified by `path`.
|
||||
pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let (path, id) = split_path(path)?;
|
||||
let _ = self.layer_mut(path).map(|x| x.cache_dirty = true);
|
||||
self.document_folder_mut(path)?.as_folder_mut()?.remove_layer(id)
|
||||
self.mark_as_dirty(path)?;
|
||||
self.folder_mut(path)?.remove_layer(id)
|
||||
}
|
||||
|
||||
pub fn layer_axis_aligned_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
|
||||
// TODO: Replace with functions of the transform api
|
||||
if path.is_empty() {
|
||||
// Special case for root. Root's local is the documents global, so we avoid transforming its transform by itself.
|
||||
self.layer_local_bounding_box(path)
|
||||
} else {
|
||||
let layer = self.document_layer(path)?;
|
||||
Ok(layer.bounding_box(self.root.transform * layer.transform, layer.style))
|
||||
pub fn visible_layers(&self, path: &mut Vec<LayerId>, paths: &mut Vec<Vec<LayerId>>) -> Result<(), DocumentError> {
|
||||
if !self.layer(path)?.visible {
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(folder) = self.folder(path) {
|
||||
for layer in folder.layer_ids.iter() {
|
||||
path.push(*layer);
|
||||
self.visible_layers(path, paths)?;
|
||||
path.pop();
|
||||
}
|
||||
} else {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn layer_local_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
|
||||
// TODO: Replace with functions of the transform api
|
||||
let layer = self.document_layer(path)?;
|
||||
Ok(layer.bounding_box(layer.transform, layer.style))
|
||||
pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
|
||||
let layer = self.layer(path)?;
|
||||
let transform = self.multiply_transforms(path)?;
|
||||
Ok(layer.data.bounding_box(transform))
|
||||
}
|
||||
|
||||
fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
let mut paths = vec![];
|
||||
self.visible_layers(&mut vec![], &mut paths).ok()?;
|
||||
self.combined_viewport_bounding_box(paths.iter().map(|x| x.as_slice()))
|
||||
}
|
||||
|
||||
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>) -> Option<[DVec2; 2]> {
|
||||
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path).ok()?);
|
||||
boxes.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
|
||||
pub fn mark_upstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let mut root = &mut self.root;
|
||||
root.cache_dirty = true;
|
||||
for id in path {
|
||||
|
|
@ -251,37 +185,114 @@ impl Document {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn working_paths(&mut self) -> Vec<Vec<LayerId>> {
|
||||
self.work
|
||||
.as_folder()
|
||||
.unwrap()
|
||||
.layer_ids
|
||||
.iter()
|
||||
.map(|id| self.work_mount_path.iter().chain([*id].iter()).cloned().collect())
|
||||
.collect()
|
||||
pub fn mark_downstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let mut layer = self.layer_mut(path)?;
|
||||
layer.cache_dirty = true;
|
||||
|
||||
let mut path = path.to_vec();
|
||||
let len = path.len();
|
||||
path.push(0);
|
||||
|
||||
if let Some(ids) = layer.as_folder().ok().map(|f| f.layer_ids.clone()) {
|
||||
for id in ids {
|
||||
path[len] = id;
|
||||
self.mark_downstream_as_dirty(&path)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
self.mark_downstream_as_dirty(path)?;
|
||||
self.mark_upstream_as_dirty(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
let mut transforms = vec![self.root.transform];
|
||||
for id in path {
|
||||
if let Ok(folder) = root.as_folder() {
|
||||
root = folder.layer(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
transforms.push(root.transform);
|
||||
}
|
||||
Ok(transforms)
|
||||
}
|
||||
|
||||
pub fn multiply_transforms(&self, path: &[LayerId]) -> Result<DAffine2, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
let mut trans = self.root.transform;
|
||||
for id in path {
|
||||
if let Ok(folder) = root.as_folder() {
|
||||
root = folder.layer(*id).ok_or(DocumentError::LayerNotFound)?;
|
||||
}
|
||||
trans = trans * root.transform;
|
||||
}
|
||||
Ok(trans)
|
||||
}
|
||||
|
||||
pub fn generate_transform_across_scope(&self, from: &[LayerId], to: Option<DAffine2>) -> Result<DAffine2, DocumentError> {
|
||||
let from_rev = self.multiply_transforms(from)?;
|
||||
let scope = to.unwrap_or(DAffine2::IDENTITY);
|
||||
Ok(scope * from_rev)
|
||||
}
|
||||
|
||||
pub fn transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
|
||||
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
|
||||
let layer = self.layer_mut(layer)?;
|
||||
layer.transform = to.inverse() * transform * to * layer.transform;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
|
||||
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
|
||||
let layer = self.layer_mut(layer)?;
|
||||
layer.transform = to.inverse() * transform;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
|
||||
self.transform_relative_to_scope(layer, None, transform)
|
||||
}
|
||||
|
||||
pub fn set_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
|
||||
self.set_transform_relative_to_scope(layer, None, transform)
|
||||
}
|
||||
|
||||
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
|
||||
/// reaction from the frontend, responses may be returned.
|
||||
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
operation.pseudo_hash().hash(&mut self.hasher);
|
||||
|
||||
let responses = match &operation {
|
||||
Operation::AddEllipse { path, insert_index, transform, style } => {
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new()), *transform, *style), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path }])
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform), *insert_index)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
}
|
||||
Operation::AddRect { path, insert_index, transform, style } => {
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect), *transform, *style), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path }])
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform), *insert_index)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
}
|
||||
Operation::AddBoundingBox { path, transform, style } => {
|
||||
let mut rect = Shape::rectangle(*style);
|
||||
rect.render_index = -1;
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(rect), *transform), -1)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
}
|
||||
Operation::AddShape {
|
||||
path,
|
||||
insert_index,
|
||||
transform,
|
||||
style,
|
||||
sides,
|
||||
} => {
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::shape(*sides, *style)), *transform), *insert_index)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
}
|
||||
Operation::AddLine { path, insert_index, transform, style } => {
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line), *transform, *style), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path }])
|
||||
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform), *insert_index)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
|
||||
}
|
||||
Operation::AddPen {
|
||||
path,
|
||||
|
|
@ -291,27 +302,11 @@ impl Document {
|
|||
style,
|
||||
} => {
|
||||
let points: Vec<glam::DVec2> = points.iter().map(|&it| it.into()).collect();
|
||||
let polyline = PolyLine::new(points);
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::PolyLine(polyline), *transform, *style), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path }])
|
||||
}
|
||||
Operation::AddShape {
|
||||
path,
|
||||
insert_index,
|
||||
transform,
|
||||
equal_sides,
|
||||
sides,
|
||||
style,
|
||||
} => {
|
||||
let s = Shape::new(*equal_sides, *sides);
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s), *transform, *style), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path }])
|
||||
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() }])
|
||||
}
|
||||
Operation::DeleteLayer { path } => {
|
||||
self.delete(&path)?;
|
||||
self.delete(path)?;
|
||||
|
||||
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||
Some(vec![
|
||||
|
|
@ -322,9 +317,9 @@ impl Document {
|
|||
}
|
||||
Operation::PasteLayer { path, layer, insert_index } => {
|
||||
let folder = self.folder_mut(path)?;
|
||||
//FIXME: This clone of layer should be avoided somehow
|
||||
let id = folder.add_layer(layer.clone(), *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
let full_path = [path.clone(), vec![id]].concat();
|
||||
self.mark_as_dirty(&full_path)?;
|
||||
|
||||
Some(vec![
|
||||
DocumentResponse::DocumentChanged,
|
||||
|
|
@ -333,111 +328,91 @@ impl Document {
|
|||
])
|
||||
}
|
||||
Operation::DuplicateLayer { path } => {
|
||||
let layer = self.layer(&path)?.clone();
|
||||
let layer = self.layer(path)?.clone();
|
||||
let (folder_path, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
|
||||
let folder = self.folder_mut(folder_path)?;
|
||||
folder.add_layer(layer, -1).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
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() }])
|
||||
}
|
||||
Operation::RenameLayer { path, name } => {
|
||||
self.layer_mut(path)?.name = Some(name.clone());
|
||||
Some(vec![DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::AddFolder { path } => {
|
||||
self.set_layer(&path, Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()))?;
|
||||
self.set_layer(path, Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()), -1)?;
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::MountWorkingFolder { path } => {
|
||||
let mut responses: Vec<_> = self.working_paths().into_iter().map(|path| DocumentResponse::DeletedLayer { path }).collect();
|
||||
self.work_mount_path = path.clone();
|
||||
self.work_operations.clear();
|
||||
self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default());
|
||||
self.work_mounted = true;
|
||||
responses.push(DocumentResponse::DocumentChanged);
|
||||
Some(responses)
|
||||
}
|
||||
Operation::TransformLayer { path, transform } => {
|
||||
let layer = self.document_layer_mut(path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(&transform) * layer.transform;
|
||||
let layer = self.layer_mut(path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(transform) * layer.transform;
|
||||
layer.transform = transform;
|
||||
layer.cache_dirty = true;
|
||||
self.root.cache_dirty = true;
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::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() }])
|
||||
}
|
||||
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() }])
|
||||
}
|
||||
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() }])
|
||||
}
|
||||
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() }])
|
||||
}
|
||||
Operation::SetLayerTransform { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
let layer = self.document_layer_mut(path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(transform);
|
||||
let layer = self.layer_mut(path)?;
|
||||
layer.transform = transform;
|
||||
layer.cache_dirty = true;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
}
|
||||
Operation::DiscardWorkingFolder => {
|
||||
let mut responses: Vec<_> = self.working_paths().into_iter().map(|path| DocumentResponse::DeletedLayer { path }).collect();
|
||||
self.work_operations.clear();
|
||||
self.work_mount_path = vec![];
|
||||
self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default());
|
||||
self.work_mounted = false;
|
||||
responses.push(DocumentResponse::DocumentChanged);
|
||||
Some(responses)
|
||||
}
|
||||
Operation::ClearWorkingFolder => {
|
||||
let mut responses: Vec<_> = self.working_paths().into_iter().map(|path| DocumentResponse::DeletedLayer { path }).collect();
|
||||
self.work_operations.clear();
|
||||
self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default());
|
||||
responses.push(DocumentResponse::DocumentChanged);
|
||||
Some(responses)
|
||||
}
|
||||
Operation::CommitTransaction => {
|
||||
let mut responses: Vec<_> = self.working_paths().into_iter().map(|path| DocumentResponse::DeletedLayer { path }).collect();
|
||||
let mut ops = Vec::new();
|
||||
let mut path: Vec<LayerId> = vec![];
|
||||
std::mem::swap(&mut path, &mut self.work_mount_path);
|
||||
std::mem::swap(&mut ops, &mut self.work_operations);
|
||||
self.work_mounted = false;
|
||||
self.work_mount_path = vec![];
|
||||
self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default());
|
||||
for operation in ops.into_iter() {
|
||||
if let Some(mut op_responses) = self.handle_operation(operation)? {
|
||||
responses.append(&mut op_responses);
|
||||
}
|
||||
}
|
||||
responses.extend(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }]);
|
||||
|
||||
Some(responses)
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::ToggleVisibility { path } => {
|
||||
if let Ok(layer) = self.layer_mut(&path) {
|
||||
self.mark_as_dirty(path)?;
|
||||
if let Ok(layer) = self.layer_mut(path) {
|
||||
layer.visible = !layer.visible;
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
let path = path.as_slice()[..path.len() - 1].to_vec();
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::SetLayerBlendMode { path, blend_mode } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
self.layer_mut(path)?.blend_mode = *blend_mode;
|
||||
|
||||
let path = path.as_slice()[..path.len() - 1].to_vec();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::SetLayerOpacity { path, opacity } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
self.layer_mut(path)?.opacity = *opacity;
|
||||
|
||||
let path = path.as_slice()[..path.len() - 1].to_vec();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::FillLayer { path, color } => {
|
||||
self.layer_mut(path)?.style.set_fill(layers::style::Fill::new(*color));
|
||||
let layer = self.layer_mut(path)?;
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(s) => s.style.set_fill(layers::style::Fill::new(*color)),
|
||||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(path)?;
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
|
||||
}
|
||||
};
|
||||
if !matches!(
|
||||
operation,
|
||||
Operation::CommitTransaction | Operation::MountWorkingFolder { .. } | Operation::DiscardWorkingFolder | Operation::ClearWorkingFolder
|
||||
) {
|
||||
self.work_operations.push(operation);
|
||||
}
|
||||
Ok(responses)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,57 @@
|
|||
use glam::DVec2;
|
||||
use std::ops::Mul;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{BezPath, Line, PathSeg, Point, Shape, Vec2};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
pub struct Quad([DVec2; 4]);
|
||||
|
||||
impl Quad {
|
||||
pub fn from_box(bbox: [DVec2; 2]) -> Self {
|
||||
let size = bbox[1] - bbox[0];
|
||||
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[0] + size * DVec2::Y, bbox[1]])
|
||||
}
|
||||
|
||||
pub fn lines(&self) -> [Line; 4] {
|
||||
[
|
||||
Line::new(to_point(self.0[0]), to_point(self.0[1])),
|
||||
Line::new(to_point(self.0[1]), to_point(self.0[2])),
|
||||
Line::new(to_point(self.0[2]), to_point(self.0[3])),
|
||||
Line::new(to_point(self.0[3]), to_point(self.0[0])),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Quad> for DAffine2 {
|
||||
type Output = Quad;
|
||||
|
||||
fn mul(self, rhs: Quad) -> Self::Output {
|
||||
let mut output = Quad::default();
|
||||
for (i, point) in rhs.0.iter().enumerate() {
|
||||
output.0[i] = self.transform_point2(*point);
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
fn to_point(vec: DVec2) -> Point {
|
||||
Point::new(vec.x, vec.y)
|
||||
}
|
||||
|
||||
pub fn intersect_quad_bez_path(quad: [DVec2; 4], shape: &BezPath, closed: bool) -> bool {
|
||||
let lines = vec![
|
||||
Line::new(to_point(quad[0]), to_point(quad[1])),
|
||||
Line::new(to_point(quad[1]), to_point(quad[2])),
|
||||
Line::new(to_point(quad[2]), to_point(quad[3])),
|
||||
Line::new(to_point(quad[3]), to_point(quad[0])),
|
||||
];
|
||||
pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, closed: bool) -> bool {
|
||||
// check if outlines intersect
|
||||
for path_segment in shape.segments() {
|
||||
for line in &lines {
|
||||
if !path_segment.intersect_line(*line).is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if shape.segments().any(|path_segment| quad.lines().iter().any(|line| !path_segment.intersect_line(*line).is_empty())) {
|
||||
return true;
|
||||
}
|
||||
// check if selection is entirely within the shape
|
||||
if closed && shape.contains(to_point(quad[0])) {
|
||||
if closed && quad.0.iter().any(|q| shape.contains(to_point(*q))) {
|
||||
return true;
|
||||
}
|
||||
// check if shape is entirely within the selection
|
||||
if let Some(shape_point) = get_arbitrary_point_on_path(shape) {
|
||||
let mut pos = 0;
|
||||
let mut neg = 0;
|
||||
for line in lines {
|
||||
for line in quad.lines() {
|
||||
if line.p0 == shape_point {
|
||||
return true;
|
||||
};
|
||||
|
|
|
|||
44
core/document/src/layers/blend_mode.rs
Normal file
44
core/document/src/layers/blend_mode.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BlendMode {
|
||||
Normal,
|
||||
Multiply,
|
||||
Darken,
|
||||
ColorBurn,
|
||||
Screen,
|
||||
Lighten,
|
||||
ColorDodge,
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
Difference,
|
||||
Exclusion,
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
pub fn to_svg_style_name(&self) -> &str {
|
||||
match self {
|
||||
BlendMode::Normal => "normal",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use glam::DAffine2;
|
||||
use glam::DVec2;
|
||||
use kurbo::Shape;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Deserialize, Serialize)]
|
||||
pub struct Ellipse {}
|
||||
|
||||
impl Ellipse {
|
||||
pub fn new() -> Ellipse {
|
||||
Ellipse {}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerData for Ellipse {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
|
||||
kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.01)
|
||||
}
|
||||
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
use glam::DVec2;
|
||||
|
||||
use crate::{DocumentError, LayerId};
|
||||
use crate::{DocumentError, LayerId, Quad};
|
||||
|
||||
use super::{style, Layer, LayerData, LayerDataTypes};
|
||||
use super::{Layer, LayerData, LayerDataType};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct Folder {
|
||||
next_assignment_id: LayerId,
|
||||
pub layer_ids: Vec<LayerId>,
|
||||
|
|
@ -15,44 +15,53 @@ pub struct Folder {
|
|||
}
|
||||
|
||||
impl LayerData for Folder {
|
||||
fn to_kurbo_path(&self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, _style: style::PathStyle) {
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
|
||||
let _ = svg.write_str(&(f.to_string() + if i != 5 { "," } else { "" }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
|
||||
for layer in &mut self.layers {
|
||||
let _ = writeln!(svg, "{}", layer.render());
|
||||
let _ = writeln!(svg, "{}", layer.render(transforms));
|
||||
}
|
||||
let _ = writeln!(svg, "</g>");
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _style: style::PathStyle) {
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
|
||||
path.push(*layer_id);
|
||||
layer.intersects_quad(quad, path, intersections);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.layers
|
||||
.iter()
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
}
|
||||
|
||||
impl Folder {
|
||||
pub fn add_layer(&mut self, layer: Layer, insert_index: isize) -> Option<LayerId> {
|
||||
/// When a insertion id is provided, try to insert the layer with the given id.
|
||||
/// If that id is already used, return None.
|
||||
/// When no insertion id is provided, search for the next free id and insert it with that.
|
||||
pub fn add_layer(&mut self, layer: Layer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
|
||||
let mut insert_index = insert_index as i128;
|
||||
if insert_index < 0 {
|
||||
insert_index = self.layers.len() as i128 + insert_index as i128 + 1;
|
||||
}
|
||||
|
||||
if insert_index <= self.layers.len() as i128 && insert_index >= 0 {
|
||||
if let Some(id) = id {
|
||||
self.next_assignment_id = id;
|
||||
}
|
||||
if self.layer_ids.contains(&self.next_assignment_id) {
|
||||
return None;
|
||||
}
|
||||
let id = self.next_assignment_id;
|
||||
self.layers.insert(insert_index as usize, layer);
|
||||
self.layer_ids.insert(insert_index as usize, self.next_assignment_id);
|
||||
self.next_assignment_id += 1;
|
||||
Some(self.next_assignment_id - 1)
|
||||
self.layer_ids.insert(insert_index as usize, id);
|
||||
// Linear probing for collision avoidance
|
||||
while self.layer_ids.contains(&self.next_assignment_id) {
|
||||
self.next_assignment_id += 1;
|
||||
}
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -95,8 +104,8 @@ impl Folder {
|
|||
pub fn folder(&self, id: LayerId) -> Option<&Folder> {
|
||||
match self.layer(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataTypes::Folder(folder), ..
|
||||
}) => Some(&folder),
|
||||
data: LayerDataType::Folder(folder), ..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -104,46 +113,9 @@ impl Folder {
|
|||
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut Folder> {
|
||||
match self.layer_mut(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataTypes::Folder(folder), ..
|
||||
data: LayerDataType::Folder(folder), ..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut layers_non_empty_bounding_boxes = self.layers.iter().filter_map(|layer| layer.bounding_box(transform * layer.transform, layer.style)).peekable();
|
||||
|
||||
layers_non_empty_bounding_boxes.peek()?;
|
||||
|
||||
let mut x_min = f64::MAX;
|
||||
let mut y_min = f64::MAX;
|
||||
let mut x_max = f64::MIN;
|
||||
let mut y_max = f64::MIN;
|
||||
|
||||
for [bounding_box_min, bounding_box_max] in layers_non_empty_bounding_boxes {
|
||||
if bounding_box_min.x < x_min {
|
||||
x_min = bounding_box_min.x
|
||||
}
|
||||
if bounding_box_min.y < y_min {
|
||||
y_min = bounding_box_min.y
|
||||
}
|
||||
if bounding_box_max.x > x_max {
|
||||
x_max = bounding_box_max.x
|
||||
}
|
||||
if bounding_box_max.y > y_max {
|
||||
y_max = bounding_box_max.y
|
||||
}
|
||||
}
|
||||
Some([DVec2::new(x_min, y_min), DVec2::new(x_max, y_max)])
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Folder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layer_ids: vec![],
|
||||
layers: vec![],
|
||||
next_assignment_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
use glam::DAffine2;
|
||||
use glam::DVec2;
|
||||
use kurbo::Point;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Line;
|
||||
|
||||
impl LayerData for Line {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
|
||||
fn new_point(a: DVec2) -> Point {
|
||||
Point::new(a.x, a.y)
|
||||
}
|
||||
let mut path = kurbo::BezPath::new();
|
||||
path.move_to(new_point(transform.translation));
|
||||
path.line_to(new_point(transform.transform_point2(DVec2::ONE)));
|
||||
path
|
||||
}
|
||||
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
let [x1, y1] = transform.translation.to_array();
|
||||
let [x2, y2] = transform.transform_point2(DVec2::ONE).to_array();
|
||||
|
||||
let _ = write!(svg, r#"<line x1="{}" y1="{}" x2="{}" y2="{}"{} />"#, x1, y1, x2, y2, style.render(),);
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,159 +1,60 @@
|
|||
pub mod style;
|
||||
|
||||
pub mod ellipse;
|
||||
pub use ellipse::Ellipse;
|
||||
|
||||
pub mod line;
|
||||
use glam::DAffine2;
|
||||
use glam::{DMat2, DVec2};
|
||||
use kurbo::BezPath;
|
||||
use kurbo::Shape as KurboShape;
|
||||
pub use line::Line;
|
||||
|
||||
pub mod rect;
|
||||
pub use rect::Rect;
|
||||
pub mod blend_mode;
|
||||
pub use blend_mode::BlendMode;
|
||||
|
||||
pub mod polyline;
|
||||
pub use polyline::PolyLine;
|
||||
|
||||
pub mod shape;
|
||||
pub use shape::Shape;
|
||||
pub mod simple_shape;
|
||||
pub use simple_shape::Shape;
|
||||
|
||||
pub mod folder;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
use crate::{DocumentError, Quad};
|
||||
pub use folder::Folder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
pub trait LayerData {
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle);
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle);
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>);
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
// TODO: Rename this `LayerDataType` to not be plural in a separate commit (together with `enum ToolOptions`)
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum LayerDataTypes {
|
||||
pub enum LayerDataType {
|
||||
Folder(Folder),
|
||||
Ellipse(Ellipse),
|
||||
Rect(Rect),
|
||||
Line(Line),
|
||||
PolyLine(PolyLine),
|
||||
Shape(Shape),
|
||||
}
|
||||
|
||||
macro_rules! call_render {
|
||||
($self:ident.render($svg:ident, $transform:ident, $style:ident) { $($variant:ident),* }) => {
|
||||
match $self {
|
||||
$(Self::$variant(x) => x.render($svg, $transform, $style)),*
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! call_kurbo_path {
|
||||
($self:ident.to_kurbo_path($transform:ident, $style:ident) { $($variant:ident),* }) => {
|
||||
match $self {
|
||||
$(Self::$variant(x) => x.to_kurbo_path($transform, $style)),*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! call_intersects_quad {
|
||||
($self:ident.intersects_quad($quad:ident, $path:ident, $intersections:ident, $style:ident) { $($variant:ident),* }) => {
|
||||
match $self {
|
||||
$(Self::$variant(x) => x.intersects_quad($quad, $path, $intersections, $style)),*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl LayerDataTypes {
|
||||
pub fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
call_render! {
|
||||
self.render(svg, transform, style) {
|
||||
Folder,
|
||||
Ellipse,
|
||||
Rect,
|
||||
Line,
|
||||
PolyLine,
|
||||
Shape
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath {
|
||||
call_kurbo_path! {
|
||||
self.to_kurbo_path(transform, style) {
|
||||
Folder,
|
||||
Ellipse,
|
||||
Rect,
|
||||
Line,
|
||||
PolyLine,
|
||||
Shape
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
call_intersects_quad! {
|
||||
self.intersects_quad(quad, path, intersections, style) {
|
||||
Folder,
|
||||
Ellipse,
|
||||
Rect,
|
||||
Line,
|
||||
PolyLine,
|
||||
Shape
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self, transform: glam::DAffine2, style: style::PathStyle) -> [DVec2; 2] {
|
||||
let bez_path = self.to_kurbo_path(transform, style);
|
||||
let bbox = bez_path.bounding_box();
|
||||
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BlendMode {
|
||||
Normal,
|
||||
Multiply,
|
||||
Darken,
|
||||
ColorBurn,
|
||||
Screen,
|
||||
Lighten,
|
||||
ColorDodge,
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
Difference,
|
||||
Exclusion,
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
fn to_svg_style_name(&self) -> &str {
|
||||
impl LayerDataType {
|
||||
pub fn inner(&self) -> &dyn LayerData {
|
||||
match self {
|
||||
BlendMode::Normal => "normal",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerData for LayerDataType {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
|
||||
self.inner_mut().render(svg, transforms)
|
||||
}
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
self.inner().intersects_quad(quad, path, intersections)
|
||||
}
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
|
@ -163,14 +64,13 @@ struct DAffine2Ref {
|
|||
pub translation: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Layer {
|
||||
pub visible: bool,
|
||||
pub name: Option<String>,
|
||||
pub data: LayerDataTypes,
|
||||
pub data: LayerDataType,
|
||||
#[serde(with = "DAffine2Ref")]
|
||||
pub transform: glam::DAffine2,
|
||||
pub style: style::PathStyle,
|
||||
pub cache: String,
|
||||
pub thumbnail_cache: String,
|
||||
pub cache_dirty: bool,
|
||||
|
|
@ -179,13 +79,12 @@ pub struct Layer {
|
|||
}
|
||||
|
||||
impl Layer {
|
||||
pub fn new(data: LayerDataTypes, transform: [f64; 6], style: style::PathStyle) -> Self {
|
||||
pub fn new(data: LayerDataType, transform: [f64; 6]) -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
name: None,
|
||||
data,
|
||||
transform: glam::DAffine2::from_cols_array(&transform),
|
||||
style,
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
|
|
@ -194,79 +93,72 @@ impl Layer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> &str {
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>) -> &str {
|
||||
if !self.visible {
|
||||
return "";
|
||||
}
|
||||
if self.cache_dirty {
|
||||
transforms.push(self.transform);
|
||||
self.thumbnail_cache.clear();
|
||||
self.data.render(&mut self.thumbnail_cache, self.transform, self.style);
|
||||
self.data.render(&mut self.thumbnail_cache, transforms);
|
||||
|
||||
self.cache.clear();
|
||||
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
|
||||
self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
|
||||
let _ = self.cache.write_str(&(f.to_string() + if i != 5 { "," } else { "" }));
|
||||
});
|
||||
let _ = write!(
|
||||
self.cache,
|
||||
r#"<g style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
|
||||
r#")" style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
|
||||
self.blend_mode.to_svg_style_name(),
|
||||
self.opacity,
|
||||
self.thumbnail_cache.as_str()
|
||||
);
|
||||
|
||||
transforms.pop();
|
||||
self.cache_dirty = false;
|
||||
}
|
||||
self.cache.as_str()
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
let inv_transform = self.transform.inverse();
|
||||
let transformed_quad = [
|
||||
inv_transform.transform_point2(quad[0]),
|
||||
inv_transform.transform_point2(quad[1]),
|
||||
inv_transform.transform_point2(quad[2]),
|
||||
inv_transform.transform_point2(quad[3]),
|
||||
];
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if !self.visible {
|
||||
return;
|
||||
}
|
||||
self.data.intersects_quad(transformed_quad, path, intersections, self.style)
|
||||
}
|
||||
|
||||
pub fn render_on(&mut self, svg: &mut String) {
|
||||
*svg += self.render();
|
||||
}
|
||||
|
||||
pub fn to_kurbo_path(&self) -> BezPath {
|
||||
self.data.to_kurbo_path(self.transform, self.style)
|
||||
let transformed_quad = self.transform.inverse() * quad;
|
||||
self.data.intersects_quad(transformed_quad, path, intersections)
|
||||
}
|
||||
|
||||
pub fn current_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box(self.transform, self.style)
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self, transform: glam::DAffine2, style: style::PathStyle) -> Option<[DVec2; 2]> {
|
||||
if let Ok(folder) = self.as_folder() {
|
||||
folder.bounding_box(transform)
|
||||
} else {
|
||||
Some(self.data.bounding_box(transform, style))
|
||||
}
|
||||
self.data.bounding_box(self.transform)
|
||||
}
|
||||
|
||||
pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataTypes::Folder(f) => Ok(f),
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_folder(&self) -> Result<&Folder, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataTypes::Folder(f) => Ok(&f),
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_as_folder(&mut self, svg: &mut String) {
|
||||
if let LayerDataTypes::Folder(f) = &mut self.data {
|
||||
f.render(svg, self.transform, self.style)
|
||||
impl Clone for Layer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
visible: self.visible,
|
||||
name: self.name.clone(),
|
||||
data: self.data.clone(),
|
||||
transform: self.transform,
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
blend_mode: self.blend_mode,
|
||||
opacity: self.opacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
use crate::{intersection::intersect_quad_bez_path, LayerId};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::{style, LayerData};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PolyLine {
|
||||
points: Vec<glam::DVec2>,
|
||||
}
|
||||
|
||||
impl PolyLine {
|
||||
pub fn new(points: Vec<impl Into<glam::DVec2>>) -> PolyLine {
|
||||
PolyLine {
|
||||
points: points.into_iter().map(|it| it.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerData for PolyLine {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
|
||||
let mut path = kurbo::BezPath::new();
|
||||
self.points
|
||||
.iter()
|
||||
.map(|v| transform.transform_point2(*v))
|
||||
.map(|v| kurbo::Point { x: v.x, y: v.y })
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
|
||||
path
|
||||
}
|
||||
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
if self.points.is_empty() {
|
||||
return;
|
||||
}
|
||||
let _ = write!(svg, r#"<polyline points=""#);
|
||||
let mut points = self.points.iter().map(|v| transform.transform_point2(*v));
|
||||
let first = points.next().unwrap();
|
||||
let _ = write!(svg, "{:.3} {:.3}", first.x, first.y);
|
||||
for point in points {
|
||||
let _ = write!(svg, " {:.3} {:.3}", point.x, point.y);
|
||||
}
|
||||
let _ = write!(svg, r#""{} />"#, style.render());
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn polyline_should_render() {
|
||||
use super::style::PathStyle;
|
||||
use glam::DVec2;
|
||||
let mut polyline = PolyLine {
|
||||
points: vec![DVec2::new(3.0, 4.12354), DVec2::new(1.0, 5.54)],
|
||||
};
|
||||
|
||||
let mut svg = String::new();
|
||||
polyline.render(&mut svg, glam::DAffine2::IDENTITY, PathStyle::default());
|
||||
assert_eq!(r##"<polyline points="3.000 4.124 1.000 5.540" />"##, svg);
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
use glam::DAffine2;
|
||||
use glam::DVec2;
|
||||
use kurbo::Point;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Rect;
|
||||
|
||||
impl LayerData for Rect {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
|
||||
fn new_point(a: DVec2) -> Point {
|
||||
Point::new(a.x, a.y)
|
||||
}
|
||||
let mut path = kurbo::BezPath::new();
|
||||
path.move_to(new_point(transform.translation));
|
||||
|
||||
// TODO: Use into_iter when new impls get added in rust 2021
|
||||
[(1., 0.), (1., 1.), (0., 1.)].iter().for_each(|v| path.line_to(new_point(transform.transform_point2((*v).into()))));
|
||||
path.close_path();
|
||||
path
|
||||
}
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
use glam::DAffine2;
|
||||
use glam::DVec2;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
use kurbo::BezPath;
|
||||
|
||||
use super::style;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Shape {
|
||||
equal_sides: bool,
|
||||
sides: u8,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn new(equal_sides: bool, sides: u8) -> Shape {
|
||||
Shape { equal_sides, sides }
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerData for Shape {
|
||||
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath {
|
||||
fn unit_rotation(theta: f64) -> DVec2 {
|
||||
DVec2::new(-theta.sin(), theta.cos())
|
||||
}
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let apothem_offset_angle = std::f64::consts::PI / (self.sides as f64);
|
||||
|
||||
let relative_points = (0..self.sides).map(|i| apothem_offset_angle * ((i * 2 + ((self.sides + 1) % 2)) as f64)).map(unit_rotation);
|
||||
|
||||
let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||
relative_points.clone().for_each(|p| {
|
||||
min_x = min_x.min(p.x);
|
||||
min_y = min_y.min(p.y);
|
||||
max_x = max_x.max(p.x);
|
||||
max_y = max_y.max(p.y);
|
||||
});
|
||||
|
||||
relative_points
|
||||
.map(|p| {
|
||||
if self.equal_sides {
|
||||
p
|
||||
} else {
|
||||
DVec2::new((p.x - min_x) / (max_x - min_x) * 2. - 1., (p.y - min_y) / (max_y - min_y) * 2. - 1.)
|
||||
}
|
||||
})
|
||||
.map(|p| DVec2::new(p.x / 2. + 0.5, p.y / 2. + 0.5))
|
||||
.map(|unit| transform.transform_point2(unit))
|
||||
.map(|pos| kurbo::Point::new(pos.x, pos.y))
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| {
|
||||
if i == 0 {
|
||||
path.move_to(p);
|
||||
} else {
|
||||
path.line_to(p);
|
||||
}
|
||||
});
|
||||
|
||||
path.close_path();
|
||||
path
|
||||
}
|
||||
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
|
||||
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
146
core/document/src/layers/simple_shape.rs
Normal file
146
core/document/src/layers/simple_shape.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use glam::DAffine2;
|
||||
use glam::DMat2;
|
||||
use glam::DVec2;
|
||||
|
||||
use kurbo::Affine;
|
||||
use kurbo::Shape as KurboShape;
|
||||
|
||||
use crate::intersection::intersect_quad_bez_path;
|
||||
use crate::LayerId;
|
||||
use crate::Quad;
|
||||
use kurbo::BezPath;
|
||||
|
||||
use super::style;
|
||||
use super::style::PathStyle;
|
||||
use super::LayerData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Shape {
|
||||
pub path: BezPath,
|
||||
pub style: style::PathStyle,
|
||||
pub render_index: i32,
|
||||
pub solid: bool,
|
||||
}
|
||||
|
||||
impl LayerData for Shape {
|
||||
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>) {
|
||||
let mut path = self.path.clone();
|
||||
let transform = self.transform(transforms);
|
||||
let inverse = transform.inverse();
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i != 5 { "," } else { "" }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render());
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.path.clone();
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
use kurbo::Shape;
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.solid) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn transform(&self, transforms: &[DAffine2]) -> DAffine2 {
|
||||
let start = match self.render_index {
|
||||
-1 => 0,
|
||||
x => (transforms.len() as i32 - x).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn shape(sides: u8, style: PathStyle) -> Self {
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
fn unit_rotation(theta: f64) -> DVec2 {
|
||||
DVec2::new(theta.sin(), theta.cos())
|
||||
}
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let apothem_offset_angle = TAU / (sides as f64);
|
||||
// Rotate odd sided shapes by 90 degrees
|
||||
let offset = ((sides + 1) % 2) as f64 * FRAC_PI_2;
|
||||
|
||||
let relative_points = (0..sides).map(|i| apothem_offset_angle * i as f64 + offset).map(unit_rotation);
|
||||
let min = relative_points.clone().reduce(|a, b| a.min(b)).unwrap_or_default();
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::ONE / 2., 0., -min / 2.);
|
||||
let point = |vec: DVec2| kurbo::Point::new(vec.x, vec.y);
|
||||
let mut relative_points = relative_points.map(|p| point(transform.transform_point2(p)));
|
||||
path.move_to(relative_points.next().expect("Tried to create an ngon with 0 sides"));
|
||||
relative_points.for_each(|p| path.line_to(p));
|
||||
|
||||
path.close_path();
|
||||
Self {
|
||||
path,
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn rectangle(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn ellipse(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn line(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
|
||||
style,
|
||||
render_index: 1,
|
||||
solid: true,
|
||||
}
|
||||
}
|
||||
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
let mut path = kurbo::BezPath::new();
|
||||
points
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.map(|v: DVec2| kurbo::Point { x: v.x, y: v.y })
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
|
||||
Self {
|
||||
path,
|
||||
style,
|
||||
render_index: 0,
|
||||
solid: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ pub mod layers;
|
|||
pub mod operation;
|
||||
pub mod response;
|
||||
|
||||
pub use intersection::Quad;
|
||||
pub use operation::Operation;
|
||||
pub use response::DocumentResponse;
|
||||
|
||||
|
|
@ -24,4 +25,5 @@ pub enum DocumentError {
|
|||
IndexOutOfBounds,
|
||||
NotAFolder,
|
||||
NonReorderableSelection,
|
||||
NotAShape,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
layers::{style, BlendMode, Layer},
|
||||
|
|
@ -7,7 +12,7 @@ use crate::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Operation {
|
||||
AddEllipse {
|
||||
path: Vec<LayerId>,
|
||||
|
|
@ -21,6 +26,11 @@ pub enum Operation {
|
|||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddBoundingBox {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddLine {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
|
|
@ -38,7 +48,6 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
equal_sides: bool,
|
||||
sides: u8,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
|
|
@ -48,6 +57,10 @@ pub enum Operation {
|
|||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
RenameLayer {
|
||||
path: Vec<LayerId>,
|
||||
name: String,
|
||||
},
|
||||
PasteLayer {
|
||||
layer: Layer,
|
||||
path: Vec<LayerId>,
|
||||
|
|
@ -56,20 +69,32 @@ pub enum Operation {
|
|||
AddFolder {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
MountWorkingFolder {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
TransformLayer {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
TransformLayerInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
TransformLayerInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
SetLayerTransform {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
DiscardWorkingFolder,
|
||||
ClearWorkingFolder,
|
||||
CommitTransaction,
|
||||
ToggleVisibility {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
|
|
@ -86,3 +111,24 @@ pub enum Operation {
|
|||
color: Color,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
/// Returns the byte representation of the message.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory!!!
|
||||
/// Only use if you know what you are doing
|
||||
unsafe fn as_slice(&self) -> &[u8] {
|
||||
core::slice::from_raw_parts(self as *const Operation as *const u8, std::mem::size_of::<Operation>())
|
||||
}
|
||||
/// Returns a pseudo hash that should uniquely identify the operation.
|
||||
/// This is needed because `Hash` is not implemented for f64s
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory but the generated value should be fine.
|
||||
pub fn pseudo_hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
unsafe { self.as_slice() }.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub enum DocumentResponse {
|
|||
FolderChanged { path: Vec<LayerId> },
|
||||
CreatedLayer { path: Vec<LayerId> },
|
||||
DeletedLayer { path: Vec<LayerId> },
|
||||
LayerChanged { path: Vec<LayerId> },
|
||||
}
|
||||
|
||||
impl fmt::Display for DocumentResponse {
|
||||
|
|
@ -17,6 +18,7 @@ impl fmt::Display for DocumentResponse {
|
|||
DocumentResponse::DocumentChanged { .. } => "DocumentChanged",
|
||||
DocumentResponse::FolderChanged { .. } => "FolderChanged",
|
||||
DocumentResponse::CreatedLayer { .. } => "CreatedLayer",
|
||||
DocumentResponse::LayerChanged { .. } => "LayerChanged",
|
||||
DocumentResponse::DeletedLayer { .. } => "DeleteLayer",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ bitflags = "1.2.1"
|
|||
thiserror = "1.0.24"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
graphite-proc-macros = { path = "../proc-macro" }
|
||||
glam = { version="0.16", features = ["serde"] }
|
||||
glam = { version="0.17", features = ["serde"] }
|
||||
|
||||
[dependencies.document-core]
|
||||
path = "../document"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{frontend::FrontendMessageHandler, message_prelude::*, Callback, EditorError};
|
||||
|
||||
pub use crate::document::DocumentMessageHandler;
|
||||
pub use crate::document::DocumentsMessageHandler;
|
||||
pub use crate::input::{InputMapper, InputPreprocessor};
|
||||
pub use crate::tool::ToolMessageHandler;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ pub struct Dispatcher {
|
|||
input_mapper: InputMapper,
|
||||
global_message_handler: GlobalMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
document_message_handler: DocumentMessageHandler,
|
||||
documents_message_handler: DocumentsMessageHandler,
|
||||
messages: VecDeque<Message>,
|
||||
}
|
||||
|
||||
|
|
@ -25,23 +25,23 @@ impl Dispatcher {
|
|||
message,
|
||||
Message::InputPreprocessor(_)
|
||||
| Message::InputMapper(_)
|
||||
| Message::Document(DocumentMessage::RenderDocument)
|
||||
| Message::Documents(DocumentsMessage::Document(DocumentMessage::RenderDocument))
|
||||
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
|
||||
| Message::Frontend(FrontendMessage::SetCanvasZoom { .. })
|
||||
| Message::Frontend(FrontendMessage::SetCanvasRotation { .. })
|
||||
| Message::Document(DocumentMessage::DispatchOperation { .. })
|
||||
| Message::Documents(DocumentsMessage::Document(DocumentMessage::DispatchOperation { .. }))
|
||||
) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove"))
|
||||
{
|
||||
log::trace!("Message: {}", message.to_discriminant().local_name());
|
||||
log::trace!("Hints:{}", self.input_mapper.hints(self.collect_actions()));
|
||||
log::trace!("Message: {:?}", message);
|
||||
//log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions()));
|
||||
}
|
||||
match message {
|
||||
NoOp => (),
|
||||
Document(message) => self.document_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
|
||||
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
|
||||
Global(message) => self.global_message_handler.process_action(message, (), &mut self.messages),
|
||||
Tool(message) => self
|
||||
.tool_message_handler
|
||||
.process_action(message, (&self.document_message_handler.active_document(), &self.input_preprocessor), &mut self.messages),
|
||||
.process_action(message, (self.documents_message_handler.active_document(), &self.input_preprocessor), &mut self.messages),
|
||||
Frontend(message) => self.frontend_message_handler.process_action(message, (), &mut self.messages),
|
||||
InputPreprocessor(message) => self.input_preprocessor.process_action(message, (), &mut self.messages),
|
||||
InputMapper(message) => {
|
||||
|
|
@ -63,7 +63,7 @@ impl Dispatcher {
|
|||
list.extend(self.input_mapper.actions());
|
||||
list.extend(self.global_message_handler.actions());
|
||||
list.extend(self.tool_message_handler.actions());
|
||||
list.extend(self.document_message_handler.actions());
|
||||
list.extend(self.documents_message_handler.actions());
|
||||
list
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ impl Dispatcher {
|
|||
input_preprocessor: InputPreprocessor::default(),
|
||||
global_message_handler: GlobalMessageHandler::new(),
|
||||
input_mapper: InputMapper::default(),
|
||||
document_message_handler: DocumentMessageHandler::default(),
|
||||
documents_message_handler: DocumentsMessageHandler::default(),
|
||||
tool_message_handler: ToolMessageHandler::default(),
|
||||
messages: VecDeque::new(),
|
||||
}
|
||||
|
|
@ -82,12 +82,7 @@ impl Dispatcher {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
communication::DocumentMessageHandler,
|
||||
message_prelude::{DocumentMessage, Message},
|
||||
misc::test_utils::EditorTestUtils,
|
||||
Editor,
|
||||
};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
|
||||
use document_core::{color::Color, Operation};
|
||||
use log::info;
|
||||
|
||||
|
|
@ -123,10 +118,10 @@ mod test {
|
|||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
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();
|
||||
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();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
|
@ -153,14 +148,14 @@ mod test {
|
|||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![shape_id]]))).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![shape_id]])).unwrap();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
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();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
|
@ -190,42 +185,42 @@ mod test {
|
|||
const LINE_INDEX: usize = 0;
|
||||
const PEN_INDEX: usize = 1;
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::AddFolder(vec![]))).unwrap();
|
||||
editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
|
||||
|
||||
let document_before_added_shapes = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
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];
|
||||
|
||||
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
|
||||
// This has not been done yet as the line and pen tool are not yet able to add layers to the currently selected folder
|
||||
editor
|
||||
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddLine {
|
||||
path: vec![folder_id],
|
||||
.handle_message(Operation::AddLine {
|
||||
path: vec![folder_id, LINE_INDEX as u64],
|
||||
insert_index: 0,
|
||||
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
style: Default::default(),
|
||||
})))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
editor
|
||||
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddPen {
|
||||
path: vec![folder_id],
|
||||
.handle_message(Operation::AddPen {
|
||||
path: vec![folder_id, PEN_INDEX as u64],
|
||||
insert_index: 0,
|
||||
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
style: Default::default(),
|
||||
points: vec![(10.0, 20.0), (30.0, 40.0)],
|
||||
})))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![folder_id]]))).unwrap();
|
||||
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![folder_id]])).unwrap();
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).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();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
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();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
|
@ -276,18 +271,18 @@ mod test {
|
|||
const SHAPE_INDEX: usize = 1;
|
||||
const RECT_INDEX: usize = 0;
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
|
||||
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
|
||||
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]]))).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap();
|
||||
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
|
||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
|
||||
editor.draw_rect(0, 800, 12, 200);
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
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();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
|
|
@ -314,18 +309,18 @@ mod test {
|
|||
|
||||
let verify_order = |handler: &mut DocumentMessageHandler| (handler.all_layers_sorted(), handler.non_selected_layers_sorted(), handler.selected_layers_sorted());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![0], vec![2]]))).unwrap();
|
||||
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![0], vec![2]])).unwrap();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(1))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers(1)).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(-1))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers(-1)).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
|
||||
assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(i32::MAX))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers(i32::MAX)).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
use crate::message_prelude::*;
|
||||
use graphite_proc_macros::*;
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
pub trait AsMessage: TransitiveChild
|
||||
where
|
||||
|
|
@ -12,11 +16,11 @@ where
|
|||
}
|
||||
|
||||
#[impl_message]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Message {
|
||||
NoOp,
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
Documents(DocumentsMessage),
|
||||
#[child]
|
||||
Global(GlobalMessage),
|
||||
#[child]
|
||||
|
|
@ -28,3 +32,24 @@ pub enum Message {
|
|||
#[child]
|
||||
InputMapper(InputMapperMessage),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Returns the byte representation of the message.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory!!!
|
||||
/// Only use if you know what you are doing
|
||||
unsafe fn as_slice(&self) -> &[u8] {
|
||||
core::slice::from_raw_parts(self as *const Message as *const u8, std::mem::size_of::<Message>())
|
||||
}
|
||||
/// Returns a pseudo hash that should uniquely identify the message.
|
||||
/// This is needed because `Hash` is not implemented for f64s
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory but the generated value should be fine.
|
||||
pub fn pseudo_hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
unsafe { self.as_slice() }.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub mod dispatcher;
|
||||
pub mod message;
|
||||
use crate::message_prelude::*;
|
||||
|
|
@ -21,3 +24,13 @@ where
|
|||
fn process_action(&mut self, action: A, data: T, responses: &mut VecDeque<Message>);
|
||||
fn actions(&self) -> ActionList;
|
||||
}
|
||||
|
||||
pub fn generate_hash<'a>(messages: impl IntoIterator<Item = &'a Message>, ipp: &InputPreprocessor, document_hash: u64) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
document_hash.hash(&mut s);
|
||||
ipp.hash(&mut s);
|
||||
for message in messages {
|
||||
message.pseudo_hash().hash(&mut s);
|
||||
}
|
||||
s.finish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ pub const MOUSE_ZOOM_RATE: f64 = 1. / 400.;
|
|||
|
||||
pub const ROTATE_SNAP_INTERVAL: f64 = 15.;
|
||||
|
||||
pub const SELECTION_TOLERANCE: f64 = 5.0;
|
||||
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
pub const SELECTION_TOLERANCE: f64 = 1.0;
|
||||
|
|
|
|||
|
|
@ -1,143 +1,501 @@
|
|||
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*, EditorError};
|
||||
use document_core::{document::Document as InternalDocument, layers::Layer, LayerId};
|
||||
pub use super::layer_panel::*;
|
||||
use crate::{frontend::layer_panel::*, EditorError};
|
||||
use document_core::{document::Document as InternalDocument, LayerId};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Document {
|
||||
pub document: InternalDocument,
|
||||
pub name: String,
|
||||
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::message_prelude::*;
|
||||
use document_core::layers::BlendMode;
|
||||
use document_core::{DocumentResponse, Operation as DocumentOperation};
|
||||
use log::warn;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub enum FlipAxis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
impl Default for Document {
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub enum AlignAxis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub enum AlignAggregate {
|
||||
Min,
|
||||
Max,
|
||||
Center,
|
||||
Average,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentMessageHandler {
|
||||
pub document: InternalDocument,
|
||||
pub document_backup: Option<InternalDocument>,
|
||||
pub name: String,
|
||||
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
||||
movement_handler: MovementMessageHandler,
|
||||
}
|
||||
|
||||
impl Default for DocumentMessageHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
document: InternalDocument::default(),
|
||||
document_backup: None,
|
||||
name: String::from("Untitled Document"),
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
#[impl_message(Message, DocumentsMessage, Document)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum DocumentMessage {
|
||||
#[child]
|
||||
Movement(MovementMessage),
|
||||
DispatchOperation(Box<DocumentOperation>),
|
||||
SelectLayers(Vec<Vec<LayerId>>),
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
DeleteLayer(Vec<LayerId>),
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
SetBlendModeForSelectedLayers(BlendMode),
|
||||
SetOpacityForSelectedLayers(f64),
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
FlipSelectedLayers(FlipAxis),
|
||||
ToggleLayerExpansion(Vec<LayerId>),
|
||||
FolderChanged(Vec<LayerId>),
|
||||
StartTransaction,
|
||||
RollbackTransaction,
|
||||
AbortTransaction,
|
||||
CommitTransaction,
|
||||
ExportDocument,
|
||||
RenderDocument,
|
||||
Undo,
|
||||
NudgeSelectedLayers(f64, f64),
|
||||
AlignSelectedLayers(AlignAxis, AlignAggregate),
|
||||
MoveSelectedLayersTo {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
ReorderSelectedLayers(i32), // relative_position,
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for DocumentMessage {
|
||||
fn from(operation: DocumentOperation) -> DocumentMessage {
|
||||
Self::DispatchOperation(Box::new(operation))
|
||||
}
|
||||
}
|
||||
impl From<DocumentOperation> for Message {
|
||||
fn from(operation: DocumentOperation) -> Message {
|
||||
DocumentMessage::DispatchOperation(Box::new(operation)).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentMessageHandler {
|
||||
pub fn active_document(&self) -> &DocumentMessageHandler {
|
||||
self
|
||||
}
|
||||
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
||||
self
|
||||
}
|
||||
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
|
||||
let len = document_responses.len();
|
||||
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
|
||||
document_responses.len() != len
|
||||
}
|
||||
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
|
||||
let _ = self.document.render_root();
|
||||
self.layer_data(&path).expanded.then(|| {
|
||||
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
|
||||
FrontendMessage::ExpandFolder { path, children }.into()
|
||||
})
|
||||
}
|
||||
fn clear_selection(&mut self) {
|
||||
self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
|
||||
}
|
||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||
self.layer_data(path).selected = true;
|
||||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
|
||||
}
|
||||
pub fn layerdata(&self, path: &[LayerId]) -> &LayerData {
|
||||
self.layer_data.get(path).expect("Layerdata does not exist")
|
||||
}
|
||||
pub fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
|
||||
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))
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
|
||||
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
|
||||
// Compute the indices for each layer to be able to sort them
|
||||
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
|
||||
|
||||
.layer_data
|
||||
.iter()
|
||||
// 'path.len() > 0' filters out root layer since it has no indices
|
||||
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
|
||||
.filter_map(|path| {
|
||||
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
|
||||
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
||||
// Once this problem is solved this code can be simplified
|
||||
match self.document.indices_for_path(&path) {
|
||||
Err(err) => {
|
||||
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
|
||||
None
|
||||
}
|
||||
Ok(indices) => Some((path, indices)),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
|
||||
layers_with_indices.into_iter().map(|(path, _)| path).collect()
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order
|
||||
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(None)
|
||||
}
|
||||
|
||||
/// Returns the paths to all selected layers in order
|
||||
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(true))
|
||||
}
|
||||
|
||||
/// Returns the paths to all non_selected layers in order
|
||||
#[allow(dead_code)] // used for test cases
|
||||
pub fn non_selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(false))
|
||||
}
|
||||
pub fn with_name(name: String) -> Self {
|
||||
Self {
|
||||
document: InternalDocument::default(),
|
||||
document_backup: None,
|
||||
name,
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &mut Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||
let blend_mode = layer.blend_mode;
|
||||
let opacity = layer.opacity;
|
||||
let layer_type: LayerType = (&layer.data).into();
|
||||
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
|
||||
let arr = layer.current_bounding_box().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
|
||||
if layer.cache_dirty {
|
||||
layer.render();
|
||||
}
|
||||
|
||||
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
|
||||
format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}</svg>"#,
|
||||
x_min,
|
||||
y_min,
|
||||
x_max - x_min,
|
||||
y_max - y_min,
|
||||
layer.thumbnail_cache.clone()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
LayerPanelEntry {
|
||||
name,
|
||||
visible: layer.visible,
|
||||
blend_mode,
|
||||
opacity,
|
||||
layer_type,
|
||||
layer_data: *layer_data,
|
||||
path,
|
||||
thumbnail,
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn layer_data(&mut self, path: &[LayerId]) -> &mut LayerData {
|
||||
layer_data(&mut self.layer_data, path)
|
||||
}
|
||||
|
||||
pub fn backup(&mut self) {
|
||||
self.document_backup = Some(self.document.clone())
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> Result<(), EditorError> {
|
||||
self.backup();
|
||||
self.reset()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<(), EditorError> {
|
||||
match self.document_backup.take() {
|
||||
Some(backup) => {
|
||||
self.document = backup;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(EditorError::NoTransactionInProgress),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||
self.document.render_root();
|
||||
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);
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
|
||||
/// any actual data, but rather metadata such as visibility and names of the layers.
|
||||
/// any actual data, but ratfolderch as visibility and names of the layers.
|
||||
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
|
||||
let folder = self.document.document_folder_mut(path)?;
|
||||
let ids = folder.as_folder()?.layer_ids.clone();
|
||||
let self_layer_data = &mut self.layer_data;
|
||||
let folder = self.document.folder(path)?;
|
||||
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
|
||||
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
|
||||
let folder = self.document.folder(path)?;
|
||||
let entries = folder
|
||||
.as_folder_mut()?
|
||||
.layers_mut()
|
||||
.iter_mut()
|
||||
.zip(ids)
|
||||
.layers()
|
||||
.iter()
|
||||
.zip(paths.iter().zip(data))
|
||||
.rev()
|
||||
.map(|(layer, id)| {
|
||||
let path = [path, &[id]].concat();
|
||||
layer_panel_entry(layer_data(self_layer_data, &path), layer, path)
|
||||
.map(|(layer, (path, data))| {
|
||||
layer_panel_entry(
|
||||
&data,
|
||||
self.document.generate_transform_across_scope(path, Some(self.document.root.transform.inverse())).unwrap(),
|
||||
layer,
|
||||
path.to_vec(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
|
||||
pub struct LayerData {
|
||||
pub selected: bool,
|
||||
pub expanded: bool,
|
||||
pub translation: DVec2,
|
||||
pub rotation: f64,
|
||||
pub snap_rotate: bool,
|
||||
pub scale: f64,
|
||||
}
|
||||
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
|
||||
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
|
||||
use DocumentMessage::*;
|
||||
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));
|
||||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
||||
}
|
||||
AbortTransaction => {
|
||||
self.reset().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
||||
}
|
||||
CommitTransaction => self.document_backup = None,
|
||||
ExportDocument => {
|
||||
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_size.as_f64()]);
|
||||
let size = bbox[1] - bbox[0];
|
||||
responses.push_back(
|
||||
FrontendMessage::ExportDocument {
|
||||
document: format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}{}</svg>"#,
|
||||
bbox[0].x,
|
||||
bbox[0].y,
|
||||
size.x,
|
||||
size.y,
|
||||
"\n",
|
||||
self.document.render_root()
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
SetBlendModeForSelectedLayers(blend_mode) => {
|
||||
let active_document = self;
|
||||
|
||||
impl LayerData {
|
||||
pub fn new(expanded: bool) -> LayerData {
|
||||
LayerData {
|
||||
selected: false,
|
||||
expanded,
|
||||
translation: DVec2::ZERO,
|
||||
rotation: 0.,
|
||||
snap_rotate: false,
|
||||
scale: 1.,
|
||||
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
}
|
||||
}
|
||||
SetOpacityForSelectedLayers(opacity) => {
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
|
||||
}
|
||||
}
|
||||
ToggleLayerVisibility(path) => {
|
||||
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
|
||||
}
|
||||
ToggleLayerExpansion(path) => {
|
||||
self.layer_data(&path).expanded ^= true;
|
||||
responses.extend(self.handle_folder_changed(path));
|
||||
}
|
||||
DeleteSelectedLayers => {
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
for path in self.selected_layers_sorted() {
|
||||
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
|
||||
}
|
||||
}
|
||||
SelectLayers(paths) => {
|
||||
self.clear_selection();
|
||||
for path in paths {
|
||||
responses.extend(self.select_layer(&path));
|
||||
}
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
responses.extend(self.handle_folder_changed(Vec::new()));
|
||||
}
|
||||
SelectAllLayers => {
|
||||
let all_layer_paths = self.layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::<Vec<_>>();
|
||||
for path in all_layer_paths {
|
||||
responses.extend(self.select_layer(&path));
|
||||
}
|
||||
}
|
||||
DeselectAllLayers => {
|
||||
self.clear_selection();
|
||||
let children = self.layer_panel(&[]).expect("The provided Path was not valid");
|
||||
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
|
||||
}
|
||||
Undo => {
|
||||
// this is a temporary fix and will be addressed by #123
|
||||
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
|
||||
}
|
||||
}
|
||||
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
|
||||
DispatchOperation(op) => match self.document.handle_operation(&op) {
|
||||
Ok(Some(mut document_responses)) => {
|
||||
let canvas_dirty = self.filter_document_responses(&mut document_responses);
|
||||
responses.extend(
|
||||
document_responses
|
||||
.into_iter()
|
||||
.map(|response| match response {
|
||||
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
|
||||
DocumentResponse::DeletedLayer { path } => {
|
||||
self.layer_data.remove(&path);
|
||||
None
|
||||
}
|
||||
DocumentResponse::LayerChanged { path } => Some(
|
||||
FrontendMessage::UpdateLayer {
|
||||
path: path.clone(),
|
||||
data: self.layer_panel_entry(path).unwrap(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
DocumentResponse::CreatedLayer { path } => self.select_layer(&path),
|
||||
DocumentResponse::DocumentChanged => unreachable!(),
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
if canvas_dirty {
|
||||
responses.push_back(RenderDocument.into())
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("DocumentError: {:?}", e),
|
||||
Ok(_) => (),
|
||||
},
|
||||
RenderDocument => responses.push_back(
|
||||
FrontendMessage::UpdateCanvas {
|
||||
document: self.document.render_root(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
for path in self.selected_layers().cloned() {
|
||||
let operation = DocumentOperation::TransformLayerInViewport {
|
||||
path,
|
||||
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
|
||||
};
|
||||
responses.push_back(operation.into());
|
||||
}
|
||||
}
|
||||
MoveSelectedLayersTo { path, insert_index } => {
|
||||
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_position) => {
|
||||
let all_layer_paths = self.all_layers_sorted();
|
||||
let selected_layers = self.selected_layers_sorted();
|
||||
if let Some(pivot) = match relative_position.signum() {
|
||||
-1 => selected_layers.first(),
|
||||
1 => selected_layers.last(),
|
||||
_ => unreachable!(),
|
||||
} {
|
||||
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);
|
||||
if let Some(insert_path) = insert {
|
||||
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
||||
if let Some(folder) = self.document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
|
||||
let selected: Vec<_> = selected_layers
|
||||
.iter()
|
||||
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
||||
.map(|x| x.last().unwrap())
|
||||
.collect();
|
||||
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
|
||||
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
|
||||
let fallback = offset * (non_selected.len());
|
||||
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
|
||||
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FlipSelectedLayers(axis) => {
|
||||
let scale = match axis {
|
||||
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())) {
|
||||
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(),
|
||||
transform: DAffine2::from_scale(scale).to_cols_array(),
|
||||
scope: bbox_trans.to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
AlignSelectedLayers(axis, aggregate) => {
|
||||
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
|
||||
|
||||
let axis = match axis {
|
||||
AlignAxis::X => DVec2::X,
|
||||
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())) {
|
||||
let aggregated = match aggregate {
|
||||
AlignAggregate::Min => combined_box[0],
|
||||
AlignAggregate::Max => combined_box[1],
|
||||
AlignAggregate::Center => lerp(&combined_box),
|
||||
AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(),
|
||||
};
|
||||
for (path, bbox) in paths.into_iter().zip(boxes) {
|
||||
let center = match aggregate {
|
||||
AlignAggregate::Min => bbox[0],
|
||||
AlignAggregate::Max => bbox[1],
|
||||
_ => lerp(&bbox),
|
||||
};
|
||||
let translation = (aggregated - center) * axis;
|
||||
responses.push_back(
|
||||
DocumentOperation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_translation(translation).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
|
||||
}
|
||||
}
|
||||
pub fn snapped_angle(&self) -> f64 {
|
||||
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
|
||||
if self.snap_rotate {
|
||||
(self.rotation / increment_radians).round() * increment_radians
|
||||
} else {
|
||||
self.rotation
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(DocumentMessageDiscriminant;
|
||||
Undo,
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
RenderDocument,
|
||||
ExportDocument,
|
||||
);
|
||||
|
||||
if self.layer_data.values().any(|data| data.selected) {
|
||||
let select = actions!(DocumentMessageDiscriminant;
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
NudgeSelectedLayers,
|
||||
ReorderSelectedLayers,
|
||||
);
|
||||
common.extend(select);
|
||||
}
|
||||
}
|
||||
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
|
||||
let offset_transform = DAffine2::from_translation(offset);
|
||||
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
|
||||
let angle_transform = DAffine2::from_angle(self.snapped_angle());
|
||||
let translation_transform = DAffine2::from_translation(self.translation);
|
||||
scale_transform * offset_transform * angle_transform * scale_transform * translation_transform
|
||||
}
|
||||
pub fn calculate_transform(&self) -> DAffine2 {
|
||||
self.calculate_offset_transform(DVec2::ZERO)
|
||||
common.extend(self.movement_handler.actions());
|
||||
common
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,25 @@
|
|||
use crate::input::InputPreprocessor;
|
||||
use crate::message_prelude::*;
|
||||
use crate::{
|
||||
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
|
||||
input::{mouse::ViewportPosition, InputPreprocessor},
|
||||
};
|
||||
use document_core::layers::BlendMode;
|
||||
use document_core::layers::Layer;
|
||||
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use document_core::{LayerId, Operation as DocumentOperation};
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::document::Document;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::LayerData;
|
||||
use super::DocumentMessageHandler;
|
||||
|
||||
#[impl_message(Message, Document)]
|
||||
#[impl_message(Message, Documents)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum DocumentMessage {
|
||||
DispatchOperation(DocumentOperation),
|
||||
SelectLayers(Vec<Vec<LayerId>>),
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
DeleteLayer(Vec<LayerId>),
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
pub enum DocumentsMessage {
|
||||
CopySelectedLayers,
|
||||
SetBlendModeForSelectedLayers(BlendMode),
|
||||
SetOpacityForSelectedLayers(f64),
|
||||
PasteLayers { path: Vec<LayerId>, insert_index: isize },
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
ToggleLayerExpansion(Vec<LayerId>),
|
||||
PasteLayers {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
SelectDocument(usize),
|
||||
CloseDocument(usize),
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
CloseActiveDocumentWithConfirmation,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseAllDocuments,
|
||||
|
|
@ -42,187 +27,40 @@ pub enum DocumentMessage {
|
|||
GetOpenDocumentsList,
|
||||
NextDocument,
|
||||
PrevDocument,
|
||||
ExportDocument,
|
||||
RenderDocument,
|
||||
Undo,
|
||||
MouseMove,
|
||||
TranslateCanvasBegin,
|
||||
WheelCanvasTranslate { use_y_as_x: bool },
|
||||
RotateCanvasBegin { snap: bool },
|
||||
EnableSnapping,
|
||||
DisableSnapping,
|
||||
ZoomCanvasBegin,
|
||||
TranslateCanvasEnd,
|
||||
SetCanvasZoom(f64),
|
||||
MultiplyCanvasZoom(f64),
|
||||
WheelCanvasZoom,
|
||||
SetCanvasRotation(f64),
|
||||
NudgeSelectedLayers(f64, f64),
|
||||
FlipSelectedLayers(FlipAxis),
|
||||
AlignSelectedLayers(AlignAxis, AlignAggregate),
|
||||
DragLayer(Vec<LayerId>, DVec2),
|
||||
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
|
||||
ReorderSelectedLayers(i32), // relative_position,
|
||||
SetLayerTranslation(Vec<LayerId>, Option<f64>, Option<f64>),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum FlipAxis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum AlignAxis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum AlignAggregate {
|
||||
Min,
|
||||
Max,
|
||||
Center,
|
||||
Average,
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for DocumentMessage {
|
||||
fn from(operation: DocumentOperation) -> DocumentMessage {
|
||||
Self::DispatchOperation(operation)
|
||||
}
|
||||
}
|
||||
impl From<DocumentOperation> for Message {
|
||||
fn from(operation: DocumentOperation) -> Message {
|
||||
DocumentMessage::DispatchOperation(operation).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentMessageHandler {
|
||||
documents: Vec<Document>,
|
||||
pub struct DocumentsMessageHandler {
|
||||
documents: Vec<DocumentMessageHandler>,
|
||||
active_document_index: usize,
|
||||
translating: bool,
|
||||
rotating: bool,
|
||||
zooming: bool,
|
||||
snapping: bool,
|
||||
mouse_pos: ViewportPosition,
|
||||
copy_buffer: Vec<Layer>,
|
||||
}
|
||||
|
||||
impl DocumentMessageHandler {
|
||||
pub fn active_document(&self) -> &Document {
|
||||
impl DocumentsMessageHandler {
|
||||
pub fn active_document(&self) -> &DocumentMessageHandler {
|
||||
&self.documents[self.active_document_index]
|
||||
}
|
||||
pub fn active_document_mut(&mut self) -> &mut Document {
|
||||
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
||||
&mut self.documents[self.active_document_index]
|
||||
}
|
||||
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
|
||||
let len = document_responses.len();
|
||||
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
|
||||
document_responses.len() != len
|
||||
}
|
||||
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
|
||||
let document = self.active_document_mut();
|
||||
document.layer_data(&path).expanded.then(|| {
|
||||
let children = document.layer_panel(path.as_slice()).expect("The provided Path was not valid");
|
||||
FrontendMessage::ExpandFolder { path, children }.into()
|
||||
})
|
||||
}
|
||||
fn clear_selection(&mut self) {
|
||||
self.active_document_mut().layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
|
||||
}
|
||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||
self.active_document_mut().layer_data(&path).selected = true;
|
||||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
|
||||
}
|
||||
fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
|
||||
self.active_document().layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
|
||||
}
|
||||
fn layerdata(&self, path: &[LayerId]) -> &LayerData {
|
||||
self.active_document().layer_data.get(path).expect("Layerdata does not exist")
|
||||
}
|
||||
fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
|
||||
self.active_document_mut().layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
|
||||
}
|
||||
fn create_document_transform_from_layerdata(&self, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
|
||||
let half_viewport = viewport_size.as_dvec2() / 2.;
|
||||
let layerdata = self.layerdata(&[]);
|
||||
let scaled_half_viewport = half_viewport / layerdata.scale;
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path: vec![],
|
||||
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
|
||||
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
|
||||
// Compute the indices for each layer to be able to sort them
|
||||
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
|
||||
.active_document()
|
||||
.layer_data
|
||||
.iter()
|
||||
// 'path.len() > 0' filters out root layer since it has no indices
|
||||
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
|
||||
.filter_map(|path| {
|
||||
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
|
||||
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
||||
// Once this problem is solved this code can be simplified
|
||||
match self.active_document().document.indices_for_path(&path) {
|
||||
Err(err) => {
|
||||
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
|
||||
None
|
||||
}
|
||||
Ok(indices) => Some((path, indices)),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
|
||||
layers_with_indices.into_iter().map(|(path, _)| path).collect()
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order
|
||||
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(None)
|
||||
}
|
||||
|
||||
/// Returns the paths to all selected layers in order
|
||||
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(true))
|
||||
}
|
||||
|
||||
/// Returns the paths to all non_selected layers in order
|
||||
#[allow(dead_code)] // used for test cases
|
||||
pub fn non_selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DocumentMessageHandler {
|
||||
impl Default for DocumentsMessageHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
documents: vec![Document::default()],
|
||||
documents: vec![DocumentMessageHandler::default()],
|
||||
active_document_index: 0,
|
||||
translating: false,
|
||||
rotating: false,
|
||||
zooming: false,
|
||||
snapping: false,
|
||||
mouse_pos: ViewportPosition::default(),
|
||||
copy_buffer: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
|
||||
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
|
||||
impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHandler {
|
||||
fn process_action(&mut self, message: DocumentsMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
|
||||
use DocumentMessage::*;
|
||||
use DocumentsMessage::*;
|
||||
match message {
|
||||
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
|
||||
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
|
||||
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
|
||||
SelectDocument(id) => {
|
||||
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
|
||||
self.active_document_index = id;
|
||||
|
|
@ -250,7 +88,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
self.documents.clear();
|
||||
|
||||
// Create a new blank document
|
||||
responses.push_back(DocumentMessage::NewDocument.into());
|
||||
responses.push_back(NewDocument.into());
|
||||
}
|
||||
CloseDocument(id) => {
|
||||
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
|
||||
|
|
@ -264,7 +102,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
// Last tab was closed, so create a new blank tab
|
||||
if self.documents.is_empty() {
|
||||
self.active_document_index = 0;
|
||||
responses.push_back(DocumentMessage::NewDocument.into());
|
||||
responses.push_back(NewDocument.into());
|
||||
}
|
||||
// The currently selected doc is being closed
|
||||
else if id == self.active_document_index {
|
||||
|
|
@ -327,7 +165,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
};
|
||||
|
||||
self.active_document_index = self.documents.len();
|
||||
let new_document = Document::with_name(name);
|
||||
let new_document = DocumentMessageHandler::with_name(name);
|
||||
self.documents.push(new_document);
|
||||
|
||||
// Send the new list of document tab names
|
||||
|
|
@ -356,48 +194,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
let id = (self.active_document_index + self.documents.len() - 1) % self.documents.len();
|
||||
responses.push_back(SelectDocument(id).into());
|
||||
}
|
||||
ExportDocument => responses.push_back(
|
||||
FrontendMessage::ExportDocument {
|
||||
//TODO: Add canvas size instead of using 1920x1080 by default
|
||||
document: format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">{}{}</svg>"#,
|
||||
"\n",
|
||||
self.active_document_mut().document.render_root(),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
SetBlendModeForSelectedLayers(blend_mode) => {
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
}
|
||||
}
|
||||
SetOpacityForSelectedLayers(opacity) => {
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
|
||||
}
|
||||
}
|
||||
ToggleLayerVisibility(path) => {
|
||||
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
|
||||
}
|
||||
ToggleLayerExpansion(path) => {
|
||||
self.active_document_mut().layer_data(&path).expanded ^= true;
|
||||
responses.extend(self.handle_folder_changed(path));
|
||||
}
|
||||
DeleteSelectedLayers => {
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
for path in self.selected_layers_sorted() {
|
||||
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
|
||||
}
|
||||
}
|
||||
CopySelectedLayers => {
|
||||
let paths = self.selected_layers_sorted();
|
||||
let paths = self.active_document().selected_layers_sorted();
|
||||
self.copy_buffer.clear();
|
||||
for path in paths {
|
||||
match self.active_document().document.layer(&path).map(|t| t.clone()) {
|
||||
|
|
@ -430,405 +228,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
}
|
||||
SelectLayers(paths) => {
|
||||
self.clear_selection();
|
||||
for path in paths {
|
||||
responses.extend(self.select_layer(&path));
|
||||
}
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
responses.extend(self.handle_folder_changed(Vec::new()));
|
||||
}
|
||||
SelectAllLayers => {
|
||||
let all_layer_paths = self.active_document().layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::<Vec<_>>();
|
||||
for path in all_layer_paths {
|
||||
responses.extend(self.select_layer(&path));
|
||||
}
|
||||
}
|
||||
DeselectAllLayers => {
|
||||
self.clear_selection();
|
||||
let children = self.active_document_mut().layer_panel(&[]).expect("The provided Path was not valid");
|
||||
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
|
||||
}
|
||||
Undo => {
|
||||
// this is a temporary fix and will be addressed by #123
|
||||
if let Some(id) = self.active_document().document.root.as_folder().unwrap().list_layers().last() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
|
||||
}
|
||||
}
|
||||
DispatchOperation(op) => {
|
||||
if let Ok(Some(mut document_responses)) = self.active_document_mut().document.handle_operation(op) {
|
||||
let canvas_dirty = self.filter_document_responses(&mut document_responses);
|
||||
responses.extend(
|
||||
document_responses
|
||||
.into_iter()
|
||||
.map(|response| match response {
|
||||
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
|
||||
DocumentResponse::DeletedLayer { path } => {
|
||||
self.active_document_mut().layer_data.remove(&path);
|
||||
None
|
||||
}
|
||||
DocumentResponse::CreatedLayer { path } => {
|
||||
if !self.active_document().document.work_mounted {
|
||||
self.select_layer(&path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
DocumentResponse::DocumentChanged => unreachable!(),
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
if canvas_dirty {
|
||||
responses.push_back(RenderDocument.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
RenderDocument => responses.push_back(
|
||||
FrontendMessage::UpdateCanvas {
|
||||
document: self.active_document_mut().document.render_root(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
TranslateCanvasBegin => {
|
||||
self.translating = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
|
||||
RotateCanvasBegin { snap } => {
|
||||
self.rotating = true;
|
||||
self.snapping = snap;
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.snap_rotate = snap;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
EnableSnapping => self.snapping = true,
|
||||
DisableSnapping => self.snapping = false,
|
||||
ZoomCanvasBegin => {
|
||||
self.zooming = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
TranslateCanvasEnd => {
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.rotation = layerdata.snapped_angle();
|
||||
layerdata.snap_rotate = false;
|
||||
self.translating = false;
|
||||
self.rotating = false;
|
||||
self.zooming = false;
|
||||
}
|
||||
MouseMove => {
|
||||
if self.translating {
|
||||
let delta = ipp.mouse.position.as_dvec2() - self.mouse_pos.as_dvec2();
|
||||
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
|
||||
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.translation += transformed_delta;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
if self.rotating {
|
||||
let half_viewport = ipp.viewport_size.as_dvec2() / 2.;
|
||||
let rotation = {
|
||||
let start_vec = self.mouse_pos.as_dvec2() - half_viewport;
|
||||
let end_vec = ipp.mouse.position.as_dvec2() - half_viewport;
|
||||
start_vec.angle_between(end_vec)
|
||||
};
|
||||
|
||||
let snapping = self.snapping;
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.rotation += rotation;
|
||||
layerdata.snap_rotate = snapping;
|
||||
responses.push_back(
|
||||
FrontendMessage::SetCanvasRotation {
|
||||
new_radians: layerdata.snapped_angle(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
if self.zooming {
|
||||
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
|
||||
let amount = 1. + difference * MOUSE_ZOOM_RATE;
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
let new = (layerdata.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
SetCanvasZoom(new) => {
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
MultiplyCanvasZoom(multiplier) => {
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
let new = (layerdata.scale * multiplier).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
WheelCanvasZoom => {
|
||||
let scroll = ipp.mouse.scroll_delta.scroll_delta();
|
||||
let mouse = ipp.mouse.position.as_dvec2();
|
||||
let viewport_size = ipp.viewport_size.as_dvec2();
|
||||
let mut zoom_factor = 1. + scroll.abs() * WHEEL_ZOOM_RATE;
|
||||
if ipp.mouse.scroll_delta.y > 0 {
|
||||
zoom_factor = 1. / zoom_factor
|
||||
};
|
||||
let new_viewport_size = viewport_size * (1. / zoom_factor);
|
||||
let delta_size = viewport_size - new_viewport_size;
|
||||
let mouse_percent = mouse / viewport_size;
|
||||
let delta = delta_size * -2. * (mouse_percent - (0.5, 0.5).into());
|
||||
|
||||
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
let new = (layerdata.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
layerdata.translation += transformed_delta;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
WheelCanvasTranslate { use_y_as_x } => {
|
||||
let delta = match use_y_as_x {
|
||||
false => -ipp.mouse.scroll_delta.as_dvec2(),
|
||||
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
|
||||
} * VIEWPORT_SCROLL_RATE;
|
||||
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.translation += transformed_delta;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
}
|
||||
SetCanvasRotation(new) => {
|
||||
let layerdata = self.layerdata_mut(&[]);
|
||||
layerdata.rotation = new;
|
||||
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
|
||||
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
|
||||
}
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
let delta = {
|
||||
let root_layer_rotation = self.layerdata_mut(&[]).rotation;
|
||||
let rotate_to_viewport_space = DAffine2::from_angle(root_layer_rotation).inverse();
|
||||
rotate_to_viewport_space.transform_point2((x, y).into())
|
||||
};
|
||||
for path in self.selected_layers().cloned() {
|
||||
let operation = DocumentOperation::TransformLayer {
|
||||
path,
|
||||
transform: DAffine2::from_translation(delta).to_cols_array(),
|
||||
};
|
||||
responses.push_back(operation.into());
|
||||
}
|
||||
}
|
||||
MoveSelectedLayersTo { path, insert_index } => {
|
||||
responses.push_back(DocumentMessage::CopySelectedLayers.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(DocumentMessage::PasteLayers { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_position) => {
|
||||
let all_layer_paths = self.all_layers_sorted();
|
||||
let selected_layers = self.selected_layers_sorted();
|
||||
if let Some(pivot) = match relative_position.signum() {
|
||||
-1 => selected_layers.first(),
|
||||
1 => selected_layers.last(),
|
||||
_ => unreachable!(),
|
||||
} {
|
||||
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);
|
||||
if let Some(insert_path) = insert {
|
||||
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
||||
if let Some(folder) = self.active_document().document.document_layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
|
||||
let selected: Vec<_> = selected_layers
|
||||
.iter()
|
||||
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
||||
.map(|x| x.last().unwrap())
|
||||
.collect();
|
||||
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
|
||||
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
|
||||
let fallback = offset * (non_selected.len());
|
||||
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
|
||||
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FlipSelectedLayers(axis) => {
|
||||
// TODO: Handle folder nested transforms with the transforms API
|
||||
let selected_paths = self.selected_layers_sorted();
|
||||
if selected_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_layers = selected_paths.iter().filter_map(|path| {
|
||||
let layer = self.active_document().document.layer(path).ok()?;
|
||||
// TODO: Refactor with `reduce` and `merge_bounding_boxes` once the latter is added
|
||||
let (min, max) = {
|
||||
let bounding_box = layer.bounding_box(layer.transform, layer.style)?;
|
||||
match axis {
|
||||
FlipAxis::X => (bounding_box[0].x, bounding_box[1].x),
|
||||
FlipAxis::Y => (bounding_box[0].y, bounding_box[1].y),
|
||||
}
|
||||
};
|
||||
Some((path.clone(), (min, max)))
|
||||
});
|
||||
|
||||
let (min, max) = selected_layers
|
||||
.clone()
|
||||
.map(|(_, extrema)| extrema)
|
||||
.reduce(|(min_a, max_a), (min_b, max_b)| (min_a.min(min_b), max_a.max(max_b)))
|
||||
.unwrap();
|
||||
let middle = (min + max) / 2.;
|
||||
|
||||
for (path, _) in selected_layers {
|
||||
let layer = self.active_document().document.layer(&path).unwrap();
|
||||
let mut transform = layer.transform;
|
||||
let scale = match axis {
|
||||
FlipAxis::X => DVec2::new(-1., 1.),
|
||||
FlipAxis::Y => DVec2::new(1., -1.),
|
||||
};
|
||||
transform = transform * DAffine2::from_scale(scale);
|
||||
|
||||
let coord = match axis {
|
||||
FlipAxis::X => &mut transform.translation.x,
|
||||
FlipAxis::Y => &mut transform.translation.y,
|
||||
};
|
||||
*coord = *coord - 2. * (*coord - middle);
|
||||
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path,
|
||||
transform: transform.to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
AlignSelectedLayers(axis, aggregate) => {
|
||||
// TODO: Handle folder nested transforms with the transforms API
|
||||
if self.selected_layers().next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_layers = self.selected_layers().cloned().filter_map(|path| {
|
||||
let layer = self.active_document().document.layer(&path).ok()?;
|
||||
let point = {
|
||||
let bounding_box = layer.bounding_box(layer.transform, layer.style)?;
|
||||
match aggregate {
|
||||
AlignAggregate::Min => bounding_box[0],
|
||||
AlignAggregate::Max => bounding_box[1],
|
||||
AlignAggregate::Center => bounding_box[0].lerp(bounding_box[1], 0.5),
|
||||
AlignAggregate::Average => bounding_box[0].lerp(bounding_box[1], 0.5),
|
||||
}
|
||||
};
|
||||
let (bounding_box_coord, translation_coord) = match axis {
|
||||
AlignAxis::X => (point.x, layer.transform.translation.x),
|
||||
AlignAxis::Y => (point.y, layer.transform.translation.y),
|
||||
};
|
||||
Some((path, bounding_box_coord, translation_coord))
|
||||
});
|
||||
let selected_layers: Vec<_> = selected_layers.collect();
|
||||
|
||||
let bounding_box_coords = selected_layers.iter().map(|(_, bounding_box_coord, _)| bounding_box_coord).cloned();
|
||||
if let Some(aggregated_coord) = match aggregate {
|
||||
AlignAggregate::Min => bounding_box_coords.reduce(|a, b| a.min(b)),
|
||||
AlignAggregate::Max => bounding_box_coords.reduce(|a, b| a.max(b)),
|
||||
AlignAggregate::Center => {
|
||||
// TODO: Refactor with `reduce` and `merge_bounding_boxes` once the latter is added
|
||||
self.selected_layers()
|
||||
.filter_map(|path| self.active_document().document.layer(path).ok().map(|layer| layer.bounding_box(layer.transform, layer.style)).flatten())
|
||||
.map(|bbox| match axis {
|
||||
AlignAxis::X => (bbox[0].x, bbox[1].x),
|
||||
AlignAxis::Y => (bbox[0].y, bbox[1].y),
|
||||
})
|
||||
.reduce(|(a, b), (c, d)| (a.min(c), b.max(d)))
|
||||
.map(|(min, max)| (min + max) / 2.)
|
||||
}
|
||||
AlignAggregate::Average => Some(bounding_box_coords.sum::<f64>() / selected_layers.len() as f64),
|
||||
} {
|
||||
for (path, bounding_box_coord, translation_coord) in selected_layers {
|
||||
let new_coord = aggregated_coord - (bounding_box_coord - translation_coord);
|
||||
match axis {
|
||||
AlignAxis::X => responses.push_back(DocumentMessage::SetLayerTranslation(path, Some(new_coord), None).into()),
|
||||
AlignAxis::Y => responses.push_back(DocumentMessage::SetLayerTranslation(path, None, Some(new_coord)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DragLayer(path, offset) => {
|
||||
// TODO: Replace root transformations with functions of the transform api
|
||||
// and do the same with all instances of `root.transform.inverse()` in other messages
|
||||
let transformed_mouse_pos = self.active_document().document.root.transform.inverse().transform_vector2(ipp.mouse.position.as_dvec2());
|
||||
let translation = offset + transformed_mouse_pos;
|
||||
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
|
||||
let transform = {
|
||||
let mut transform = layer.transform;
|
||||
transform.translation = translation;
|
||||
transform.to_cols_array()
|
||||
};
|
||||
responses.push_back(DocumentOperation::SetLayerTransform { path, transform }.into());
|
||||
}
|
||||
}
|
||||
SetLayerTranslation(path, x_option, y_option) => {
|
||||
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
|
||||
let mut transform = layer.transform;
|
||||
transform.translation = DVec2::new(x_option.unwrap_or(transform.translation.x), y_option.unwrap_or(transform.translation.y));
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path,
|
||||
transform: transform.to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
|
||||
}
|
||||
}
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(DocumentMessageDiscriminant;
|
||||
Undo,
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
RenderDocument,
|
||||
ExportDocument,
|
||||
let mut common = actions!(DocumentsMessageDiscriminant;
|
||||
NewDocument,
|
||||
CloseActiveDocumentWithConfirmation,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseAllDocuments,
|
||||
NextDocument,
|
||||
PrevDocument,
|
||||
MouseMove,
|
||||
TranslateCanvasEnd,
|
||||
TranslateCanvasBegin,
|
||||
PasteLayers,
|
||||
RotateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
SetCanvasZoom,
|
||||
MultiplyCanvasZoom,
|
||||
SetCanvasRotation,
|
||||
WheelCanvasZoom,
|
||||
WheelCanvasTranslate,
|
||||
);
|
||||
|
||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||
let select = actions!(DocumentMessageDiscriminant;
|
||||
DeleteSelectedLayers,
|
||||
DuplicateSelectedLayers,
|
||||
let select = actions!(DocumentsMessageDiscriminant;
|
||||
CopySelectedLayers,
|
||||
NudgeSelectedLayers,
|
||||
ReorderSelectedLayers,
|
||||
);
|
||||
common.extend(select);
|
||||
}
|
||||
if self.rotating {
|
||||
let snapping = actions!(DocumentMessageDiscriminant;
|
||||
EnableSnapping,
|
||||
DisableSnapping,
|
||||
);
|
||||
common.extend(snapping);
|
||||
}
|
||||
common.extend(self.active_document().actions());
|
||||
common
|
||||
}
|
||||
}
|
||||
|
|
|
|||
94
core/editor/src/document/layer_panel.rs
Normal file
94
core/editor/src/document/layer_panel.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*};
|
||||
use document_core::{
|
||||
layers::{Layer, LayerData as DocumentLayerData},
|
||||
LayerId,
|
||||
};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
|
||||
pub struct LayerData {
|
||||
pub selected: bool,
|
||||
pub expanded: bool,
|
||||
pub translation: DVec2,
|
||||
pub rotation: f64,
|
||||
pub snap_rotate: bool,
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl LayerData {
|
||||
pub fn new(expanded: bool) -> LayerData {
|
||||
LayerData {
|
||||
selected: false,
|
||||
expanded,
|
||||
translation: DVec2::ZERO,
|
||||
rotation: 0.,
|
||||
snap_rotate: false,
|
||||
scale: 1.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapped_angle(&self) -> f64 {
|
||||
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
|
||||
if self.snap_rotate {
|
||||
(self.rotation / increment_radians).round() * increment_radians
|
||||
} else {
|
||||
self.rotation
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
|
||||
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
|
||||
let offset_transform = DAffine2::from_translation(offset);
|
||||
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
|
||||
let angle_transform = DAffine2::from_angle(self.snapped_angle());
|
||||
let translation_transform = DAffine2::from_translation(self.translation);
|
||||
scale_transform * offset_transform * angle_transform * translation_transform
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||
let layer_type: LayerType = (&layer.data).into();
|
||||
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
|
||||
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
|
||||
let mut thumbnail = String::new();
|
||||
layer.data.clone().render(&mut thumbnail, &mut vec![transform]);
|
||||
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
|
||||
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
|
||||
format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><g transform="matrix({})">{}</g></svg>"#,
|
||||
x_min,
|
||||
y_min,
|
||||
x_max - x_min,
|
||||
y_max - y_min,
|
||||
transform,
|
||||
thumbnail,
|
||||
)
|
||||
} else {
|
||||
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,
|
||||
blend_mode: layer.blend_mode,
|
||||
opacity: layer.opacity,
|
||||
layer_type: (&layer.data).into(),
|
||||
layer_data: *layer_data,
|
||||
path,
|
||||
thumbnail,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
mod document_file;
|
||||
mod document_message_handler;
|
||||
mod layer_panel;
|
||||
mod movement_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use document_file::{Document, LayerData};
|
||||
pub use document_file::LayerData;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use document_message_handler::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis};
|
||||
pub use document_file::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis};
|
||||
#[doc(inline)]
|
||||
pub use document_message_handler::{DocumentsMessage, DocumentsMessageDiscriminant, DocumentsMessageHandler};
|
||||
#[doc(inline)]
|
||||
pub use movement_handler::{MovementMessage, MovementMessageDiscriminant};
|
||||
|
|
|
|||
214
core/editor/src/document/movement_handler.rs
Normal file
214
core/editor/src/document/movement_handler.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
pub use super::layer_panel::*;
|
||||
|
||||
use super::LayerData;
|
||||
|
||||
use crate::message_prelude::*;
|
||||
use crate::{
|
||||
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
|
||||
input::{mouse::ViewportPosition, InputPreprocessor},
|
||||
};
|
||||
use document_core::document::Document;
|
||||
use document_core::Operation as DocumentOperation;
|
||||
use glam::DVec2;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[impl_message(Message, DocumentMessage, Movement)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum MovementMessage {
|
||||
MouseMove,
|
||||
TranslateCanvasBegin,
|
||||
WheelCanvasTranslate { use_y_as_x: bool },
|
||||
RotateCanvasBegin { snap: bool },
|
||||
EnableSnapping,
|
||||
DisableSnapping,
|
||||
ZoomCanvasBegin,
|
||||
TranslateCanvasEnd,
|
||||
SetCanvasZoom(f64),
|
||||
MultiplyCanvasZoom(f64),
|
||||
WheelCanvasZoom,
|
||||
SetCanvasRotation(f64),
|
||||
ZoomCanvasToFitAll,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, Default, PartialEq)]
|
||||
pub struct MovementMessageHandler {
|
||||
translating: bool,
|
||||
rotating: bool,
|
||||
zooming: bool,
|
||||
snapping: bool,
|
||||
mouse_pos: ViewportPosition,
|
||||
}
|
||||
|
||||
impl MovementMessageHandler {
|
||||
fn create_document_transform_from_layerdata(&self, layerdata: &LayerData, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
|
||||
let half_viewport = viewport_size.as_f64() / 2.;
|
||||
let scaled_half_viewport = half_viewport / layerdata.scale;
|
||||
responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path: vec![],
|
||||
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreprocessor)> for MovementMessageHandler {
|
||||
fn process_action(&mut self, message: MovementMessage, data: (&mut LayerData, &Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
|
||||
let (layerdata, document, ipp) = data;
|
||||
use MovementMessage::*;
|
||||
match message {
|
||||
TranslateCanvasBegin => {
|
||||
self.translating = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
|
||||
RotateCanvasBegin { snap } => {
|
||||
self.rotating = true;
|
||||
self.snapping = snap;
|
||||
layerdata.snap_rotate = snap;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
EnableSnapping => self.snapping = true,
|
||||
DisableSnapping => self.snapping = false,
|
||||
ZoomCanvasBegin => {
|
||||
self.zooming = true;
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
TranslateCanvasEnd => {
|
||||
layerdata.rotation = layerdata.snapped_angle();
|
||||
layerdata.snap_rotate = false;
|
||||
self.translating = false;
|
||||
self.rotating = false;
|
||||
self.zooming = false;
|
||||
}
|
||||
MouseMove => {
|
||||
if self.translating {
|
||||
let delta = ipp.mouse.position.as_f64() - self.mouse_pos.as_f64();
|
||||
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
|
||||
|
||||
layerdata.translation += transformed_delta;
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
if self.rotating {
|
||||
let half_viewport = ipp.viewport_size.as_f64() / 2.;
|
||||
let rotation = {
|
||||
let start_vec = self.mouse_pos.as_f64() - half_viewport;
|
||||
let end_vec = ipp.mouse.position.as_f64() - half_viewport;
|
||||
start_vec.angle_between(end_vec)
|
||||
};
|
||||
|
||||
let snapping = self.snapping;
|
||||
|
||||
layerdata.rotation += rotation;
|
||||
layerdata.snap_rotate = snapping;
|
||||
responses.push_back(
|
||||
FrontendMessage::SetCanvasRotation {
|
||||
new_radians: layerdata.snapped_angle(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
if self.zooming {
|
||||
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
|
||||
let amount = 1. + difference * MOUSE_ZOOM_RATE;
|
||||
|
||||
let new = (layerdata.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
self.mouse_pos = ipp.mouse.position;
|
||||
}
|
||||
SetCanvasZoom(new) => {
|
||||
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
MultiplyCanvasZoom(multiplier) => {
|
||||
let new = (layerdata.scale * multiplier).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
WheelCanvasZoom => {
|
||||
let scroll = ipp.mouse.scroll_delta.scroll_delta();
|
||||
let mouse = ipp.mouse.position.as_f64();
|
||||
let viewport_size = ipp.viewport_size.as_f64();
|
||||
let mut zoom_factor = 1. + scroll.abs() * WHEEL_ZOOM_RATE;
|
||||
if ipp.mouse.scroll_delta.y > 0 {
|
||||
zoom_factor = 1. / zoom_factor
|
||||
};
|
||||
let new_viewport_size = viewport_size * (1. / zoom_factor);
|
||||
let delta_size = viewport_size - new_viewport_size;
|
||||
let mouse_percent = mouse / viewport_size;
|
||||
let delta = (delta_size * -2.) * (mouse_percent - DVec2::splat(0.5));
|
||||
|
||||
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
|
||||
let new = (layerdata.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
layerdata.scale = new;
|
||||
layerdata.translation += transformed_delta;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
WheelCanvasTranslate { use_y_as_x } => {
|
||||
let delta = match use_y_as_x {
|
||||
false => -ipp.mouse.scroll_delta.as_dvec2(),
|
||||
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
|
||||
} * VIEWPORT_SCROLL_RATE;
|
||||
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
|
||||
layerdata.translation += transformed_delta;
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
SetCanvasRotation(new) => {
|
||||
layerdata.rotation = new;
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
|
||||
}
|
||||
ZoomCanvasToFitAll => {
|
||||
if let Some([pos1, pos2]) = document.visible_layers_bounding_box() {
|
||||
let pos1 = document.root.transform.inverse().transform_point2(pos1);
|
||||
let pos2 = document.root.transform.inverse().transform_point2(pos2);
|
||||
let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
|
||||
let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_size.as_f64());
|
||||
|
||||
let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
|
||||
let size = (pos2 - pos1) / (v2 - v1);
|
||||
let size = 1. / size;
|
||||
let new_scale = size.min_element();
|
||||
|
||||
layerdata.translation += center;
|
||||
layerdata.scale *= new_scale;
|
||||
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
|
||||
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(MovementMessageDiscriminant;
|
||||
MouseMove,
|
||||
TranslateCanvasEnd,
|
||||
TranslateCanvasBegin,
|
||||
RotateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
SetCanvasZoom,
|
||||
MultiplyCanvasZoom,
|
||||
SetCanvasRotation,
|
||||
WheelCanvasZoom,
|
||||
WheelCanvasTranslate,
|
||||
ZoomCanvasToFitAll,
|
||||
);
|
||||
|
||||
if self.rotating {
|
||||
let snapping = actions!(MovementMessageDiscriminant;
|
||||
EnableSnapping,
|
||||
DisableSnapping,
|
||||
);
|
||||
common.extend(snapping);
|
||||
}
|
||||
common
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ pub enum FrontendMessage {
|
|||
DisplayConfirmationToCloseDocument { document_index: usize },
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
UpdateCanvas { document: String },
|
||||
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
|
||||
ExportDocument { document: String },
|
||||
EnableTextInput,
|
||||
DisableTextInput,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
use crate::document::LayerData;
|
||||
use document_core::{
|
||||
layers::{BlendMode, LayerDataTypes},
|
||||
LayerId,
|
||||
};
|
||||
use document_core::layers::{BlendMode, LayerDataType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
|
|
@ -14,7 +11,8 @@ pub struct LayerPanelEntry {
|
|||
pub opacity: f64,
|
||||
pub layer_type: LayerType,
|
||||
pub layer_data: LayerData,
|
||||
pub path: Vec<LayerId>,
|
||||
// TODO: instead of turning the u64 into (u32, u32)s here, do that in the wasm wrapper
|
||||
pub path: Vec<(u32, u32)>,
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
||||
|
|
@ -22,11 +20,6 @@ pub struct LayerPanelEntry {
|
|||
pub enum LayerType {
|
||||
Folder,
|
||||
Shape,
|
||||
Circle,
|
||||
Rect,
|
||||
Line,
|
||||
PolyLine,
|
||||
Ellipse,
|
||||
}
|
||||
|
||||
impl fmt::Display for LayerType {
|
||||
|
|
@ -34,27 +27,18 @@ impl fmt::Display for LayerType {
|
|||
let name = match self {
|
||||
LayerType::Folder => "Folder",
|
||||
LayerType::Shape => "Shape",
|
||||
LayerType::Rect => "Rectangle",
|
||||
LayerType::Line => "Line",
|
||||
LayerType::Circle => "Circle",
|
||||
LayerType::PolyLine => "Polyline",
|
||||
LayerType::Ellipse => "Ellipse",
|
||||
};
|
||||
|
||||
formatter.write_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LayerDataTypes> for LayerType {
|
||||
fn from(data: &LayerDataTypes) -> Self {
|
||||
use LayerDataTypes::*;
|
||||
impl From<&LayerDataType> for LayerType {
|
||||
fn from(data: &LayerDataType) -> Self {
|
||||
use LayerDataType::*;
|
||||
match data {
|
||||
Folder(_) => LayerType::Folder,
|
||||
Shape(_) => LayerType::Shape,
|
||||
Rect(_) => LayerType::Rect,
|
||||
Line(_) => LayerType::Line,
|
||||
PolyLine(_) => LayerType::PolyLine,
|
||||
Ellipse(_) => LayerType::Ellipse,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::message_prelude::*;
|
|||
use std::collections::VecDeque;
|
||||
|
||||
#[impl_message(Message, Global)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum GlobalMessage {
|
||||
LogInfo,
|
||||
LogDebug,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const NUDGE_AMOUNT: f64 = 1.;
|
|||
const SHIFT_NUDGE_AMOUNT: f64 = 10.;
|
||||
|
||||
#[impl_message(Message, InputMapper)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum InputMapperMessage {
|
||||
PointerMove,
|
||||
MouseScroll,
|
||||
|
|
@ -87,35 +87,47 @@ macro_rules! entry {
|
|||
entry!{action=$action, message=InputMapperMessage::KeyUp(Key::$key) $(, modifiers=[$($m),* ])?}
|
||||
}};
|
||||
{action=$action:expr, message=$message:expr $(, modifiers=[$($m:ident),* $(,)?])?} => {{
|
||||
MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}
|
||||
&[MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}]
|
||||
}};
|
||||
{action=$action:expr, triggers=[$($m:ident),* $(,)?]} => {{
|
||||
&[
|
||||
MappingEntry {trigger:InputMapperMessage::PointerMove, action: $action.into(), modifiers: modifiers!()},
|
||||
$(
|
||||
MappingEntry {trigger:InputMapperMessage::KeyDown(Key::$m), action: $action.into(), modifiers: modifiers!()},
|
||||
MappingEntry {trigger:InputMapperMessage::KeyUp(Key::$m), action: $action.into(), modifiers: modifiers!()},
|
||||
)*
|
||||
]
|
||||
}};
|
||||
}
|
||||
macro_rules! mapping {
|
||||
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
|
||||
[$($entry:expr),* $(,)?] => {{
|
||||
let mut key_up = KeyMappingEntries::key_array();
|
||||
let mut key_up = KeyMappingEntries::key_array();
|
||||
let mut key_down = KeyMappingEntries::key_array();
|
||||
let mut pointer_move: KeyMappingEntries = Default::default();
|
||||
let mut mouse_scroll: KeyMappingEntries = Default::default();
|
||||
$(
|
||||
let arr = match $entry.trigger {
|
||||
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
|
||||
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
|
||||
InputMapperMessage::PointerMove => &mut pointer_move,
|
||||
InputMapperMessage::MouseScroll => &mut mouse_scroll,
|
||||
};
|
||||
arr.push($entry);
|
||||
)*
|
||||
(key_up, key_down, pointer_move, mouse_scroll)
|
||||
for entry in $entry {
|
||||
let arr = match entry.trigger {
|
||||
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
|
||||
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
|
||||
InputMapperMessage::PointerMove => &mut pointer_move,
|
||||
InputMapperMessage::MouseScroll => &mut mouse_scroll,
|
||||
};
|
||||
arr.push(entry.clone());
|
||||
}
|
||||
)*
|
||||
(key_up, key_down, pointer_move, mouse_scroll)
|
||||
}};
|
||||
}
|
||||
|
||||
impl Default for Mapping {
|
||||
fn default() -> Self {
|
||||
use Key::*;
|
||||
let mappings = mapping![
|
||||
entry! {action=DocumentMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::EnableSnapping, key_down=KeyShift},
|
||||
entry! {action=DocumentMessage::DisableSnapping, key_up=KeyShift},
|
||||
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},
|
||||
// Select
|
||||
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=SelectMessage::DragStart, key_down=Lmb},
|
||||
|
|
@ -126,49 +138,31 @@ impl Default for Mapping {
|
|||
entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb},
|
||||
entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb},
|
||||
// Rectangle
|
||||
entry! {action=RectangleMessage::Center, key_down=KeyAlt},
|
||||
entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt},
|
||||
entry! {action=RectangleMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=RectangleMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=RectangleMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=RectangleMessage::Abort, key_down=Rmb},
|
||||
entry! {action=RectangleMessage::Abort, key_down=KeyEscape},
|
||||
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift},
|
||||
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift},
|
||||
entry! {action=RectangleMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
|
||||
// Ellipse
|
||||
entry! {action=EllipseMessage::Center, key_down=KeyAlt},
|
||||
entry! {action=EllipseMessage::UnCenter, key_up=KeyAlt},
|
||||
entry! {action=EllipseMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=EllipseMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=EllipseMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=EllipseMessage::Abort, key_down=Rmb},
|
||||
entry! {action=EllipseMessage::Abort, key_down=KeyEscape},
|
||||
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyShift},
|
||||
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift},
|
||||
entry! {action=EllipseMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
|
||||
// Shape
|
||||
entry! {action=ShapeMessage::Center, key_down=KeyAlt},
|
||||
entry! {action=ShapeMessage::UnCenter, key_up=KeyAlt},
|
||||
entry! {action=ShapeMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=ShapeMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=ShapeMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=ShapeMessage::Abort, key_down=Rmb},
|
||||
entry! {action=ShapeMessage::Abort, key_down=KeyEscape},
|
||||
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyShift},
|
||||
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift},
|
||||
entry! {action=ShapeMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
|
||||
// Line
|
||||
entry! {action=LineMessage::Center, key_down=KeyAlt},
|
||||
entry! {action=LineMessage::UnCenter, key_up=KeyAlt},
|
||||
entry! {action=LineMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=LineMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=LineMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=LineMessage::Abort, key_down=Rmb},
|
||||
entry! {action=LineMessage::Abort, key_down=KeyEscape},
|
||||
entry! {action=LineMessage::LockAngle, key_down=KeyControl},
|
||||
entry! {action=LineMessage::UnlockAngle, key_up=KeyControl},
|
||||
entry! {action=LineMessage::SnapToAngle, key_down=KeyShift},
|
||||
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift},
|
||||
entry! {action=LineMessage::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]},
|
||||
// Pen
|
||||
entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=PenMessage::PointerMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=PenMessage::DragStart, key_down=Lmb},
|
||||
entry! {action=PenMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=PenMessage::Confirm, key_down=Rmb},
|
||||
|
|
@ -195,27 +189,28 @@ impl Default for Mapping {
|
|||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=DocumentMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::TranslateCanvasBegin, key_down=Mmb},
|
||||
entry! {action=DocumentMessage::TranslateCanvasEnd, key_up=Mmb},
|
||||
entry! {action=DocumentMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyPlus, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyEquals, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MultiplyCanvasZoom(MINUS_KEY_ZOOM_RATE), key_down=KeyMinus, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
|
||||
entry! {action=DocumentMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll},
|
||||
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=DocumentMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
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::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
|
||||
entry! {action=MovementMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb},
|
||||
entry! {action=MovementMessage::TranslateCanvasEnd, key_up=Mmb},
|
||||
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyPlus, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyEquals, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::MultiplyCanvasZoom(MINUS_KEY_ZOOM_RATE), key_down=KeyMinus, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
|
||||
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll},
|
||||
entry! {action=DocumentsMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentsMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentsMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
||||
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=DocumentMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentsMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
|
||||
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]},
|
||||
entry! {action=DocumentMessage::NudgeSelectedLayers(0., -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift]},
|
||||
|
|
@ -289,19 +284,16 @@ pub struct InputMapper {
|
|||
impl InputMapper {
|
||||
pub fn hints(&self, actions: ActionList) -> String {
|
||||
let mut output = String::new();
|
||||
let actions: Vec<MessageDiscriminant> = actions
|
||||
let mut actions = actions
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_)))
|
||||
.collect();
|
||||
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_)));
|
||||
self.mapping
|
||||
.key_down
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, m)| {
|
||||
let ma =
|
||||
m.0.iter()
|
||||
.find_map(|m| actions.iter().find_map(|a| (a == &m.action.to_discriminant()).then(|| m.action.to_discriminant())));
|
||||
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
|
||||
|
||||
ma.map(|a| unsafe { (std::mem::transmute_copy::<usize, Key>(&i), a) })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use bitflags::bitflags;
|
|||
|
||||
#[doc(inline)]
|
||||
pub use document_core::DocumentResponse;
|
||||
use glam::DVec2;
|
||||
|
||||
#[impl_message(Message, InputPreprocessor)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
|
|
@ -31,7 +30,7 @@ bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Hash)]
|
||||
pub struct InputPreprocessor {
|
||||
pub keyboard: KeyStates,
|
||||
pub mouse: MouseState,
|
||||
|
|
@ -78,7 +77,7 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
|
|||
responses.push_back(
|
||||
document_core::Operation::TransformLayer {
|
||||
path: vec![],
|
||||
transform: glam::DAffine2::from_translation(DVec2::new((size.x as f64 - self.viewport_size.x as f64) / 2., (size.y as f64 - self.viewport_size.y as f64) / 2.)).to_cols_array(),
|
||||
transform: glam::DAffine2::from_translation((size.as_f64() - self.viewport_size.as_f64()) / 2.).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
|
@ -137,7 +136,7 @@ mod test {
|
|||
#[test]
|
||||
fn process_action_mouse_move_handle_modifier_keys() {
|
||||
let mut input_preprocessor = InputPreprocessor::default();
|
||||
let message = InputPreprocessorMessage::MouseMove(ViewportPosition { x: 4, y: 809 }, ModifierKeys::ALT);
|
||||
let message = InputPreprocessorMessage::MouseMove((4, 809).into(), ModifierKeys::ALT);
|
||||
let mut responses = VecDeque::new();
|
||||
|
||||
input_preprocessor.process_action(message, (), &mut responses);
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ impl<const LENGTH: usize> BitVector<LENGTH> {
|
|||
let (offset, bit) = Self::convert_index(bitvector_index);
|
||||
self.0[offset] ^= bit;
|
||||
}
|
||||
pub fn get(&mut self, bitvector_index: usize) -> bool {
|
||||
pub fn get(&self, bitvector_index: usize) -> bool {
|
||||
let (offset, bit) = Self::convert_index(bitvector_index);
|
||||
(self.0[offset] & bit) != 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,9 @@ use bitflags::bitflags;
|
|||
use glam::DVec2;
|
||||
|
||||
// origin is top left
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ViewportPosition {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
pub type ViewportPosition = glam::UVec2;
|
||||
|
||||
impl ViewportPosition {
|
||||
pub fn distance(&self, other: &Self) -> f64 {
|
||||
let x_diff = other.x as i64 - self.x as i64;
|
||||
let y_diff = other.y as i64 - self.y as i64;
|
||||
f64::sqrt((x_diff * x_diff + y_diff * y_diff) as f64)
|
||||
}
|
||||
pub fn as_dvec2(&self) -> DVec2 {
|
||||
DVec2::new(self.x as f64, self.y as f64)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
||||
pub struct ScrollDelta {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
|
|
@ -38,7 +23,7 @@ impl ScrollDelta {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
||||
pub struct MouseState {
|
||||
pub position: ViewportPosition,
|
||||
pub mouse_keys: MouseKeys,
|
||||
|
|
@ -52,7 +37,7 @@ impl MouseState {
|
|||
|
||||
pub fn from_pos(x: u32, y: u32) -> MouseState {
|
||||
MouseState {
|
||||
position: ViewportPosition { x, y },
|
||||
position: (x, y).into(),
|
||||
mouse_keys: MouseKeys::default(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,9 +57,12 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub mod message_prelude {
|
||||
pub use crate::communication::generate_hash;
|
||||
pub use crate::communication::message::{AsMessage, Message, MessageDiscriminant};
|
||||
pub use crate::communication::{ActionList, MessageHandler};
|
||||
pub use crate::document::{DocumentMessage, DocumentMessageDiscriminant};
|
||||
pub use crate::document::{DocumentsMessage, DocumentsMessageDiscriminant};
|
||||
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
||||
pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant};
|
||||
pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant};
|
||||
pub use crate::input::{InputMapperMessage, InputMapperMessageDiscriminant, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ pub enum EditorError {
|
|||
UnknownTool,
|
||||
#[error("The operation caused a document error {0:?}")]
|
||||
Document(String),
|
||||
#[error("A Rollback was initated but no transaction was in progress")]
|
||||
NoTransactionInProgress,
|
||||
}
|
||||
|
||||
macro_rules! derive_from {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
input::{
|
||||
mouse::{MouseKeys, MouseState, ScrollDelta, ViewportPosition},
|
||||
mouse::{MouseKeys, MouseState, ScrollDelta},
|
||||
InputPreprocessorMessage, ModifierKeys,
|
||||
},
|
||||
message_prelude::{Message, ToolMessage},
|
||||
|
|
@ -45,14 +45,14 @@ impl EditorTestUtils for Editor {
|
|||
self.lmb_mousedown(x1, y1);
|
||||
self.move_mouse(x2, y2);
|
||||
self.mouseup(MouseState {
|
||||
position: ViewportPosition { x: x2, y: y2 },
|
||||
position: (x2, y2).into(),
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
});
|
||||
}
|
||||
|
||||
fn move_mouse(&mut self, x: u32, y: u32) {
|
||||
self.input(InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }, ModifierKeys::default()));
|
||||
self.input(InputPreprocessorMessage::MouseMove((x, y).into(), ModifierKeys::default()));
|
||||
}
|
||||
|
||||
fn mousedown(&mut self, state: MouseState) {
|
||||
|
|
@ -65,7 +65,7 @@ impl EditorTestUtils for Editor {
|
|||
|
||||
fn lmb_mousedown(&mut self, x: u32, y: u32) {
|
||||
self.mousedown(MouseState {
|
||||
position: ViewportPosition { x, y },
|
||||
position: (x, y).into(),
|
||||
mouse_keys: MouseKeys::LEFT,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ pub mod tool_message_handler;
|
|||
pub mod tool_options;
|
||||
pub mod tools;
|
||||
|
||||
use crate::document::Document;
|
||||
use crate::document::DocumentMessageHandler;
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::message_prelude::*;
|
||||
use crate::{
|
||||
|
|
@ -25,12 +25,20 @@ pub mod tool_messages {
|
|||
pub use super::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant};
|
||||
}
|
||||
|
||||
pub type ToolActionHandlerData<'a> = (&'a Document, &'a DocumentToolData, &'a InputPreprocessor);
|
||||
pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentToolData, &'a InputPreprocessor);
|
||||
|
||||
pub trait Fsm {
|
||||
type ToolData;
|
||||
|
||||
fn transition(self, message: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque<Message>) -> Self;
|
||||
fn transition(
|
||||
self,
|
||||
message: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
messages: &mut VecDeque<Message>,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use document_core::color::Color;
|
|||
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::{
|
||||
document::Document,
|
||||
document::DocumentMessageHandler,
|
||||
tool::{tool_options::ToolOptions, DocumentToolData, ToolFsmState, ToolType},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
|
@ -45,8 +45,8 @@ pub enum ToolMessage {
|
|||
pub struct ToolMessageHandler {
|
||||
tool_state: ToolFsmState,
|
||||
}
|
||||
impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessageHandler {
|
||||
fn process_action(&mut self, message: ToolMessage, data: (&Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
|
||||
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)> for ToolMessageHandler {
|
||||
fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque<Message>) {
|
||||
let (document, input) = data;
|
||||
use ToolMessage::*;
|
||||
match message {
|
||||
|
|
@ -103,7 +103,7 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
|
|||
_ => unreachable!(),
|
||||
};
|
||||
if let Some(tool) = self.tool_state.tool_data.tools.get_mut(&tool_type) {
|
||||
tool.process_action(message, (&document, &self.tool_state.document_tool_data, input), responses);
|
||||
tool.process_action(message, (document, &self.tool_state.document_tool_data, input), responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: Rename this `ToolOption` to not be plural in a separate commit (together with `enum LayerDataTypes`)
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum ToolOptions {
|
||||
Select { append_mode: SelectAppendMode },
|
||||
Ellipse,
|
||||
Shape { shape_type: ShapeType },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum SelectAppendMode {
|
||||
New,
|
||||
Add,
|
||||
|
|
@ -16,7 +16,7 @@ pub enum SelectAppendMode {
|
|||
Intersect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
pub enum ShapeType {
|
||||
Star { vertices: u32 },
|
||||
Polygon { vertices: u32 },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
|
|||
pub struct Crop;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Crop)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum CropMessage {
|
||||
MouseMove,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{document::Document, message_prelude::*};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DAffine2;
|
||||
|
||||
use super::resize::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ellipse {
|
||||
|
|
@ -11,17 +14,12 @@ pub struct Ellipse {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Ellipse)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum EllipseMessage {
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
MouseMove,
|
||||
Resize { center: Key, lock_ratio: Key },
|
||||
Abort,
|
||||
Center,
|
||||
UnCenter,
|
||||
LockAspectRatio,
|
||||
UnlockAspectRatio,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
|
||||
|
|
@ -31,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
|
|||
fn actions(&self) -> ActionList {
|
||||
use EllipseToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(EllipseMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
|
||||
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
|
||||
Ready => actions!(EllipseMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,63 +48,68 @@ impl Default for EllipseToolFsmState {
|
|||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EllipseToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
constrain_to_circle: bool,
|
||||
center_around_cursor: bool,
|
||||
sides: u8,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for EllipseToolFsmState {
|
||||
type ToolData = EllipseToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let mut shape_data = &mut data.data;
|
||||
use EllipseMessage::*;
|
||||
use EllipseToolFsmState::*;
|
||||
if let ToolMessage::Ellipse(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
Dragging
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
shape_data.drag_start = input.mouse.position;
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(
|
||||
Operation::AddEllipse {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
match shape_data.drag_start == input.mouse.position {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.path = None;
|
||||
Ready
|
||||
}
|
||||
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
shape_data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, true, Ready),
|
||||
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, false, Ready),
|
||||
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, false, tool_data, data, responses, Dragging, transform),
|
||||
|
||||
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
|
||||
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
|
||||
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -114,58 +117,3 @@ impl Fsm for EllipseToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state_no_op(state: &mut bool, value: bool, new_state: EllipseToolFsmState) -> EllipseToolFsmState {
|
||||
*state = value;
|
||||
new_state
|
||||
}
|
||||
|
||||
fn update_state(
|
||||
state: fn(&mut EllipseToolData) -> &mut bool,
|
||||
value: bool,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut EllipseToolData,
|
||||
responses: &mut VecDeque<Message>,
|
||||
new_state: EllipseToolFsmState,
|
||||
transform: DAffine2,
|
||||
) -> EllipseToolFsmState {
|
||||
*(state(data)) = value;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(&data, tool_data, transform));
|
||||
|
||||
new_state
|
||||
}
|
||||
|
||||
fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
|
||||
let x0 = data.drag_start.x as f64;
|
||||
let y0 = data.drag_start.y as f64;
|
||||
let x1 = data.drag_current.x as f64;
|
||||
let y1 = data.drag_current.y as f64;
|
||||
|
||||
if data.constrain_to_circle {
|
||||
let (cx, cy, r) = if data.center_around_cursor {
|
||||
(x0, y0, f64::hypot(x1 - x0, y1 - y0))
|
||||
} else {
|
||||
let diameter = f64::max((x1 - x0).abs(), (y1 - y0).abs());
|
||||
let (x2, y2) = (x0 + (x1 - x0).signum() * diameter, y0 + (y1 - y0).signum() * diameter);
|
||||
((x0 + x2) * 0.5, (y0 + y2) * 0.5, diameter * 0.5)
|
||||
};
|
||||
Operation::AddEllipse {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(r, r), 0., DVec2::new(cx, cy))).to_cols_array(),
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
} else {
|
||||
let (cx, cy, r_scale) = if data.center_around_cursor { (x0, y0, 1.0) } else { ((x0 + x1) * 0.5, (y0 + y1) * 0.5, 0.5) };
|
||||
let (rx, ry) = ((x1 - x0).abs() * r_scale, (y1 - y0).abs() * r_scale);
|
||||
Operation::AddEllipse {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(rx, ry), 0., DVec2::new(cx, cy))).to_cols_array(),
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::message_prelude::*;
|
||||
use crate::tool::{ToolActionHandlerData, ToolMessage};
|
||||
use document_core::layers::LayerDataType;
|
||||
use document_core::Quad;
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Eyedropper;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Eyedropper)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum EyedropperMessage {
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
|
|
@ -16,27 +18,19 @@ pub enum EyedropperMessage {
|
|||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
|
||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
let mouse_pos = data.2.mouse.position;
|
||||
let (x, y) = (mouse_pos.x as f64, mouse_pos.y as f64);
|
||||
let (point_1, point_2) = (
|
||||
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
|
||||
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
|
||||
);
|
||||
|
||||
let quad = [
|
||||
DVec2::new(point_1.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_2.y),
|
||||
DVec2::new(point_1.x, point_2.y),
|
||||
];
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
|
||||
|
||||
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
|
||||
if let Ok(layer) = data.0.document.layer(path) {
|
||||
if let Some(color) = layer.style.fill().and_then(|fill| fill.color()) {
|
||||
match action {
|
||||
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
|
||||
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
|
||||
_ => {}
|
||||
}
|
||||
if let LayerDataType::Shape(s) = &layer.data {
|
||||
s.style.fill().and_then(|fill| {
|
||||
fill.color().map(|color| match action {
|
||||
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
|
||||
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
|
||||
_ => {}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use crate::consts::SELECTION_TOLERANCE;
|
||||
use crate::message_prelude::*;
|
||||
use crate::tool::ToolActionHandlerData;
|
||||
use document_core::Operation;
|
||||
use document_core::{Operation, Quad};
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Fill;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Fill)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum FillMessage {
|
||||
MouseDown,
|
||||
}
|
||||
|
|
@ -16,18 +16,8 @@ pub enum FillMessage {
|
|||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
|
||||
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
let mouse_pos = data.2.mouse.position;
|
||||
let (x, y) = (mouse_pos.x as f64, mouse_pos.y as f64);
|
||||
let (point_1, point_2) = (
|
||||
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
|
||||
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
|
||||
);
|
||||
|
||||
let quad = [
|
||||
DVec2::new(point_1.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_2.y),
|
||||
DVec2::new(point_1.x, point_2.y),
|
||||
];
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
|
||||
|
||||
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
|
||||
responses.push_back(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use std::f64::consts::PI;
|
||||
|
||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{document::Document, message_prelude::*};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Line {
|
||||
fsm_state: LineToolFsmState,
|
||||
|
|
@ -13,18 +15,12 @@ pub struct Line {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Line)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum LineMessage {
|
||||
DragStart,
|
||||
DragStop,
|
||||
MouseMove,
|
||||
Redraw { center: Key, lock_angle: Key, snap_angle: Key },
|
||||
Abort,
|
||||
Center,
|
||||
UnCenter,
|
||||
LockAngle,
|
||||
UnlockAngle,
|
||||
SnapToAngle,
|
||||
UnSnapToAngle,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
|
||||
|
|
@ -34,8 +30,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
|
|||
fn actions(&self) -> ActionList {
|
||||
use LineToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(LineMessageDiscriminant; DragStart, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
|
||||
Dragging => actions!(LineMessageDiscriminant; DragStop, MouseMove, Abort, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
|
||||
Ready => actions!(LineMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,69 +52,69 @@ struct LineToolData {
|
|||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
angle: f64,
|
||||
snap_angle: bool,
|
||||
lock_angle: bool,
|
||||
center_around_cursor: bool,
|
||||
path: Option<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl Fsm for LineToolFsmState {
|
||||
type ToolData = LineToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use LineMessage::*;
|
||||
use LineToolFsmState::*;
|
||||
if let ToolMessage::Line(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
responses.push_back(
|
||||
Operation::AddLine {
|
||||
path: data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
|
||||
responses.push_back(generate_transform(data, values[0], values[1], values[2]));
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
// TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
match data.drag_start == input.mouse.position {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
data.path = None;
|
||||
Ready
|
||||
}
|
||||
(Ready, LockAngle) => update_state_no_op(&mut data.lock_angle, true, Ready),
|
||||
(Ready, UnlockAngle) => update_state_no_op(&mut data.lock_angle, false, Ready),
|
||||
(Dragging, LockAngle) => update_state(|data| &mut data.lock_angle, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnlockAngle) => update_state(|data| &mut data.lock_angle, false, tool_data, data, responses, Dragging, transform),
|
||||
|
||||
(Ready, SnapToAngle) => update_state_no_op(&mut data.snap_angle, true, Ready),
|
||||
(Ready, UnSnapToAngle) => update_state_no_op(&mut data.snap_angle, false, Ready),
|
||||
(Dragging, SnapToAngle) => update_state(|data| &mut data.snap_angle, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnSnapToAngle) => update_state(|data| &mut data.snap_angle, false, tool_data, data, responses, Dragging, transform),
|
||||
|
||||
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
|
||||
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
|
||||
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -127,59 +123,39 @@ impl Fsm for LineToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_state_no_op(state: &mut bool, value: bool, new_state: LineToolFsmState) -> LineToolFsmState {
|
||||
*state = value;
|
||||
new_state
|
||||
}
|
||||
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
|
||||
let mut start = data.drag_start.as_f64();
|
||||
let stop = data.drag_current.as_f64();
|
||||
|
||||
fn update_state(
|
||||
state: fn(&mut LineToolData) -> &mut bool,
|
||||
value: bool,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut LineToolData,
|
||||
responses: &mut VecDeque<Message>,
|
||||
new_state: LineToolFsmState,
|
||||
transform: DAffine2,
|
||||
) -> LineToolFsmState {
|
||||
*(state(data)) = value;
|
||||
let dir = stop - start;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
let mut angle = -dir.angle_between(DVec2::X);
|
||||
|
||||
new_state
|
||||
}
|
||||
|
||||
fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
|
||||
let x0 = data.drag_start.x as f64;
|
||||
let y0 = data.drag_start.y as f64;
|
||||
let x1 = data.drag_current.x as f64;
|
||||
let y1 = data.drag_current.y as f64;
|
||||
|
||||
let (dx, dy) = (x1 - x0, y1 - y0);
|
||||
let mut angle = f64::atan2(dx, dy);
|
||||
|
||||
if data.lock_angle {
|
||||
if lock {
|
||||
angle = data.angle
|
||||
};
|
||||
|
||||
if data.snap_angle {
|
||||
let snap_resolution = 12.0;
|
||||
angle = (angle * snap_resolution / PI).round() / snap_resolution * PI;
|
||||
if snap {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
data.angle = angle;
|
||||
|
||||
let (dir_x, dir_y) = (f64::sin(angle), f64::cos(angle));
|
||||
let projected_length = dx * dir_x + dy * dir_y;
|
||||
let (x1, y1) = (x0 + dir_x * projected_length, y0 + dir_y * projected_length);
|
||||
let mut scale = dir.length();
|
||||
|
||||
let (x0, y0) = if data.center_around_cursor { (x0 - (x1 - x0), y0 - (y1 - y0)) } else { (x0, y0) };
|
||||
if lock {
|
||||
let angle_vec = DVec2::new(angle.cos(), angle.sin());
|
||||
scale = dir.dot(angle_vec);
|
||||
}
|
||||
|
||||
Operation::AddLine {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
|
||||
if center {
|
||||
start -= dir / 2.;
|
||||
}
|
||||
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: data.path.clone().unwrap(),
|
||||
transform: glam::DAffine2::from_scale_angle_translation(DVec2::splat(scale), angle, start).to_cols_array(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub mod fill;
|
|||
pub mod line;
|
||||
pub mod pen;
|
||||
pub mod rectangle;
|
||||
pub mod resize;
|
||||
pub mod shape;
|
||||
|
||||
// not implemented yet
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
|
|||
pub struct Navigate;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Navigate)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum NavigateMessage {
|
||||
MouseMove,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
|
|||
pub struct Path;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Path)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum PathMessage {
|
||||
MouseMove,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::input::InputPreprocessor;
|
||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{document::Document, message_prelude::*};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::DAffine2;
|
||||
|
||||
|
|
@ -11,12 +11,12 @@ pub struct Pen {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Pen)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum PenMessage {
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
MouseMove,
|
||||
PointerMove,
|
||||
Confirm,
|
||||
Abort,
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
|
|||
use PenToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||
Dragging => actions!(PenMessageDiscriminant; DragStop, MouseMove, Confirm, Abort),
|
||||
Dragging => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,21 +49,32 @@ impl Default for PenToolFsmState {
|
|||
struct PenToolData {
|
||||
points: Vec<DAffine2>,
|
||||
next_point: DAffine2,
|
||||
path: Option<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl Fsm for PenToolFsmState {
|
||||
type ToolData = PenToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.as_dvec2());
|
||||
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.as_f64());
|
||||
|
||||
use PenMessage::*;
|
||||
use PenToolFsmState::*;
|
||||
if let ToolMessage::Pen(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
|
||||
data.points.push(pos);
|
||||
data.next_point = pos;
|
||||
|
|
@ -71,44 +82,41 @@ impl Fsm for PenToolFsmState {
|
|||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.points.last() != Some(&pos) {
|
||||
data.points.push(pos);
|
||||
data.next_point = pos;
|
||||
}
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, true));
|
||||
responses.extend(make_operation(data, tool_data, true));
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
(Dragging, PointerMove) => {
|
||||
data.next_point = pos;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, true));
|
||||
responses.extend(make_operation(data, tool_data, true));
|
||||
|
||||
Dragging
|
||||
}
|
||||
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
|
||||
(Dragging, Confirm) => {
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
|
||||
if data.points.len() >= 2 {
|
||||
responses.push_back(make_operation(data, tool_data, false));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
responses.extend(make_operation(data, tool_data, false));
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
}
|
||||
|
||||
data.path = None;
|
||||
data.points.clear();
|
||||
|
||||
Ready
|
||||
}
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
data.points.clear();
|
||||
data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
|
|
@ -120,17 +128,20 @@ impl Fsm for PenToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message {
|
||||
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> [Message; 2] {
|
||||
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.translation.x, p.translation.y)).collect();
|
||||
if show_preview {
|
||||
points.push((data.next_point.translation.x, data.next_point.translation.y))
|
||||
}
|
||||
Operation::AddPen {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
points,
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
|
||||
}
|
||||
.into()
|
||||
[
|
||||
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into(),
|
||||
Operation::AddPen {
|
||||
path: data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
points,
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
|
||||
}
|
||||
.into(),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{document::Document, message_prelude::*};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DAffine2;
|
||||
|
||||
use super::resize::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Rectangle {
|
||||
|
|
@ -11,16 +14,12 @@ pub struct Rectangle {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Rectangle)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum RectangleMessage {
|
||||
DragStart,
|
||||
DragStop,
|
||||
MouseMove,
|
||||
Resize { center: Key, lock_ratio: Key },
|
||||
Abort,
|
||||
Center,
|
||||
UnCenter,
|
||||
LockAspectRatio,
|
||||
UnlockAspectRatio,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
|
||||
|
|
@ -30,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
|
|||
fn actions(&self) -> ActionList {
|
||||
use RectangleToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(RectangleMessageDiscriminant; DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
|
||||
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
|
||||
Ready => actions!(RectangleMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,63 +48,68 @@ impl Default for RectangleToolFsmState {
|
|||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RectangleToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
constrain_to_square: bool,
|
||||
center_around_cursor: bool,
|
||||
sides: u8,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for RectangleToolFsmState {
|
||||
type ToolData = RectangleToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let mut shape_data = &mut data.data;
|
||||
use RectangleMessage::*;
|
||||
use RectangleToolFsmState::*;
|
||||
if let ToolMessage::Rectangle(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
Dragging
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
shape_data.drag_start = input.mouse.position;
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(
|
||||
Operation::AddRect {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
match shape_data.drag_start == input.mouse.position {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.path = None;
|
||||
Ready
|
||||
}
|
||||
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
shape_data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
|
||||
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
|
||||
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform),
|
||||
|
||||
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
|
||||
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
|
||||
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -113,60 +117,3 @@ impl Fsm for RectangleToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state_no_op(state: &mut bool, value: bool, new_state: RectangleToolFsmState) -> RectangleToolFsmState {
|
||||
*state = value;
|
||||
new_state
|
||||
}
|
||||
|
||||
fn update_state(
|
||||
state: fn(&mut RectangleToolData) -> &mut bool,
|
||||
value: bool,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut RectangleToolData,
|
||||
responses: &mut VecDeque<Message>,
|
||||
new_state: RectangleToolFsmState,
|
||||
transform: DAffine2,
|
||||
) -> RectangleToolFsmState {
|
||||
*(state(data)) = value;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
|
||||
new_state
|
||||
}
|
||||
|
||||
fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
|
||||
let x0 = data.drag_start.x as f64;
|
||||
let y0 = data.drag_start.y as f64;
|
||||
let x1 = data.drag_current.x as f64;
|
||||
let y1 = data.drag_current.y as f64;
|
||||
|
||||
let (x0, y0, x1, y1) = if data.constrain_to_square {
|
||||
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
|
||||
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
|
||||
if data.center_around_cursor {
|
||||
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
|
||||
} else {
|
||||
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
|
||||
}
|
||||
} else {
|
||||
let (x0, y0) = if data.center_around_cursor {
|
||||
let delta_x = x1 - x0;
|
||||
let delta_y = y1 - y0;
|
||||
|
||||
(x0 - delta_x, y0 - delta_y)
|
||||
} else {
|
||||
(x0, y0)
|
||||
};
|
||||
(x0, y0, x1, y1)
|
||||
};
|
||||
|
||||
Operation::AddRect {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
34
core/editor/src/tool/tools/resize.rs
Normal file
34
core/editor/src/tool/tools/resize.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::input::keyboard::Key;
|
||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::message_prelude::*;
|
||||
use document_core::Operation;
|
||||
use glam::{DAffine2, Vec2Swizzles};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
pub drag_start: ViewportPosition,
|
||||
pub path: Option<Vec<LayerId>>,
|
||||
}
|
||||
impl Resize {
|
||||
pub fn calculate_transform(&self, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
|
||||
let mut start = self.drag_start.as_f64();
|
||||
let stop = ipp.mouse.position.as_f64();
|
||||
|
||||
let mut size = stop - start;
|
||||
if ipp.keyboard.get(lock_ratio as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
if ipp.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
|
||||
self.path.clone().map(|path| {
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path,
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ use document_core::layers::style;
|
|||
use document_core::layers::style::Fill;
|
||||
use document_core::layers::style::Stroke;
|
||||
use document_core::Operation;
|
||||
use document_core::Quad;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -10,7 +12,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
|||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{
|
||||
consts::SELECTION_TOLERANCE,
|
||||
document::{AlignAggregate, AlignAxis, Document, FlipAxis},
|
||||
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
||||
message_prelude::*,
|
||||
};
|
||||
|
||||
|
|
@ -21,7 +23,7 @@ pub struct Select {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Select)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub enum SelectMessage {
|
||||
DragStart,
|
||||
DragStop,
|
||||
|
|
@ -40,8 +42,9 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
|||
fn actions(&self) -> ActionList {
|
||||
use SelectToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SelectMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
|
||||
Ready => actions!(SelectMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove),
|
||||
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +53,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
|||
enum SelectToolFsmState {
|
||||
Ready,
|
||||
Dragging,
|
||||
DrawingBox,
|
||||
}
|
||||
|
||||
impl Default for SelectToolFsmState {
|
||||
|
|
@ -62,14 +66,38 @@ impl Default for SelectToolFsmState {
|
|||
struct SelectToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
layers_dragging: Vec<(Vec<LayerId>, DVec2)>, // Paths and offsets
|
||||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||
box_id: Option<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
fn selection_box(&self) -> [DVec2; 2] {
|
||||
if self.drag_current == self.drag_start {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
[self.drag_start.as_f64() - tolerance, self.drag_start.as_f64() + tolerance]
|
||||
} else {
|
||||
[self.drag_start.as_f64(), self.drag_current.as_f64()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for SelectToolFsmState {
|
||||
type ToolData = SelectToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
_tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use SelectMessage::*;
|
||||
use SelectToolFsmState::*;
|
||||
if let ToolMessage::Select(event) = event {
|
||||
|
|
@ -77,94 +105,71 @@ impl Fsm for SelectToolFsmState {
|
|||
(Ready, DragStart) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
let (point_1, point_2) = {
|
||||
let (x, y) = (data.drag_start.x as f64, data.drag_start.y as f64);
|
||||
(
|
||||
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
|
||||
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
|
||||
)
|
||||
};
|
||||
|
||||
let quad = [
|
||||
DVec2::new(point_1.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_2.y),
|
||||
DVec2::new(point_1.x, point_2.y),
|
||||
];
|
||||
|
||||
if let Some(intersection) = document.document.intersects_quad_root(quad).last() {
|
||||
// TODO: Replace root transformations with functions of the transform api
|
||||
let transformed_start = document.document.root.transform.inverse().transform_vector2(data.drag_start.as_dvec2());
|
||||
if document.layer_data.get(intersection).map_or(false, |layer_data| layer_data.selected) {
|
||||
data.layers_dragging = document
|
||||
.layer_data
|
||||
.iter()
|
||||
.filter_map(|(path, layer_data)| {
|
||||
layer_data
|
||||
.selected
|
||||
.then(|| (path.clone(), document.document.layer(path).unwrap().transform.translation - transformed_start))
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::SelectLayers(vec![intersection.clone()]).into());
|
||||
data.layers_dragging = vec![(intersection.clone(), document.document.layer(intersection).unwrap().transform.translation - transformed_start)]
|
||||
let mut selected: Vec<_> = document.selected_layers().cloned().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.
|
||||
if selected.is_empty() {
|
||||
if let Some(layer) = intersection.last() {
|
||||
selected.push(layer.clone());
|
||||
responses.push_back(DocumentMessage::SelectLayers(selected.clone()).into());
|
||||
}
|
||||
} else {
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
data.layers_dragging = Vec::new();
|
||||
}
|
||||
|
||||
Dragging
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
// Otherwise enter the box select mode
|
||||
if selected.iter().any(|path| intersection.contains(path)) {
|
||||
data.layers_dragging = selected;
|
||||
Dragging
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
data.box_id = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
responses.push_back(
|
||||
Operation::AddBoundingBox {
|
||||
path: data.box_id.clone().unwrap(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
DrawingBox
|
||||
}
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
if data.layers_dragging.is_empty() {
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
} else {
|
||||
for (path, offset) in &data.layers_dragging {
|
||||
responses.push_back(DocumentMessage::DragLayer(path.clone(), *offset).into());
|
||||
}
|
||||
for path in data.layers_dragging.iter() {
|
||||
responses.push_back(
|
||||
Operation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_translation(input.mouse.position.as_f64() - data.drag_current.as_f64()).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
data.drag_current = input.mouse.position;
|
||||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
(DrawingBox, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
let start = data.drag_start.as_f64();
|
||||
let size = data.drag_current.as_f64() - start;
|
||||
|
||||
if data.layers_dragging.is_empty() {
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
|
||||
if data.drag_start == data.drag_current {
|
||||
responses.push_back(DocumentMessage::SelectLayers(vec![]).into());
|
||||
} else {
|
||||
let (point_1, point_2) = (
|
||||
DVec2::new(data.drag_start.x as f64, data.drag_start.y as f64),
|
||||
DVec2::new(data.drag_current.x as f64, data.drag_current.y as f64),
|
||||
);
|
||||
|
||||
let quad = [
|
||||
DVec2::new(point_1.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_1.y),
|
||||
DVec2::new(point_2.x, point_2.y),
|
||||
DVec2::new(point_1.x, point_2.y),
|
||||
];
|
||||
|
||||
responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
|
||||
responses.push_back(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: data.box_id.clone().unwrap(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
}
|
||||
} else {
|
||||
data.layers_dragging = Vec::new();
|
||||
}
|
||||
|
||||
.into(),
|
||||
);
|
||||
DrawingBox
|
||||
}
|
||||
(Dragging, DragStop) => Ready,
|
||||
(DrawingBox, Abort) => {
|
||||
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
|
||||
Ready
|
||||
}
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
data.layers_dragging = Vec::new();
|
||||
|
||||
(DrawingBox, DragStop) => {
|
||||
let quad = data.selection_quad();
|
||||
responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
|
||||
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
|
||||
Ready
|
||||
}
|
||||
(_, Align(axis, aggregate)) => {
|
||||
|
|
@ -189,18 +194,3 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_operation(data: &SelectToolData, _tool_data: &DocumentToolData, transform: DAffine2) -> Message {
|
||||
let x0 = data.drag_start.x as f64;
|
||||
let y0 = data.drag_start.y as f64;
|
||||
let x1 = data.drag_current.x as f64;
|
||||
let y1 = data.drag_current.y as f64;
|
||||
|
||||
Operation::AddRect {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x31, 0x94, 0xD6), 2.0)), Some(Fill::none())),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::input::InputPreprocessor;
|
||||
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
|
||||
use crate::{document::Document, message_prelude::*};
|
||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||
use document_core::{layers::style, Operation};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DAffine2;
|
||||
|
||||
use super::resize::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Shape {
|
||||
|
|
@ -11,17 +14,12 @@ pub struct Shape {
|
|||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Shape)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash)]
|
||||
pub enum ShapeMessage {
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
MouseMove,
|
||||
Resize { center: Key, lock_ratio: Key },
|
||||
Abort,
|
||||
Center,
|
||||
UnCenter,
|
||||
LockAspectRatio,
|
||||
UnlockAspectRatio,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
|
||||
|
|
@ -31,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
|
|||
fn actions(&self) -> ActionList {
|
||||
use ShapeToolFsmState::*;
|
||||
match self.fsm_state {
|
||||
Ready => actions!(ShapeMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
|
||||
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
|
||||
Ready => actions!(ShapeMessageDiscriminant; DragStart),
|
||||
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,26 +48,32 @@ impl Default for ShapeToolFsmState {
|
|||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ShapeToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
constrain_to_square: bool,
|
||||
center_around_cursor: bool,
|
||||
sides: u8,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for ShapeToolFsmState {
|
||||
type ToolData = ShapeToolData;
|
||||
|
||||
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
|
||||
let transform = document.document.root.transform;
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let mut shape_data = &mut data.data;
|
||||
use ShapeMessage::*;
|
||||
use ShapeToolFsmState::*;
|
||||
if let ToolMessage::Shape(event) = event {
|
||||
match (self, event) {
|
||||
(Ready, DragStart) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
shape_data.drag_start = input.mouse.position;
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
data.sides = match tool_data.tool_options.get(&ToolType::Shape) {
|
||||
Some(&ToolOptions::Shape {
|
||||
shape_type: ShapeType::Polygon { vertices },
|
||||
|
|
@ -77,43 +81,42 @@ impl Fsm for ShapeToolFsmState {
|
|||
_ => 6,
|
||||
};
|
||||
|
||||
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
|
||||
Dragging
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(
|
||||
Operation::AddShape {
|
||||
path: shape_data.path.clone().unwrap(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
sides: data.sides,
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Dragging
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
|
||||
responses.push_back(message);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
match shape_data.drag_start == input.mouse.position {
|
||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||
}
|
||||
|
||||
shape_data.path = None;
|
||||
Ready
|
||||
}
|
||||
(Dragging, Abort) => {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
shape_data.path = None;
|
||||
|
||||
Ready
|
||||
}
|
||||
|
||||
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
|
||||
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
|
||||
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform),
|
||||
|
||||
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
|
||||
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
|
||||
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
|
||||
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -121,63 +124,3 @@ impl Fsm for ShapeToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state_no_op(state: &mut bool, value: bool, new_state: ShapeToolFsmState) -> ShapeToolFsmState {
|
||||
*state = value;
|
||||
new_state
|
||||
}
|
||||
|
||||
fn update_state(
|
||||
state: fn(&mut ShapeToolData) -> &mut bool,
|
||||
value: bool,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut ShapeToolData,
|
||||
responses: &mut VecDeque<Message>,
|
||||
new_state: ShapeToolFsmState,
|
||||
transform: DAffine2,
|
||||
) -> ShapeToolFsmState {
|
||||
*(state(data)) = value;
|
||||
|
||||
responses.push_back(Operation::ClearWorkingFolder.into());
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
|
||||
new_state
|
||||
}
|
||||
|
||||
fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
|
||||
let x0 = data.drag_start.x as f64;
|
||||
let y0 = data.drag_start.y as f64;
|
||||
let x1 = data.drag_current.x as f64;
|
||||
let y1 = data.drag_current.y as f64;
|
||||
|
||||
// TODO: Use regular polygon's aspect ration for constraining rather than a square.
|
||||
let (x0, y0, x1, y1, equal_sides) = if data.constrain_to_square {
|
||||
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
|
||||
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
|
||||
if data.center_around_cursor {
|
||||
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true)
|
||||
} else {
|
||||
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true)
|
||||
}
|
||||
} else {
|
||||
let (x0, y0) = if data.center_around_cursor {
|
||||
let delta_x = x1 - x0;
|
||||
let delta_y = y1 - y0;
|
||||
|
||||
(x0 - delta_x, y0 - delta_y)
|
||||
} else {
|
||||
(x0, y0)
|
||||
};
|
||||
(x0, y0, x1, y1, false)
|
||||
};
|
||||
|
||||
Operation::AddShape {
|
||||
path: vec![],
|
||||
insert_index: -1,
|
||||
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
|
||||
equal_sides,
|
||||
sides: data.sides,
|
||||
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue