New overlay system (#1516)

* Port gradient tool overlays

* Fix tests

* Text tool

* Artboard tool and some of select tool

* Port select tool drawing box

* Pen and path tool

* Remove overlays document

* Show the overlay refactor as done on the website roadmap

* Select tool bounds in layer space (first layer)

* Code review and fixes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-12-18 11:17:43 +00:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 9e06e70aa2
commit c42d030f18
36 changed files with 552 additions and 1425 deletions
@@ -171,7 +171,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
}
#[remain::unsorted]
Overlays(message) => {
self.overlays_message_handler.process_message(message, responses, (self.overlays_visible, persistent_data, ipp));
self.overlays_message_handler.process_message(message, responses, (self.overlays_visible, ipp));
}
#[remain::unsorted]
PropertiesPanel(message) => {
@@ -569,9 +569,10 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
}
RenameLayer { layer_path, new_name } => responses.add(DocumentOperation::RenameLayer { layer_path, new_name }),
RenderDocument => {
responses.add(FrontendMessage::UpdateDocumentArtwork {
svg: self.document_legacy.render_root(&render_data),
});
// responses.add(FrontendMessage::UpdateDocumentArtwork {
// svg: self.document_legacy.render_root(&render_data),
// });
responses.add(OverlaysMessage::Draw);
}
RenderRulers => {
let document_transform_scale = self.navigation_handler.snapped_scale();
@@ -743,8 +744,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
SetOverlaysVisibility { visible } => {
self.overlays_visible = visible;
responses.add(BroadcastEvent::ToolAbort);
responses.add(OverlaysMessage::ClearAllOverlays);
responses.add(OverlaysMessage::Rerender);
responses.add(OverlaysMessage::Draw);
}
SetRangeSelectionLayer { new_layer } => {
self.layer_range_selection_reference = new_layer;
@@ -1,7 +1,9 @@
mod overlays_message;
mod overlays_message_handler;
pub mod utility_functions;
pub mod utility_types;
#[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
pub use overlays_message::*;
#[doc(inline)]
pub use overlays_message_handler::OverlaysMessageHandler;
pub use overlays_message_handler::*;
@@ -1,24 +1,14 @@
use super::utility_types::{empty_provider, OverlayProvider};
use crate::messages::prelude::*;
use document_legacy::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Overlays)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum OverlaysMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
Draw,
// Messages
ClearAllOverlays,
Rerender,
}
impl From<DocumentOperation> for OverlaysMessage {
fn from(operation: DocumentOperation) -> OverlaysMessage {
Self::DispatchOperation(Box::new(operation))
}
// Serde functionality isn't used but is required by the message system macros
AddProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
RemoveProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
}
@@ -1,50 +1,51 @@
use crate::messages::portfolio::utility_types::PersistentData;
use super::utility_functions::overlay_canvas_element;
use super::utility_types::{OverlayContext, OverlayProvider};
use crate::messages::prelude::*;
use document_legacy::document::Document as DocumentLegacy;
use document_legacy::layers::style::{RenderData, ViewMode};
use wasm_bindgen::JsCast;
#[derive(Debug, Clone, Default)]
pub struct OverlaysMessageHandler {
pub overlays_document: DocumentLegacy,
pub overlay_providers: HashSet<OverlayProvider>,
canvas: Option<web_sys::HtmlCanvasElement>,
context: Option<web_sys::CanvasRenderingContext2d>,
}
impl MessageHandler<OverlaysMessage, (bool, &PersistentData, &InputPreprocessorMessageHandler)> for OverlaysMessageHandler {
#[remain::check]
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, (overlays_visible, persistent_data, ipp): (bool, &PersistentData, &InputPreprocessorMessageHandler)) {
use OverlaysMessage::*;
#[remain::sorted]
impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> for OverlaysMessageHandler {
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, (overlays_visible, ipp): (bool, &InputPreprocessorMessageHandler)) {
match message {
// Sub-messages
#[remain::unsorted]
DispatchOperation(operation) => match self.overlays_document.handle_operation(*operation) {
Ok(_) => responses.add(OverlaysMessage::Rerender),
Err(e) => error!("OverlaysError: {e:?}"),
},
#[cfg(target_arch = "wasm32")]
OverlaysMessage::Draw => {
let canvas = self.canvas.get_or_insert_with(|| overlay_canvas_element().expect("Failed to get canvas element"));
// Messages
ClearAllOverlays => {
self.overlays_document = DocumentLegacy::default();
let context = self.context.get_or_insert_with(|| {
let context = canvas.get_context("2d").ok().flatten().expect("Failed to get canvas context");
context.dyn_into().expect("Context should be a canvas 2d context")
});
canvas.set_width(ipp.viewport_bounds.size().x as u32);
canvas.set_height(ipp.viewport_bounds.size().y as u32);
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
if overlays_visible {
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext { render_context: context.clone() }));
}
}
}
Rerender =>
// Render overlays
{
responses.add(FrontendMessage::UpdateDocumentOverlays {
svg: if overlays_visible {
let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, Some(ipp.document_bounds()));
self.overlays_document.render_root(&render_data)
} else {
String::from("")
},
})
#[cfg(not(target_arch = "wasm32"))]
OverlaysMessage::Draw => {
warn!("Cannot render overlays on non-Wasm targets {overlays_visible} {ipp:?}.");
}
OverlaysMessage::AddProvider(message) => {
self.overlay_providers.insert(message);
}
OverlaysMessage::RemoveProvider(message) => {
self.overlay_providers.remove(&message);
}
}
}
fn actions(&self) -> ActionList {
actions!(OverlaysMessageDiscriminant;
ClearAllOverlays,
)
}
advertise_actions!(OverlaysMessage;);
}
@@ -0,0 +1,54 @@
use super::utility_types::OverlayContext;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::DVec2;
use wasm_bindgen::JsCast;
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
let window = web_sys::window()?;
let document = window.document()?;
let canvas = document.query_selector("[data-overlays-canvas]").ok().flatten()?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
}
pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
let create_context = || {
let context = overlay_canvas_element()?.get_context("2d").ok().flatten()?;
context.dyn_into().ok()
};
create_context().expect("Failed to get canvas context")
}
pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
for layer in document.metadata().selected_layers() {
let Some(subpaths) = get_subpaths(layer, &document.document_legacy) else { continue };
let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
overlay_context.outline(subpaths.iter(), transform);
for manipulator_group in get_manipulator_groups(subpaths) {
let anchor = manipulator_group.anchor;
let anchor_position = transform.transform_point2(anchor);
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(in_handle);
overlay_context.line(handle_position, anchor_position);
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
}
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(out_handle);
overlay_context.line(handle_position, anchor_position);
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
}
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)));
}
}
}
@@ -0,0 +1,126 @@
use super::utility_functions::overlay_canvas_context;
use crate::consts::{COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_INNER, PIVOT_OUTER};
use crate::messages::prelude::Message;
use bezier_rs::Subpath;
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use core::f64::consts::PI;
use glam::{DAffine2, DVec2};
pub type OverlayProvider = fn(OverlayContext) -> Message;
pub fn empty_provider() -> OverlayProvider {
|_| Message::NoOp
}
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct OverlayContext {
// Serde functionality isn't used but is required by the message system macros
#[serde(skip, default = "overlay_canvas_context")]
pub render_context: web_sys::CanvasRenderingContext2d,
}
// Message hashing isn't used but is required by the message system macros
impl core::hash::Hash for OverlayContext {
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}
impl OverlayContext {
fn accent_hex() -> String {
format!("#{}", COLOR_ACCENT.rgb_hex())
}
pub fn quad(&mut self, quad: Quad) {
self.render_context.begin_path();
self.render_context.move_to(quad.0[3].x.round(), quad.0[3].y.round());
for i in 0..4 {
self.render_context.line_to(quad.0[i].x.round(), quad.0[i].y.round());
}
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
}
pub fn line(&mut self, start: DVec2, end: DVec2) {
self.render_context.begin_path();
self.render_context.move_to(start.x.round(), start.y.round());
self.render_context.line_to(end.x.round(), end.y.round());
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
}
pub fn handle(&mut self, position: DVec2, selected: bool) {
self.render_context.begin_path();
let position = position.round();
self.render_context
.arc(position.x + 0.5, position.y + 0.5, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.)
.expect("draw circle");
let fill = if selected { Self::accent_hex() } else { "white".to_string() };
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill));
self.render_context.fill();
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
}
pub fn square(&mut self, position: DVec2, selected: bool) {
self.render_context.begin_path();
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
self.render_context
.rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE);
let fill = if selected { Self::accent_hex() } else { "white".to_string() };
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill));
self.render_context.fill();
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
}
pub fn pivot(&mut self, pivot: DVec2) {
self.render_context.begin_path();
self.render_context.arc(pivot.x + 0.5, pivot.y + 0.5, PIVOT_OUTER / 2., 0., PI * 2.).expect("draw circle");
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&"white"));
self.render_context.fill();
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
self.render_context.begin_path();
self.render_context.arc(pivot.x, pivot.y, PIVOT_INNER / 2., 0., PI * 2.).expect("draw circle");
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.fill();
}
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<ManipulatorGroupId>>, transform: DAffine2) {
let transform = |point| transform.transform_point2(point);
self.render_context.begin_path();
for subpath in subpaths {
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {
continue;
};
self.render_context.move_to(transform(first.start()).x, transform(first.start()).y);
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(transform(curve.end()).x, transform(curve.end()).y),
bezier_rs::BezierHandles::Quadratic { handle } => {
self.render_context
.quadratic_curve_to(transform(handle).x, transform(handle).y, transform(curve.end()).x, transform(curve.end()).y)
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(
transform(handle_start).x,
transform(handle_start).y,
transform(handle_end).x,
transform(handle_end).y,
transform(curve.end()).x,
transform(curve.end()).y,
),
}
}
if subpath.closed() {
self.render_context.close_path();
}
}
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(&Self::accent_hex()));
self.render_context.stroke();
}
}
@@ -4,6 +4,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::ToolType;
use document_legacy::document::Document;
use document_legacy::document_metadata::LayerNodeIdentifier;
use graphene_core::renderer::Quad;
@@ -400,11 +401,8 @@ impl<'a> Selected<'a> {
}
}
pub fn update_transforms(&mut self, delta: DAffine2) {
pub fn apply_transformation(&mut self, transformation: DAffine2) {
if !self.selected.is_empty() {
let pivot = DAffine2::from_translation(*self.pivot);
let transformation = pivot * delta * pivot.inverse();
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for layer_ancestors in self.document.metadata.shallowest_unique_layers(self.selected.iter().copied()) {
let layer = *layer_ancestors.last().unwrap();
@@ -418,6 +416,12 @@ impl<'a> Selected<'a> {
}
}
pub fn update_transforms(&mut self, delta: DAffine2) {
let pivot = DAffine2::from_translation(*self.pivot);
let transformation = pivot * delta * pivot.inverse();
self.apply_transformation(transformation);
}
pub fn revert_operation(&mut self) {
for layer in self.selected.iter().copied() {
let original_transform = &self.original_transforms;