mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 08:08:13 +08:00
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:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
9e06e70aa2
commit
c42d030f18
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user