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:
TrueDoctor 2021-08-06 12:34:30 +02:00 committed by GitHub
parent 63a4ed64c8
commit f79e6e378d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 2059 additions and 2251 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -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 {

View file

@ -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,
};
}

View file

@ -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)
}

View file

@ -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"] }

View file

@ -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)
}
}

View file

@ -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;
};

View 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",
}
}
}

View file

@ -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());
}
}
}

View file

@ -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,
}
}
}

View file

@ -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());
}
}
}

View file

@ -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,
}
}
}

View file

@ -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);
}

View file

@ -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());
}
}
}

View file

@ -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());
}
}
}

View 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,
}
}
}

View file

@ -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,
}

View file

@ -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()
}
}

View file

@ -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",
};

View file

@ -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"

View file

@ -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<_>>());
}
}

View file

@ -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()
}
}

View file

@ -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()
}

View file

@ -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;

View file

@ -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
}
}

View file

@ -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
}
}

View 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,
}
}

View file

@ -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};

View 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
}
}

View file

@ -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,

View file

@ -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,
}
}
}

View file

@ -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,

View file

@ -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) })
})

View file

@ -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);

View file

@ -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
}

View file

@ -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(),
}

View file

@ -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};

View file

@ -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 {

View file

@ -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(),
})

View file

@ -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)]

View file

@ -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);
}
}
}

View file

@ -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 },

View file

@ -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,
}

View file

@ -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()
}

View file

@ -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()),
_ => {}
})
});
}
}
}

View file

@ -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(

View file

@ -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()
}

View file

@ -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

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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(),
]
}

View file

@ -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()
}

View 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()
})
}
}

View file

@ -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()
}

View file

@ -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()
}