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
parent 9e06e70aa2
commit c42d030f18
36 changed files with 552 additions and 1425 deletions

1
Cargo.lock generated
View File

@ -2326,6 +2326,7 @@ dependencies = [
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"wgpu-executor",
]

View File

@ -87,8 +87,8 @@ num-traits = { version = "0.2.15", default-features = false, features = [
"i128",
] }
js-sys = { version = "0.3.55" }
usvg = "0.35.0"
web-sys = { version = "0.3.55" }
usvg = "0.35.0"
spirv = "0.2.0"
fern = { version = "0.6", features = ["colored"] }

View File

@ -1,13 +1,13 @@
use glam::{DAffine2, DVec2};
use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint;
use graphene_core::uuid::ManipulatorGroupId;
use glam::{DAffine2, DVec2};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
use graphene_core::renderer::Quad;
#[derive(Debug, Clone)]
pub struct DocumentMetadata {
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
@ -338,11 +338,10 @@ impl DocumentMetadata {
.reduce(Quad::combine_bounds)
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath {
let Some(click_targets) = self.click_targets.get(&layer) else {
return graphene_core::vector::Subpath::new();
};
graphene_core::vector::Subpath::from_bezier_rs(click_targets.iter().map(|click_target| &click_target.subpath))
pub fn layer_outline<'a>(&'a self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &'a bezier_rs::Subpath<ManipulatorGroupId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(|click_target| &click_target.subpath)
}
}

View File

@ -51,6 +51,13 @@ wasm-bindgen-futures = { workspace = true, optional = true }
document-legacy = { workspace = true }
# Remove when `core::cell::LazyCell` is stabilized (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>)
once_cell = "1.13.0"
web-sys = { workspace = true, features = [
"Document",
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
] }
[dev-dependencies]
env_logger = "0.10"

View File

@ -33,7 +33,6 @@ pub struct DispatcherMessageHandlers {
/// In addition, these messages do not change any state in the backend (aside from caches).
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Rerender))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
PropertiesPanelMessageDiscriminant::ResendActiveProperties,
@ -44,6 +43,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::DocumentIsDirty)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::InputFrameRasterizeRegionBelowLayer)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
];
impl Dispatcher {

View File

@ -128,9 +128,6 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentArtboards {
svg: String,
},
UpdateDocumentArtwork {
svg: String,
},
@ -155,12 +152,6 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDocumentNodeRender {
svg: String,
},
UpdateDocumentOverlays {
svg: String,
},
UpdateDocumentRulers {
origin: (f64, f64),
spacing: f64,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -467,7 +467,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
if self.active_document().is_some() {
responses.add(BroadcastEvent::ToolAbort);
responses.add(OverlaysMessage::ClearAllOverlays);
responses.add(OverlaysMessage::Draw);
}
// TODO: Remove this message in favor of having tools have specific data per document instance

View File

@ -1,7 +1,5 @@
pub mod color_selector;
pub mod graph_modification_utils;
pub mod overlay_renderer;
pub mod path_outline;
pub mod pivot;
pub mod resize;
pub mod shape_editor;

View File

@ -1,359 +0,0 @@
use super::shape_editor::SelectedShapeState;
use crate::application::generate_uuid;
use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS;
use crate::consts::{COLOR_ACCENT, HIDE_HANDLE_DISTANCE, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
use bezier_rs::ManipulatorGroup;
use document_legacy::document::Document;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::{LayerId, Operation};
use graphene_core::raster::color::Color;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::{DAffine2, DVec2};
/// [ManipulatorGroupOverlay]s is the collection of overlays that make up an [ManipulatorGroup] visible in the editor.
#[derive(Clone, Debug, Default)]
struct ManipulatorGroupOverlays {
pub anchor: Option<Vec<LayerId>>,
pub in_handle: Option<Vec<LayerId>>,
pub in_line: Option<Vec<LayerId>>,
pub out_handle: Option<Vec<LayerId>>,
pub out_line: Option<Vec<LayerId>>,
}
impl ManipulatorGroupOverlays {
pub fn iter(&self) -> impl Iterator<Item = &'_ Option<Vec<LayerId>>> {
[&self.anchor, &self.in_handle, &self.in_line, &self.out_handle, &self.out_line].into_iter()
}
}
type GraphiteManipulatorGroup = ManipulatorGroup<ManipulatorGroupId>;
const POINT_STROKE_WEIGHT: f64 = 2.;
#[derive(Clone, Debug, Default)]
pub struct OverlayRenderer {
shape_overlay_cache: HashMap<LayerNodeIdentifier, Vec<LayerId>>,
manipulator_group_overlay_cache: HashMap<LayerNodeIdentifier, HashMap<ManipulatorGroupId, ManipulatorGroupOverlays>>,
}
impl OverlayRenderer {
pub fn new() -> Self {
Self::default()
}
pub fn query_cache(&self, layer: &LayerNodeIdentifier) -> Option<&Vec<LayerId>> {
self.shape_overlay_cache.get(layer)
}
pub fn render_subpath_overlays(&mut self, selected_shape_state: &SelectedShapeState, document: &Document, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
let transform = document.metadata.transform_to_viewport(layer);
let Some(subpaths) = get_subpaths(layer, document) else {
return;
};
self.layer_overlay_visibility(document, layer, true, responses);
let outline_cache = self.shape_overlay_cache.get(&layer);
trace!("Overlay: Outline cache {outline_cache:?}");
// Create an outline if we do not have a cached one
if outline_cache.is_none() {
let outline_path = self.create_shape_outline_overlay(graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
self.shape_overlay_cache.insert(layer, outline_path.clone());
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
trace!("Overlay: Creating new outline {outline_path:?}");
} else if let Some(outline_path) = outline_cache {
trace!("Overlay: Updating overlays for {outline_path:?} owning layer: {layer:?}");
Self::modify_outline_overlays(outline_path.clone(), graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
}
// Create, place, and style the manipulator overlays
for manipulator_group in get_manipulator_groups(subpaths) {
let manipulator_group_cache = self.manipulator_group_overlay_cache.entry(layer).or_default().entry(manipulator_group.id).or_default();
// Only view in and out handles if they are not on top of the anchor
let [in_handle, out_handle] = {
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;
let filter_handle = |manipulator: Option<DVec2>| manipulator.filter(not_under_anchor);
[filter_handle(manipulator_group.in_handle), filter_handle(manipulator_group.out_handle)]
};
// Create anchor
manipulator_group_cache.anchor = manipulator_group_cache.anchor.take().or_else(|| Some(Self::create_anchor_overlay(responses)));
// Create or delete in handle
if in_handle.is_none() {
Self::remove_overlay(manipulator_group_cache.in_handle.take(), responses);
Self::remove_overlay(manipulator_group_cache.in_line.take(), responses);
} else {
manipulator_group_cache.in_handle = manipulator_group_cache.in_handle.take().or_else(|| Self::create_handle_overlay_if_exists(in_handle, responses));
manipulator_group_cache.in_line = manipulator_group_cache.in_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(in_handle, responses));
}
// Create or delete out handle
if out_handle.is_none() {
Self::remove_overlay(manipulator_group_cache.out_handle.take(), responses);
Self::remove_overlay(manipulator_group_cache.out_line.take(), responses);
} else {
manipulator_group_cache.out_handle = manipulator_group_cache.out_handle.take().or_else(|| Self::create_handle_overlay_if_exists(out_handle, responses));
manipulator_group_cache.out_line = manipulator_group_cache.out_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(out_handle, responses));
}
// Update placement and style
Self::place_manipulator_group_overlays(manipulator_group, manipulator_group_cache, &transform, responses);
Self::style_overlays(selected_shape_state, layer, manipulator_group, manipulator_group_cache, responses);
}
if let Some(layer_overlays) = self.manipulator_group_overlay_cache.get_mut(&layer) {
if layer_overlays.len() > subpaths.iter().map(|subpath| subpath.len()).sum() {
layer_overlays.retain(|manipulator, manipulator_group_overlays| {
if get_manipulator_groups(subpaths).any(|current_manipulator| current_manipulator.id == *manipulator) {
true
} else {
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
false
}
});
}
}
// TODO Handle removing shapes from cache so we don't memory leak
// Eventually will get replaced with am immediate mode renderer for overlays
responses.add(OverlaysMessage::Rerender);
}
/// Delete all cached overlays
pub fn clear_all_overlays(&mut self, responses: &mut VecDeque<Message>) {
for (_, overlay_path) in self.shape_overlay_cache.drain() {
Self::remove_outline_overlays(overlay_path, responses)
}
for (_, layer_cache) in self.manipulator_group_overlay_cache.drain() {
for manipulator_group_overlays in layer_cache.values() {
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
}
}
}
pub fn layer_overlay_visibility(&mut self, document: &Document, layer: LayerNodeIdentifier, visibility: bool, responses: &mut VecDeque<Message>) {
// Hide the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(&layer) {
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
}
// Hide the manipulator group overlays
let Some(manipulator_groups) = self.manipulator_group_overlay_cache.get(&layer) else { return };
if visibility {
let Some(subpaths) = get_subpaths(layer, document) else { return };
for manipulator_group in get_manipulator_groups(subpaths) {
let id = manipulator_group.id;
if let Some(manipulator_group_overlays) = manipulator_groups.get(&id) {
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
}
}
} else {
for manipulator_group_overlays in manipulator_groups.values() {
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
}
}
}
/// Create the kurbo shape that matches the selected viewport shape.
fn create_shape_outline_overlay(&self, subpath: graphene_core::vector::Subpath, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddShape {
path: layer_path.clone(),
subpath,
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single anchor overlay and return its layer ID.
fn create_anchor_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single handle overlay and return its layer ID.
fn create_handle_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single handle overlay and return its layer id if it exists.
fn create_handle_overlay_if_exists(handle: Option<DVec2>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.map(|_| Self::create_handle_overlay(responses))
}
/// Remove an overlay at the specified path
fn remove_overlay(path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = path {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
}
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add_front(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay_if_exists(handle: Option<DVec2>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_line_overlay(responses))
}
fn place_outline_overlays(outline_path: Vec<LayerId>, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let transform_message = Self::overlay_transform_message(outline_path, parent_transform.to_cols_array());
responses.add(transform_message);
}
fn modify_outline_overlays(outline_path: Vec<LayerId>, subpath: graphene_core::vector::Subpath, responses: &mut VecDeque<Message>) {
let outline_modify_message = Self::overlay_modify_message(outline_path, subpath);
responses.add(outline_modify_message);
}
/// Updates the position of the overlays based on the [Subpath] points.
fn place_manipulator_group_overlays(manipulator_group: &GraphiteManipulatorGroup, overlays: &mut ManipulatorGroupOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let anchor = manipulator_group.anchor;
let mut place_handle_and_line = |handle_position: DVec2, line_overlay: &[LayerId], marker_source: &mut Option<Vec<LayerId>>| {
let line_vector = parent_transform.transform_point2(anchor) - parent_transform.transform_point2(handle_position);
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (parent_transform.transform_point2(handle_position) + VIEWPORT_GRID_ROUNDING_BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.add(Self::overlay_transform_message(line_overlay.to_vec(), transform));
let marker_overlay = marker_source.take().unwrap_or_else(|| Self::create_handle_overlay(responses));
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(handle_position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.add(Self::overlay_transform_message(marker_overlay.clone(), transform));
*marker_source = Some(marker_overlay);
};
// Place the handle overlays
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.in_handle, overlays.in_line.as_mut()) {
place_handle_and_line(handle_position, line_overlay, &mut overlays.in_handle);
}
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.out_handle, overlays.out_line.as_ref()) {
place_handle_and_line(handle_position, line_overlay, &mut overlays.out_handle);
}
// Place the anchor point overlay
if let Some(anchor_overlay) = &overlays.anchor {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(anchor) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let message = Self::overlay_transform_message(anchor_overlay.clone(), transform);
responses.add(message);
}
}
/// Removes the manipulator overlays from the overlay document.
fn remove_manipulator_group_overlays(overlay_paths: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
overlay_paths.iter().flatten().for_each(|layer_id| {
trace!("Overlay: Sending delete message for: {layer_id:?}");
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()));
});
}
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.into()));
}
/// Sets the visibility of the handles overlay.
fn set_manipulator_group_overlay_visibility(manipulator_group_overlays: &ManipulatorGroupOverlays, visibility: bool, responses: &mut VecDeque<Message>) {
manipulator_group_overlays.iter().flatten().for_each(|layer_id| {
responses.add(Self::overlay_visibility_message(layer_id.clone(), visibility));
});
}
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
responses.add(Self::overlay_visibility_message(overlay_path, visibility));
}
/// Create a visibility message for an overlay.
fn overlay_visibility_message(layer_path: Vec<LayerId>, visibility: bool) -> Message {
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: layer_path,
visible: visibility,
}
.into(),
)
.into()
}
/// Create a transform message for an overlay.
fn overlay_transform_message(layer_path: Vec<LayerId>, transform: [f64; 6]) -> Message {
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: layer_path, transform }.into()).into()
}
/// Create an update message for an overlay.
fn overlay_modify_message(layer_path: Vec<LayerId>, subpath: graphene_core::vector::Subpath) -> Message {
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, subpath }.into()).into()
}
/// Sets the overlay style for this point.
fn style_overlays(state: &SelectedShapeState, layer: LayerNodeIdentifier, manipulator_group: &GraphiteManipulatorGroup, overlays: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
// TODO Move the style definitions out of the Subpath, should be looked up from a stylesheet or similar
let selected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
let selected_shape_state = state.get(&layer);
// Update if the manipulator points are shown as selected
// Here the index is important, even though overlays has five elements we only care about the first three
for (index, overlay) in [&overlays.in_handle, &overlays.out_handle, &overlays.anchor].into_iter().enumerate() {
let selected_type = [SelectedType::InHandle, SelectedType::OutHandle, SelectedType::Anchor][index];
if let Some(overlay_path) = overlay {
let selected = selected_shape_state
.filter(|state| state.is_selected(ManipulatorPointId::new(manipulator_group.id, selected_type)))
.is_some();
let style = if selected { selected_style.clone() } else { deselected_style.clone() };
responses.add(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay_path.clone(), style }.into()));
}
}
}
}

View File

@ -1,129 +0,0 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::{LayerId, Operation};
use glam::DAffine2;
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
#[derive(Clone, Debug, Default)]
pub struct PathOutline {
hovered_layer_path: Option<LayerNodeIdentifier>,
hovered_overlay_path: Option<Vec<LayerId>>,
selected_overlay_paths: Vec<Vec<LayerId>>,
}
impl PathOutline {
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one
fn try_create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
let subpath = document.metadata().layer_outline(layer);
let transform = document.metadata().transform_to_viewport(layer);
// Generate a new overlay layer if necessary
let overlay = overlay_path.unwrap_or_else(|| {
let overlay_path = vec![generate_uuid()];
responses.add(DocumentMessage::Overlays(
(Operation::AddShape {
path: overlay_path.clone(),
subpath: Default::default(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
})
.into(),
));
overlay_path
});
// Update the shape subpath
responses.add(DocumentMessage::Overlays((Operation::SetShapePath { path: overlay.clone(), subpath }).into()));
// Update the transform to match the document
responses.add(DocumentMessage::Overlays(
(Operation::SetLayerTransform {
path: overlay.clone(),
transform: transform.to_cols_array(),
})
.into(),
));
Some(overlay)
}
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one.
///
/// Creates an outline, discarding the overlay on failure.
fn create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
let copied_overlay_path = overlay_path.clone();
let result = Self::try_create_outline(layer, overlay_path, document, responses);
if result.is_none() {
// Discard the overlay layer if it exists
if let Some(overlay_path) = copied_overlay_path {
let operation = Operation::DeleteLayer { path: overlay_path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
result
}
/// Removes the hovered overlay and deletes path references
pub fn clear_hovered(&mut self, responses: &mut VecDeque<Message>) {
if let Some(path) = self.hovered_overlay_path.take() {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
self.hovered_layer_path = None;
}
/// Performs an intersect test and generates a hovered overlay if necessary
pub fn intersect_test_hovered(&mut self, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
// Get the layer the user is hovering over
let intersection = document.document_legacy.click(input.mouse.position, &document.document_legacy.document_network);
let Some(hovered_layer) = intersection else {
self.clear_hovered(responses);
return;
};
if document.metadata().selected_layers_contains(hovered_layer) {
self.clear_hovered(responses);
return;
}
// Updates the overlay, generating a new one if necessary
self.hovered_overlay_path = Self::create_outline(hovered_layer, self.hovered_overlay_path.take(), document, responses);
if self.hovered_overlay_path.is_none() {
self.clear_hovered(responses);
}
self.hovered_layer_path = Some(hovered_layer);
}
/// Clears overlays for the selected paths and removes references
pub fn clear_selected(&mut self, responses: &mut VecDeque<Message>) {
while let Some(path) = self.selected_overlay_paths.pop() {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
/// Updates the selected overlays, generating or removing overlays if necessary
pub fn update_selected(&mut self, selected: impl Iterator<Item = LayerNodeIdentifier>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
for layer_identifier in selected {
if let Some(overlay_path) = Self::create_outline(layer_identifier, old_overlay_paths.pop(), document, responses) {
self.selected_overlay_paths.push(overlay_path);
}
}
for path in old_overlay_paths {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
}

View File

@ -1,19 +1,16 @@
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale and origin of the layer.
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PIVOT_INNER, PIVOT_OUTER, PIVOT_OUTER_OUTLINE_THICKNESS};
use super::graph_modification_utils;
use crate::consts::PIVOT_OUTER;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style;
use document_legacy::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::VecDeque;
use super::graph_modification_utils;
#[derive(Clone, Debug)]
pub struct Pivot {
/// Pivot between (0,0) and (1,1)
@ -22,8 +19,6 @@ pub struct Pivot {
transform_from_normalized: DAffine2,
/// The viewspace pivot position (if applicable)
pivot: Option<DVec2>,
/// A reference to the previous overlays so we can destroy them
pivot_overlay_circles: Option<[Vec<LayerId>; 2]>,
/// The old pivot position in the GUI, used to reduce refreshes of the document bar
old_pivot_position: PivotPosition,
}
@ -34,7 +29,6 @@ impl Default for Pivot {
normalized_pivot: DVec2::splat(0.5),
transform_from_normalized: Default::default(),
pivot: Default::default(),
pivot_overlay_circles: Default::default(),
old_pivot_position: PivotPosition::Center,
}
}
@ -87,59 +81,11 @@ impl Pivot {
}
}
pub fn clear_overlays(&mut self, responses: &mut VecDeque<Message>) {
if let Some(overlays) = self.pivot_overlay_circles.take() {
for path in overlays {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
}
}
}
fn redraw_pivot(&mut self, responses: &mut VecDeque<Message>) {
self.clear_overlays(responses);
let pivot = match self.pivot {
Some(pivot) => pivot,
None => return,
};
let layer_paths = [vec![generate_uuid()], vec![generate_uuid()]];
responses.add(DocumentMessage::Overlays(
Operation::AddEllipse {
path: layer_paths[0].clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(
Some(style::Stroke::new(Some(COLOR_ACCENT), PIVOT_OUTER_OUTLINE_THICKNESS)),
style::Fill::Solid(graphene_core::raster::color::Color::WHITE),
),
insert_index: -1,
}
.into(),
));
responses.add(DocumentMessage::Overlays(
Operation::AddEllipse {
path: layer_paths[1].clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
.into(),
));
self.pivot_overlay_circles = Some(layer_paths.clone());
let [outer, inner] = layer_paths;
let pivot_diameter_without_outline = PIVOT_OUTER - PIVOT_OUTER_OUTLINE_THICKNESS;
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(pivot_diameter_without_outline), 0., pivot - DVec2::splat(pivot_diameter_without_outline / 2.)).to_cols_array();
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: outer, transform }.into()));
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(PIVOT_INNER), 0., pivot - DVec2::splat(PIVOT_INNER / 2.)).to_cols_array();
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: inner, transform }.into()));
}
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
self.recalculate_pivot(document);
self.redraw_pivot(responses);
if let Some(pivot) = self.pivot {
overlay_context.pivot(pivot);
}
}
/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).

View File

@ -1,169 +1,19 @@
use super::shape_editor::ManipulatorPointInfo;
use crate::application::generate_uuid;
use crate::consts::{
COLOR_ACCENT, SNAP_AXIS_OVERLAY_FADE_DISTANCE, SNAP_AXIS_TOLERANCE, SNAP_AXIS_UNSNAPPED_OPACITY, SNAP_POINT_OVERLAY_FADE_FAR, SNAP_POINT_OVERLAY_FADE_NEAR, SNAP_POINT_SIZE, SNAP_POINT_TOLERANCE,
SNAP_POINT_UNSNAPPED_OPACITY,
};
use crate::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::layer_info::LegacyLayer;
use document_legacy::layers::style::{self, Stroke};
use document_legacy::{LayerId, Operation};
use document_legacy::LayerId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;
// Handles snap overlays
#[derive(Debug, Clone, Default)]
struct SnapOverlays {
axis_overlay_paths: Vec<Vec<LayerId>>,
point_overlay_paths: Vec<Vec<LayerId>>,
axis_index: usize,
point_index: usize,
}
impl SnapOverlays {
/// Draws an overlay (axis or point) with the correct transform and fade opacity, reusing lines from the pool if available.
fn add_overlay(is_axis: bool, responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: Option<f64>, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
// If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform
let layer_path = if index >= overlay_paths.len() {
let layer_path = vec![generate_uuid()];
responses.add(DocumentMessage::Overlays(
if is_axis {
Operation::AddLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), style::Fill::None),
insert_index: -1,
}
} else {
Operation::AddEllipse {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
}
.into(),
));
overlay_paths.push(layer_path.clone());
layer_path
}
// Otherwise, reuse an overlay from the pool and update its new transform
else {
let layer_path = overlay_paths[index].clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()));
layer_path
};
// Then set its opacity to the fade amount
if let Some(opacity) = opacity {
responses.add(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()));
}
}
/// Draw the alignment lines for an axis
/// Note: horizontal refers to the overlay line being horizontal and the snap being along the Y axis
fn draw_alignment_lines(&mut self, is_horizontal: bool, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, goal, distance) in distances.filter(|(_target, _pos, dist)| dist.abs() < SNAP_AXIS_OVERLAY_FADE_DISTANCE) {
let offset = if is_horizontal { target.y } else { target.x }.round() - 0.5;
let offset_other = if is_horizontal { target.x } else { target.y }.round() - 0.5;
let goal_axis = if is_horizontal { goal.x } else { goal.y }.round() - 0.5;
let scale = DVec2::new(offset_other - goal_axis, 1.);
let angle = if is_horizontal { 0. } else { PI / 2. };
let translation = if is_horizontal { DVec2::new(goal_axis, offset) } else { DVec2::new(offset, goal_axis) };
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let closest = if is_horizontal { closest_distance.y } else { closest_distance.x };
let opacity = if (closest - distance).abs() < 1. {
1.
} else {
SNAP_AXIS_UNSNAPPED_OPACITY - distance.abs() / (SNAP_AXIS_OVERLAY_FADE_DISTANCE / SNAP_AXIS_UNSNAPPED_OPACITY)
};
// Add line
Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths);
self.axis_index += 1;
let size = DVec2::splat(SNAP_POINT_SIZE);
// Add point at target
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1;
// Add point along line but towards goal
let translation = if is_horizontal { DVec2::new(goal.x, target.y) } else { DVec2::new(target.x, goal.y) };
let transform = DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Draw the snap points
fn draw_snap_points(&mut self, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, offset, distance) in distances.filter(|(_pos, _offset, dist)| dist.abs() < SNAP_POINT_OVERLAY_FADE_FAR) {
let active = (closest_distance - offset).length_squared() < 1.;
if active {
continue;
}
let opacity = (1. - (distance - SNAP_POINT_OVERLAY_FADE_NEAR) / (SNAP_POINT_OVERLAY_FADE_FAR - SNAP_POINT_OVERLAY_FADE_NEAR)).min(1.) / SNAP_POINT_UNSNAPPED_OPACITY;
let size = DVec2::splat(SNAP_POINT_SIZE);
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Updates the snapping overlays with the specified distances.
/// `positions_and_distances` is a tuple of `x`, `y` & `point` iterators,, each with `(position, goal, distance)` values.
fn update_overlays<X, Y, P>(&mut self, responses: &mut VecDeque<Message>, positions_and_distances: (X, Y, P), closest_distance: DVec2, snapped_to_point: bool)
where
X: Iterator<Item = (DVec2, DVec2, f64)>,
Y: Iterator<Item = (DVec2, DVec2, f64)>,
P: Iterator<Item = (DVec2, DVec2, f64)>,
{
self.axis_index = 0;
self.point_index = 0;
let (x, y, points) = positions_and_distances;
if !snapped_to_point {
self.draw_alignment_lines(true, y, responses, closest_distance);
self.draw_alignment_lines(false, x, responses, closest_distance);
self.draw_snap_points(points, responses, closest_distance);
}
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, self.axis_index);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, self.point_index);
}
/// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept.
fn remove_unused_overlays(overlay_paths: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, remove_after_index: usize) {
while overlay_paths.len() > remove_after_index {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()));
}
}
/// Deletes all overlays
fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, 0);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, 0);
}
}
use glam::DVec2;
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
pub struct SnapManager {
point_targets: Option<Vec<DVec2>>,
bound_targets: Option<Vec<DVec2>>,
snap_overlays: SnapOverlays,
snap_x: bool,
snap_y: bool,
}
@ -193,7 +43,7 @@ impl SnapManager {
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
// Snap to a point if possible
let (clamped_closest_distance, snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
let (clamped_closest_distance, _snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
(min_points.1, true)
} else {
// Do not move if over snap tolerance
@ -206,8 +56,7 @@ impl SnapManager {
false,
)
};
self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance, snapped_to_point);
responses.add(OverlaysMessage::Draw);
clamped_closest_distance
}
@ -336,9 +185,9 @@ impl SnapManager {
/// Removes snap target data and overlays. Call this when snapping is done.
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_overlays.cleanup(responses);
self.bound_targets = None;
self.point_targets = None;
responses.add(OverlaysMessage::Draw);
}
}

View File

@ -1,13 +1,10 @@
use crate::application::generate_uuid;
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_DRAG_ANGLE};
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, SELECTION_DRAG_ANGLE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
use crate::messages::prelude::*;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::LayerId;
use document_legacy::Operation;
use graphene_core::raster::color::Color;
use graphene_core::renderer::Quad;
use glam::{DAffine2, DVec2};
@ -149,71 +146,6 @@ impl SelectedEdges {
}
}
/// Create a viewport relative bounding box overlay with no transform handles
pub fn add_bounding_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
path
}
/// Update the location of a bounding box with no handles
pub fn update_bounding_box(pos1: DVec2, pos2: DVec2, layer: &Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = layer.as_ref().cloned() {
let transform = transform_from_box(pos1, pos2, DAffine2::IDENTITY).to_cols_array();
let operation = Operation::SetLayerTransformInViewport { path, transform };
responses.add_front(DocumentMessage::Overlays(operation.into()));
}
}
/// Removes the bounding box overlay with no transform handles
pub fn remove_bounding_box(layer_path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = layer_path {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
/// Add the transform handle overlay
fn add_transform_handles(responses: &mut VecDeque<Message>) -> [Vec<LayerId>; 8] {
const EMPTY_VEC: Vec<LayerId> = Vec::new();
let mut transform_handle_paths = [EMPTY_VEC; 8];
for item in &mut transform_handle_paths {
let current_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: current_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
*item = current_path;
}
transform_handle_paths
}
/// Converts a bounding box to a rounded transform (with translation and scale)
pub fn transform_from_box(pos1: DVec2, pos2: DVec2, transform: DAffine2) -> DAffine2 {
let inverse = transform.inverse();
transform
* DAffine2::from_scale_angle_translation(
inverse.transform_vector2(transform.transform_vector2(pos2 - pos1).round()),
0.,
inverse.transform_point2(transform.transform_point2(pos1).round() - DVec2::splat(0.5)),
)
}
/// Aligns the mouse position to the closest axis
pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2 {
if axis_align {
@ -229,27 +161,17 @@ pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2
/// Contains info on the overlays for the bounding box and transform handles
#[derive(Clone, Debug, Default)]
pub struct BoundingBoxOverlays {
pub bounding_box: Vec<LayerId>,
pub transform_handles: [Vec<LayerId>; 8],
pub struct BoundingBoxManager {
pub bounds: [DVec2; 2],
pub transform: DAffine2,
pub original_bound_transform: DAffine2,
pub selected_edges: Option<SelectedEdges>,
pub original_transforms: OriginalTransforms,
pub opposite_pivot: DVec2,
pub center_of_transformation: DVec2,
}
impl BoundingBoxOverlays {
#[must_use]
pub fn new(responses: &mut VecDeque<Message>) -> Self {
Self {
bounding_box: add_bounding_box(responses),
transform_handles: add_transform_handles(responses),
..Default::default()
}
}
impl BoundingBoxManager {
/// Calculates the transformed handle positions based on the bounding box and the transform
pub fn evaluate_transform_handle_positions(&self) -> [DVec2; 8] {
let (left, top): (f64, f64) = self.bounds[0].into();
@ -267,20 +189,11 @@ impl BoundingBoxOverlays {
}
/// Update the position of the bounding box and transform handles
pub fn transform(&mut self, responses: &mut VecDeque<Message>) {
let transform = transform_from_box(self.bounds[0], self.bounds[1], self.transform).to_cols_array();
let path = self.bounding_box.clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
const BIAS: f64 = 0.0001;
for (position, path) in self.evaluate_transform_handle_positions().into_iter().zip(&self.transform_handles) {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
let path = path.clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
for position in self.evaluate_transform_handle_positions() {
overlay_context.square(position, false);
}
}
@ -355,14 +268,4 @@ impl BoundingBoxOverlays {
MouseCursorIcon::Default
}
}
/// Removes the overlays
pub fn delete(self, responses: &mut VecDeque<Message>) {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()));
responses.extend(
self.transform_handles
.iter()
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
);
}
}

View File

@ -1,4 +1,3 @@
use super::common_functionality::overlay_renderer::OverlayRenderer;
use super::common_functionality::shape_editor::ShapeState;
use super::utility_types::{tool_message_to_tool_type, ToolActionHandlerData, ToolFsmState};
use crate::application::generate_uuid;
@ -15,7 +14,6 @@ use graphene_core::raster::color::Color;
pub struct ToolMessageHandler {
pub tool_state: ToolFsmState,
pub transform_layer_handler: TransformLayerMessageHandler,
pub shape_overlay: OverlayRenderer,
pub shape_editor: ShapeState,
}
@ -92,7 +90,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
global_tool_data: &self.tool_state.document_tool_data,
input,
render_data: &render_data,
shape_overlay: &mut self.shape_overlay,
shape_editor: &mut self.shape_editor,
node_graph,
};
@ -175,7 +172,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
global_tool_data: &self.tool_state.document_tool_data,
input,
render_data: &render_data,
shape_overlay: &mut self.shape_overlay,
shape_editor: &mut self.shape_editor,
node_graph,
};
@ -246,7 +242,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
global_tool_data: &self.tool_state.document_tool_data,
input,
render_data: &render_data,
shape_overlay: &mut self.shape_overlay,
shape_editor: &mut self.shape_editor,
node_graph,
};

View File

@ -1,12 +1,13 @@
use super::tool_prelude::*;
use crate::application::generate_uuid;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::RenderData;
use glam::{IVec2, Vec2Swizzles};
#[derive(Default)]
@ -24,6 +25,8 @@ pub enum ArtboardToolMessage {
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
Overlays(OverlayContext),
// Tool-specific messages
DeleteSelected,
@ -77,6 +80,7 @@ impl ToolTransition for ArtboardTool {
EventToMessageMap {
document_dirty: Some(ArtboardToolMessage::DocumentIsDirty.into()),
tool_abort: Some(ArtboardToolMessage::Abort.into()),
overlay_provider: Some(|overlay_context| ArtboardToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
@ -93,7 +97,7 @@ enum ArtboardToolFsmState {
#[derive(Clone, Debug, Default)]
struct ArtboardToolData {
bounding_box_overlays: Option<BoundingBoxOverlays>,
bounding_box_manager: Option<BoundingBoxManager>,
selected_artboard: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
cursor: MouseCursorIcon,
@ -102,28 +106,8 @@ struct ArtboardToolData {
}
impl ArtboardToolData {
fn refresh_overlays(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let current_artboard = self.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer));
match (current_artboard, self.bounding_box_overlays.take()) {
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
(Some(bounds), paths) => {
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
bounding_box_overlays.bounds = bounds;
bounding_box_overlays.transform = document.metadata().document_to_viewport;
bounding_box_overlays.transform(responses);
self.bounding_box_overlays = Some(bounding_box_overlays);
responses.add(OverlaysMessage::Rerender);
}
_ => {}
};
}
fn check_dragging_bounds(&mut self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
let bounding_box = self.bounding_box_overlays.as_mut()?;
let bounding_box = self.bounding_box_manager.as_mut()?;
let edges = bounding_box.check_selected_edges(cursor)?;
let (top, bottom, left, right) = edges;
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
@ -142,7 +126,7 @@ impl ArtboardToolData {
.start_snap(document, input, document.bounding_boxes(None, Some(artboard.to_node()), render_data), snap_x, snap_y);
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
if let Some(bounds) = &mut self.bounding_box_overlays {
if let Some(bounds) = &mut self.bounding_box_manager {
bounds.center_of_transformation = (bounds.bounds[0] + bounds.bounds[1]) / 2.;
}
}
@ -174,7 +158,7 @@ impl ArtboardToolData {
}
fn resize_artboard(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, from_center: bool, constrain_square: bool) {
let Some(bounds) = &self.bounding_box_overlays else {
let Some(bounds) = &self.bounding_box_manager else {
return;
};
let Some(movement) = &bounds.selected_edges else {
@ -206,11 +190,20 @@ impl Fsm for ArtboardToolFsmState {
};
match (self, event) {
(state, ArtboardToolMessage::DocumentIsDirty) if state != ArtboardToolFsmState::Drawing => {
tool_data.refresh_overlays(document, responses);
(state, ArtboardToolMessage::Overlays(mut overlay_context)) if state != ArtboardToolFsmState::Drawing => {
if let Some(bounds) = tool_data.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer)) {
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = document.metadata().document_to_viewport;
bounding_box_manager.render_overlays(&mut overlay_context);
} else {
tool_data.bounding_box_manager.take();
}
self
}
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerDown) => {
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
@ -235,7 +228,7 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::ResizingBounds
}
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, .. }) => {
if let Some(bounds) = &tool_data.bounding_box_overlays {
if let Some(bounds) = &tool_data.bounding_box_manager {
let axis_align = input.keyboard.get(constrain_axis_or_aspect as usize);
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
@ -314,7 +307,7 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Drawing
}
(ArtboardToolFsmState::Ready, ArtboardToolMessage::PointerMove { .. }) => {
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
let cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
if tool_data.cursor != cursor {
tool_data.cursor = cursor;
@ -326,7 +319,7 @@ impl Fsm for ArtboardToolFsmState {
(ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
@ -335,20 +328,21 @@ impl Fsm for ArtboardToolFsmState {
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(OverlaysMessage::Draw);
ArtboardToolFsmState::Ready
}
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerUp) => {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
responses.add(OverlaysMessage::Draw);
ArtboardToolFsmState::Ready
}
@ -362,7 +356,7 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Ready
}
(_, ArtboardToolMessage::NudgeSelected { delta_x, delta_y }) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
responses.add(GraphOperationMessage::ResizeArtboard {
id: tool_data.selected_artboard.unwrap().to_node(),
location: DVec2::new(bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y).round().as_ivec2(),
@ -373,16 +367,13 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Ready
}
(_, ArtboardToolMessage::Abort) => {
if let Some(bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
bounding_box_overlays.delete(responses);
}
// Register properties when switching back to other tools
responses.add(PropertiesPanelMessage::SetActiveLayers {
paths: document.selected_layers().map(|path| path.to_vec()).collect(),
});
tool_data.snap_manager.cleanup(responses);
responses.add(OverlaysMessage::Draw);
ArtboardToolFsmState::Ready
}
_ => self,

View File

@ -1,13 +1,12 @@
use super::tool_prelude::*;
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD};
use crate::consts::{LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD};
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{Fill, Gradient, GradientType, PathStyle, RenderData, Stroke};
use document_legacy::LayerId;
use document_legacy::Operation;
use document_legacy::layers::style::{Fill, Gradient, GradientType, RenderData};
use graphene_core::raster::color::Color;
#[derive(Default)]
@ -31,6 +30,8 @@ pub enum GradientToolMessage {
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
Overlays(OverlayContext),
// Tool-specific messages
DeleteStop,
@ -124,99 +125,6 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
multiplied * bound_transform
}
/// Contains info on the overlays for a single gradient
#[derive(Clone, Debug, Default)]
pub struct GradientOverlay {
pub handles: [Vec<LayerId>; 2],
pub line: Vec<LayerId>,
pub steps: Vec<Vec<LayerId>>,
layer: LayerNodeIdentifier,
transform: DAffine2,
gradient: Gradient,
}
impl GradientOverlay {
fn generate_overlay_handle(translation: DVec2, responses: &mut VecDeque<Message>, selected: bool) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let size = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) };
let operation = Operation::AddEllipse {
path: path.clone(),
transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(),
style: PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), fill),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
path
}
fn generate_overlay_line(start: DVec2, end: DVec2, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let line_vector = end - start;
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = start;
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let operation = Operation::AddLine {
path: path.clone(),
transform,
style: PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
path
}
pub fn new(gradient: Gradient, dragging: Option<GradientDragTarget>, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Self {
let transform = gradient_space_transform(layer, document);
let Gradient { start, end, positions, .. } = &gradient;
let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)];
let line = Self::generate_overlay_line(start, end, responses);
let handles = [
Self::generate_overlay_handle(start, responses, dragging == Some(GradientDragTarget::Start)),
Self::generate_overlay_handle(end, responses, dragging == Some(GradientDragTarget::End)),
];
let not_at_end = |(_, x): &(_, f64)| x.abs() > f64::EPSILON * 1000. && (1. - x).abs() > f64::EPSILON * 1000.;
let create_step = |(index, pos)| Self::generate_overlay_handle(start.lerp(end, pos), responses, dragging == Some(GradientDragTarget::Step(index)));
let steps = positions.iter().map(|(pos, _)| *pos).enumerate().filter(not_at_end).map(create_step).collect();
Self {
handles,
steps,
line,
layer,
transform,
gradient,
}
}
pub fn delete_overlays(self, responses: &mut VecDeque<Message>) {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.line }.into()));
let [start, end] = self.handles;
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()));
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()));
for step in self.steps {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: step }.into()));
}
}
pub fn evaluate_gradient_start(&self) -> DVec2 {
self.transform.transform_point2(self.gradient.start)
}
pub fn evaluate_gradient_end(&self) -> DVec2 {
self.transform.transform_point2(self.gradient.end)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub enum GradientDragTarget {
Start,
@ -346,6 +254,7 @@ impl ToolTransition for GradientTool {
document_dirty: Some(GradientToolMessage::DocumentIsDirty.into()),
tool_abort: Some(GradientToolMessage::Abort.into()),
selection_changed: Some(GradientToolMessage::DocumentIsDirty.into()),
overlay_provider: Some(|overlay_context| GradientToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
@ -353,7 +262,6 @@ impl ToolTransition for GradientTool {
#[derive(Clone, Debug, Default)]
struct GradientToolData {
gradient_overlays: Vec<GradientOverlay>,
selected_gradient: Option<SelectedGradient>,
snap_manager: SnapManager,
drag_start: DVec2,
@ -383,21 +291,33 @@ impl Fsm for GradientToolFsmState {
match (self, event) {
(_, GradientToolMessage::DocumentIsDirty) => {
while let Some(overlay) = tool_data.gradient_overlays.pop() {
overlay.delete_overlays(responses);
}
if self != GradientToolFsmState::Drawing {
SelectedGradient::update(&mut tool_data.selected_gradient, document, responses);
}
self
}
(_, GradientToolMessage::Overlays(mut overlay_context)) => {
let selected = tool_data.selected_gradient.as_ref();
for layer in document.document_legacy.selected_visible_layers() {
if let Some(gradient) = get_gradient(layer, &document.document_legacy) {
let dragging = tool_data
.selected_gradient
.as_ref()
.and_then(|selected| if selected.layer == layer { Some(selected.dragging) } else { None });
tool_data.gradient_overlays.push(GradientOverlay::new(gradient, dragging, layer, document, responses))
let Some(gradient) = get_gradient(layer, &document.document_legacy) else { continue };
let transform = gradient_space_transform(layer, document);
let dragging = selected.filter(|selected| selected.layer == layer).map(|selected| selected.dragging);
let Gradient { start, end, positions, .. } = gradient;
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end);
overlay_context.handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.handle(end, dragging == Some(GradientDragTarget::End));
for (index, (position, _)) in positions.into_iter().enumerate() {
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
continue;
}
overlay_context.handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)));
}
}
@ -450,22 +370,23 @@ impl Fsm for GradientToolFsmState {
self
}
(_, GradientToolMessage::InsertStop) => {
for overlay in &tool_data.gradient_overlays {
for layer in document.document_legacy.selected_visible_layers() {
let Some(mut gradient) = get_gradient(layer, &document.document_legacy) else { continue };
let transform = gradient_space_transform(layer, document);
let mouse = input.mouse.position;
let (start, end) = (overlay.evaluate_gradient_start(), overlay.evaluate_gradient_end());
let (start, end) = (transform.transform_point2(gradient.start), transform.transform_point2(gradient.end));
// Compute the distance from the mouse to the gradient line in viewport space
let distance = (end - start).angle_between(mouse - start).sin() * (mouse - start).length();
// If click is on the line then insert point
if distance < SELECTION_THRESHOLD {
let mut gradient = overlay.gradient.clone();
if distance < (SELECTION_THRESHOLD * 2.) {
// Try and insert the new stop
if let Some(index) = gradient.insert_stop(mouse, overlay.transform) {
if let Some(index) = gradient.insert_stop(mouse, transform) {
document.backup_nonmut(responses);
let mut selected_gradient = SelectedGradient::new(gradient, overlay.layer, document);
let mut selected_gradient = SelectedGradient::new(gradient, layer, document);
// Select the new point
selected_gradient.dragging = GradientDragTarget::Step(index);
@ -487,36 +408,37 @@ impl Fsm for GradientToolFsmState {
let mouse = input.mouse.position;
tool_data.drag_start = mouse;
let tolerance = MANIPULATOR_GROUP_MARKER_SIZE.powi(2);
let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2);
let mut dragging = false;
for overlay in &tool_data.gradient_overlays {
for layer in document.document_legacy.selected_visible_layers() {
let Some(gradient) = get_gradient(layer, &document.document_legacy) else { continue };
let transform = gradient_space_transform(layer, document);
// Check for dragging step
for (index, (pos, _)) in overlay.gradient.positions.iter().enumerate() {
let pos = overlay.transform.transform_point2(overlay.gradient.start.lerp(overlay.gradient.end, *pos));
for (index, (pos, _)) in gradient.positions.iter().enumerate() {
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
if pos.distance_squared(mouse) < tolerance {
dragging = true;
tool_data.selected_gradient = Some(SelectedGradient {
layer: overlay.layer,
transform: overlay.transform,
gradient: overlay.gradient.clone(),
layer,
transform,
gradient: gradient.clone(),
dragging: GradientDragTarget::Step(index),
})
}
}
// Check dragging start or end handle
for (pos, dragging_target) in [
(overlay.evaluate_gradient_start(), GradientDragTarget::Start),
(overlay.evaluate_gradient_end(), GradientDragTarget::End),
] {
for (pos, dragging_target) in [(gradient.start, GradientDragTarget::Start), (gradient.end, GradientDragTarget::End)] {
let pos = transform.transform_point2(pos);
if pos.distance_squared(mouse) < tolerance {
dragging = true;
start_snap(&mut tool_data.snap_manager, document, input, render_data);
tool_data.selected_gradient = Some(SelectedGradient {
layer: overlay.layer,
transform: overlay.transform,
gradient: overlay.gradient.clone(),
layer,
transform,
gradient: gradient.clone(),
dragging: dragging_target,
})
}
@ -530,20 +452,10 @@ impl Fsm for GradientToolFsmState {
// Apply the gradient to the selected layer
if let Some(layer) = selected_layer {
// let is_bitmap = document
// .document_legacy
// .layer(&layer)
// .ok()
// .and_then(|layer| layer.as_layer().ok())
// .map_or(false, |layer| matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_)));
// if is_bitmap {
// return self;
// }
if !document.metadata().selected_layers_contains(layer) {
let replacement_selected_layers = vec![layer.to_path()];
let nodes = vec![layer.to_node()];
responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers });
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
}
responses.add(DocumentMessage::StartTransaction);
@ -592,10 +504,8 @@ impl Fsm for GradientToolFsmState {
(_, GradientToolMessage::Abort) => {
tool_data.snap_manager.cleanup(responses);
responses.add(OverlaysMessage::Draw);
while let Some(overlay) = tool_data.gradient_overlays.pop() {
overlay.delete_overlays(responses);
}
GradientToolFsmState::Ready
}
_ => self,

View File

@ -1,6 +1,5 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::node_graph::{self, IMAGINATE_NODE};
use crate::messages::tool::common_functionality::path_outline::PathOutline;
use crate::messages::tool::common_functionality::resize::Resize;
use document_legacy::document_metadata::LayerNodeIdentifier;
@ -96,7 +95,6 @@ enum ImaginateToolFsmState {
#[derive(Clone, Debug, Default)]
struct ImaginateToolData {
data: Resize,
path_outlines: PathOutline,
}
impl Fsm for ImaginateToolFsmState {
@ -118,14 +116,11 @@ impl Fsm for ImaginateToolFsmState {
};
match (self, event) {
(_, ImaginateToolMessage::DocumentIsDirty | ImaginateToolMessage::SelectionChanged) => {
tool_data.path_outlines.clear_selected(responses);
//tool_data.path_outlines.update_selected(document.document_legacy.selected_visible_layers(), document, responses, render_data);
self
}
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
tool_data.path_outlines.clear_selected(responses);
shape_data.start(responses, document, input, render_data);
responses.add(DocumentMessage::StartTransaction);
shape_data.layer = Some(LayerNodeIdentifier::new(generate_uuid(), document.network()));
@ -193,15 +188,10 @@ impl Fsm for ImaginateToolFsmState {
responses.add(DocumentMessage::AbortTransaction);
shape_data.cleanup(responses);
tool_data.path_outlines.clear_selected(responses);
ImaginateToolFsmState::Ready
}
(_, ImaginateToolMessage::Abort) => {
tool_data.path_outlines.clear_selected(responses);
ImaginateToolFsmState::Ready
}
(_, ImaginateToolMessage::Abort) => ImaginateToolFsmState::Ready,
_ => self,
}
}

View File

@ -1,18 +1,18 @@
use std::vec;
use super::tool_prelude::*;
use crate::consts::{DRAG_THRESHOLD, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_mirror_handles, get_subpaths};
use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer;
use crate::messages::tool::common_functionality::shape_editor::{ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::{add_bounding_box, remove_bounding_box, update_bounding_box};
use document_legacy::document::Document;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::LayerId;
use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use std::vec;
#[derive(Default)]
pub struct PathTool {
fsm_state: PathToolFsmState,
@ -27,7 +27,7 @@ pub enum PathToolMessage {
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
Overlays(OverlayContext),
#[remain::unsorted]
SelectionChanged,
@ -182,9 +182,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
impl ToolTransition for PathTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: Some(PathToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PathToolMessage::Abort.into()),
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
@ -205,34 +205,18 @@ struct PathToolData {
previous_mouse_position: DVec2,
alt_debounce: bool,
opposing_handle_lengths: Option<OpposingHandleLengths>,
drag_box_overlay_layer: Option<Vec<LayerId>>,
/// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle).
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
selection_status: SelectionStatus,
}
impl PathToolData {
fn refresh_overlays(&mut self, document: &DocumentMessageHandler, shape_editor: &mut ShapeState, shape_overlay: &mut OverlayRenderer, responses: &mut VecDeque<Message>) {
// Set the previously selected layers to invisible
for layer in document.metadata().all_layers() {
shape_overlay.layer_overlay_visibility(&document.document_legacy, layer, false, responses);
}
// Render the new overlays
for &layer in shape_editor.selected_shape_state.keys() {
shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer, responses);
}
self.opposing_handle_lengths = None;
}
fn mouse_down(
&mut self,
shift: bool,
shape_editor: &mut ShapeState,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_overlay: &mut OverlayRenderer,
responses: &mut VecDeque<Message>,
) -> PathToolFsmState {
self.opposing_handle_lengths = None;
@ -241,7 +225,7 @@ impl PathToolData {
// Select the first point within the threshold (in pixels)
if let Some(selected_points) = shape_editor.select_point(&document.document_legacy, input.mouse.position, SELECTION_THRESHOLD, shift) {
self.start_dragging_point(selected_points, input, document, responses);
self.refresh_overlays(document, shape_editor, shape_overlay, responses);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Dragging
}
@ -261,7 +245,6 @@ impl PathToolData {
// Start drawing a box
self.drag_start_pos = input.mouse.position;
self.previous_mouse_position = input.mouse.position;
self.drag_box_overlay_layer = Some(add_bounding_box(responses));
PathToolFsmState::DrawingBox
}
@ -328,13 +311,7 @@ impl Fsm for PathToolFsmState {
type ToolOptions = ();
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData {
document,
input,
shape_editor,
shape_overlay,
..
} = tool_action_data;
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
let ToolMessage::Path(event) = event else {
return self;
};
@ -345,17 +322,17 @@ impl Fsm for PathToolFsmState {
let target_layers = document.metadata().selected_layers().collect();
shape_editor.set_selected_layers(target_layers);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated);
// This can happen in any state (which is why we return self)
self
}
(_, PathToolMessage::DocumentIsDirty) => {
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer in document.metadata().selected_layers() {
shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer, responses);
(_, PathToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context);
if self == Self::DrawingBox {
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]))
}
responses.add(PathToolMessage::SelectedPointUpdated);
@ -366,11 +343,11 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::DragStart { add_to_selection }) => {
let shift = input.keyboard.get(add_to_selection as usize);
tool_data.mouse_down(shift, shape_editor, document, input, shape_overlay, responses)
tool_data.mouse_down(shift, shape_editor, document, input, responses)
}
(PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { .. }) => {
tool_data.previous_mouse_position = input.mouse.position;
update_bounding_box(tool_data.drag_start_pos, input.mouse.position, &tool_data.drag_box_overlay_layer, responses);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::DrawingBox
}
@ -389,9 +366,8 @@ impl Fsm for PathToolFsmState {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else {
shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
};
remove_bounding_box(tool_data.drag_box_overlay_layer.take(), responses);
}
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready
}
@ -404,11 +380,10 @@ impl Fsm for PathToolFsmState {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else {
shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
};
remove_bounding_box(tool_data.drag_box_overlay_layer.take(), responses);
}
responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated);
PathToolFsmState::Ready
}
@ -426,7 +401,7 @@ impl Fsm for PathToolFsmState {
if clicked_selected {
shape_editor.deselect_all();
shape_editor.select_point(&document.document_legacy, input.mouse.position, SELECTION_THRESHOLD, false);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
responses.add(OverlaysMessage::Draw);
}
}
@ -455,9 +430,7 @@ impl Fsm for PathToolFsmState {
self
}
(_, PathToolMessage::Abort) => {
// TODO Tell overlay manager to remove the overlays
shape_overlay.clear_all_overlays(responses);
remove_bounding_box(tool_data.drag_box_overlay_layer.take(), responses);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready
}
@ -469,7 +442,7 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::SelectAllPoints) => {
shape_editor.select_all_points(&document.document_legacy);
tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses);
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready
}
(_, PathToolMessage::SelectedPointXChanged { new_x }) => {

View File

@ -1,6 +1,8 @@
use super::tool_prelude::*;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::portfolio::document::node_graph::VectorDataModification;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
@ -43,13 +45,13 @@ pub enum PenToolMessage {
#[remain::unsorted]
CanvasTransformed,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
Abort,
#[remain::unsorted]
SelectionChanged,
#[remain::unsorted]
WorkingColorChanged,
#[remain::unsorted]
Overlays(OverlayContext),
// Tool-specific messages
Confirm,
@ -184,10 +186,11 @@ impl ToolTransition for PenTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
canvas_transformed: Some(PenToolMessage::CanvasTransformed.into()),
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
}
@ -542,7 +545,6 @@ impl Fsm for PenToolFsmState {
input,
render_data,
shape_editor,
shape_overlay,
..
} = tool_action_data;
@ -569,24 +571,13 @@ impl Fsm for PenToolFsmState {
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
self
}
(_, PenToolMessage::DocumentIsDirty) => {
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer in document.metadata().selected_layers() {
shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer, responses);
}
(_, PenToolMessage::SelectionChanged) => {
responses.add(OverlaysMessage::Draw);
self
}
(_, PenToolMessage::SelectionChanged) => {
// Set the previously selected layers to invisible
for layer in document.metadata().all_layers() {
shape_overlay.layer_overlay_visibility(&document.document_legacy, layer, false, responses);
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context);
// Redraw the overlays of the newly selected layers
for layer in document.metadata().selected_layers() {
shape_overlay.render_subpath_overlays(&shape_editor.selected_shape_state, &document.document_legacy, layer, responses);
}
self
}
(_, PenToolMessage::WorkingColorChanged) => {
@ -660,8 +651,7 @@ impl Fsm for PenToolFsmState {
PenToolFsmState::Ready
}
(_, PenToolMessage::Abort) => {
// Clean up overlays
shape_overlay.clear_all_overlays(responses);
responses.add(OverlaysMessage::Draw);
self
}

View File

@ -1,18 +1,18 @@
#![allow(clippy::too_many_arguments)]
use super::tool_prelude::*;
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
use crate::messages::tool::common_functionality::path_outline::*;
use crate::messages::tool::common_functionality::pivot::Pivot;
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
use crate::messages::tool::common_functionality::transformation_cage::*;
use document_legacy::document::Document;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::LayerId;
use document_legacy::Operation;
use graphene_core::renderer::Quad;
use std::fmt;
@ -62,6 +62,8 @@ pub enum SelectToolMessage {
DocumentIsDirty,
#[remain::unsorted]
SelectionChanged,
#[remain::unsorted]
Overlays(OverlayContext),
// Tool-specific messages
DragStart {
@ -241,6 +243,7 @@ impl ToolTransition for SelectTool {
document_dirty: Some(SelectToolMessage::DocumentIsDirty.into()),
tool_abort: Some(SelectToolMessage::Abort.into()),
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
@ -266,9 +269,7 @@ struct SelectToolData {
select_single_layer: Option<LayerNodeIdentifier>,
has_dragged: bool,
not_duplicated_layers: Option<Vec<LayerNodeIdentifier>>,
drag_box_overlay_layer: Option<Vec<LayerId>>,
path_outlines: PathOutline,
bounding_box_overlays: Option<BoundingBoxOverlays>,
bounding_box_manager: Option<BoundingBoxManager>,
snap_manager: SnapManager,
cursor: MouseCursorIcon,
pivot: Pivot,
@ -387,30 +388,53 @@ impl Fsm for SelectToolFsmState {
return self;
};
match (self, event) {
(_, SelectToolMessage::DocumentIsDirty | SelectToolMessage::SelectionChanged) => {
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
let selected_layers_count = document.metadata().selected_layers().count();
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
tool_data.selected_layers_count = selected_layers_count;
tool_data.path_outlines.update_selected(document.document_legacy.selected_visible_layers(), document, responses);
tool_data.path_outlines.intersect_test_hovered(input, document, responses);
// Outline selected layers
for layer in document.document_legacy.selected_visible_layers() {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
}
match (document.document_legacy.selected_visible_layers_bounding_box_viewport(), tool_data.bounding_box_overlays.take()) {
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(responses),
(Some(bounds), paths) => {
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(responses));
// Get the layer the user is hovering over
let click = document.document_legacy.click(input.mouse.position, &document.document_legacy.document_network);
let not_selected_click = click.filter(|&hovered_layer| !document.metadata().selected_layers_contains(hovered_layer));
if let Some(layer) = not_selected_click {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
}
bounding_box_overlays.bounds = bounds;
bounding_box_overlays.transform = DAffine2::IDENTITY;
// Update bounds
let transform = document.document_legacy.selected_visible_layers().next().map(|layer| document.metadata().transform_to_viewport(layer));
let transform = transform.unwrap_or(DAffine2::IDENTITY);
let bounds = document
.document_legacy
.selected_visible_layers()
.filter_map(|layer| {
document
.metadata()
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
})
.reduce(graphene_core::renderer::Quad::combine_bounds);
if let Some(bounds) = bounds {
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
bounding_box_overlays.transform(responses);
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = transform;
tool_data.bounding_box_overlays = Some(bounding_box_overlays);
}
(_, _) => {}
};
bounding_box_manager.render_overlays(&mut overlay_context);
} else {
tool_data.bounding_box_manager.take();
}
tool_data.pivot.update_pivot(document, responses);
// Update pivot
tool_data.pivot.update_pivot(document, &mut overlay_context);
// Update dragging box
if self == Self::DrawingBox {
overlay_context.quad(Quad::from_box([tool_data.drag_start, tool_data.drag_current]));
}
self
}
@ -426,12 +450,10 @@ impl Fsm for SelectToolFsmState {
self
}
(SelectToolFsmState::Ready, SelectToolMessage::DragStart { add_to_selection, select_deepest: _ }) => {
tool_data.path_outlines.clear_hovered(responses);
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
let dragging_bounds = tool_data.bounding_box_overlays.as_mut().and_then(|bounding_box| {
let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
@ -444,7 +466,7 @@ impl Fsm for SelectToolFsmState {
});
let rotating_bounds = tool_data
.bounding_box_overlays
.bounding_box_manager
.as_ref()
.map(|bounding_box| bounding_box.check_rotate(input.mouse.position))
.unwrap_or_default();
@ -479,8 +501,9 @@ impl Fsm for SelectToolFsmState {
tool_data.layers_dragging = selected;
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
let document = &document.document_legacy;
bounds.original_bound_transform = bounds.transform;
tool_data.layers_dragging.retain(|layer| document.document_network.nodes.contains_key(&layer.to_node()));
let mut selected = Selected::new(
@ -499,7 +522,7 @@ impl Fsm for SelectToolFsmState {
} else if rotating_bounds {
responses.add(DocumentMessage::StartTransaction);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
let mut selected = Selected::new(
&mut bounds.original_transforms,
@ -556,7 +579,6 @@ impl Fsm for SelectToolFsmState {
responses.add(DocumentMessage::DeselectAllLayers);
tool_data.layers_dragging.clear();
}
tool_data.drag_box_overlay_layer = Some(add_bounding_box(responses));
SelectToolFsmState::DrawingBox
}
};
@ -601,7 +623,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Dragging
}
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
if let Some(movement) = &mut bounds.selected_edges {
let (center, axis_align) = (input.keyboard.key(center), input.keyboard.key(axis_align));
@ -609,20 +631,23 @@ impl Fsm for SelectToolFsmState {
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
let (position, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, bounds.center_of_transformation, axis_align);
let (delta, mut _pivot) = movement.bounds_to_scale_transform(position, size);
let (position, size) = movement.new_size(snapped_mouse_position, bounds.original_bound_transform, center, bounds.center_of_transformation, axis_align);
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
let pivot_transform = DAffine2::from_translation(pivot);
let transformation = pivot_transform * delta * pivot_transform.inverse();
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
let selected = &tool_data.layers_dragging;
let mut selected = Selected::new(&mut bounds.original_transforms, &mut _pivot, selected, responses, &document.document_legacy, None, &ToolType::Select);
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, selected, responses, &document.document_legacy, None, &ToolType::Select);
selected.update_transforms(delta);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse());
}
}
SelectToolFsmState::ResizingBounds
}
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { snap_angle, .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
let angle = {
let start_offset = tool_data.drag_start - bounds.center_of_transformation;
let end_offset = input.mouse.position - bounds.center_of_transformation;
@ -664,30 +689,20 @@ impl Fsm for SelectToolFsmState {
}
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove { .. }) => {
tool_data.drag_current = input.mouse.position;
responses.add(OverlaysMessage::Draw);
responses.add_front(DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: tool_data.drag_box_overlay_layer.clone().unwrap(),
transform: transform_from_box(tool_data.drag_start, tool_data.drag_current, DAffine2::IDENTITY).to_cols_array(),
}
.into(),
));
SelectToolFsmState::DrawingBox
}
(SelectToolFsmState::Ready, SelectToolMessage::PointerMove { .. }) => {
let mut cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
// Dragging the pivot overrules the other operations
if tool_data.pivot.is_over(input.mouse.position) {
cursor = MouseCursorIcon::Move;
}
// Generate the select outline (but not if the user is going to use the bound overlays)
if cursor == MouseCursorIcon::Default {
tool_data.path_outlines.intersect_test_hovered(input, document, responses);
} else {
tool_data.path_outlines.clear_hovered(responses);
}
// Generate the hover outline
responses.add(OverlaysMessage::Draw);
if tool_data.cursor != cursor {
tool_data.cursor = cursor;
@ -748,7 +763,7 @@ impl Fsm for SelectToolFsmState {
tool_data.snap_manager.cleanup(responses);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
@ -761,7 +776,7 @@ impl Fsm for SelectToolFsmState {
};
responses.add(response);
if let Some(bounds) = &mut tool_data.bounding_box_overlays {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
@ -789,13 +804,8 @@ impl Fsm for SelectToolFsmState {
nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(),
});
}
responses.add(OverlaysMessage::Draw);
responses.add_front(DocumentMessage::Overlays(
Operation::DeleteLayer {
path: tool_data.drag_box_overlay_layer.take().unwrap(),
}
.into(),
));
SelectToolFsmState::Ready
}
(SelectToolFsmState::Ready, SelectToolMessage::Enter) => {
@ -814,18 +824,13 @@ impl Fsm for SelectToolFsmState {
(SelectToolFsmState::Dragging, SelectToolMessage::Abort) => {
tool_data.snap_manager.cleanup(responses);
responses.add(DocumentMessage::Undo);
tool_data.path_outlines.clear_selected(responses);
tool_data.pivot.clear_overlays(responses);
responses.add(OverlaysMessage::Draw);
SelectToolFsmState::Ready
}
(_, SelectToolMessage::Abort) => {
if let Some(path) = tool_data.drag_box_overlay_layer.take() {
responses.add_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()))
};
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
if let Some(mut bounding_box_overlays) = tool_data.bounding_box_overlays.take() {
if let Some(mut bounding_box_overlays) = tool_data.bounding_box_manager.take() {
let mut selected = Selected::new(
&mut bounding_box_overlays.original_transforms,
&mut bounding_box_overlays.opposite_pivot,
@ -837,13 +842,9 @@ impl Fsm for SelectToolFsmState {
);
selected.revert_operation();
bounding_box_overlays.delete(responses);
}
tool_data.path_outlines.clear_hovered(responses);
tool_data.path_outlines.clear_selected(responses);
tool_data.pivot.clear_overlays(responses);
responses.add(OverlaysMessage::Draw);
tool_data.snap_manager.cleanup(responses);
SelectToolFsmState::Ready

View File

@ -1,16 +1,15 @@
#![allow(clippy::too_many_arguments)]
use super::tool_prelude::*;
use crate::application::generate_uuid;
use crate::consts::COLOR_ACCENT;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::intersection::Quad;
use document_legacy::layers::style::{self, Fill, RenderData, Stroke};
use document_legacy::LayerId;
use document_legacy::Operation;
use document_legacy::layers::style::{Fill, RenderData};
use graph_craft::document::value::TaggedValue;
use graphene_core::renderer::Quad;
use graphene_core::text::{load_face, Font};
use graphene_core::Color;
@ -50,6 +49,8 @@ pub enum TextToolMessage {
DocumentIsDirty,
#[remain::unsorted]
WorkingColorChanged,
#[remain::unsorted]
Overlays(OverlayContext),
// Tool-specific messages
CommitText,
@ -194,6 +195,7 @@ impl ToolTransition for TextTool {
tool_abort: Some(TextToolMessage::Abort.into()),
selection_changed: Some(TextToolMessage::DocumentIsDirty.into()),
working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| TextToolMessage::Overlays(overlay_context).into()),
}
}
}
@ -216,7 +218,6 @@ pub struct EditingText {
#[derive(Clone, Debug, Default)]
struct TextToolData {
layer: LayerNodeIdentifier,
overlays: Vec<Vec<LayerId>>,
editing_text: Option<EditingText>,
new_text: String,
}
@ -317,31 +318,10 @@ impl TextToolData {
// Removing old text as editable
self.set_editing(false, render_data, document, responses);
resize_overlays(&mut self.overlays, responses, 0);
TextToolFsmState::Ready
}
}
pub fn update_bounds_overlay(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> Option<()> {
resize_overlays(&mut self.overlays, responses, 1);
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(&self.new_text, buzz_face, editing_text.font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(self.layer) * quad;
let bounds = transformed_quad.bounding_box();
let operation = Operation::SetLayerTransformInViewport {
path: self.overlays[0].clone(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.add(DocumentMessage::Overlays(operation.into()));
Some(())
}
fn get_bounds(&self, text: &str, render_data: &RenderData) -> Option<[DVec2; 2]> {
let editing_text = self.editing_text.as_ref()?;
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
@ -361,53 +341,6 @@ impl TextToolData {
}
}
fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array()
}
fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, newlen: usize) {
while overlays.len() > newlen {
let operation = Operation::DeleteLayer { path: overlays.pop().unwrap() };
responses.add(DocumentMessage::Overlays(operation.into()));
}
while overlays.len() < newlen {
let path = vec![generate_uuid()];
overlays.push(path.clone());
let operation = Operation::AddRect {
path,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, render_data: &RenderData) {
let get_bounds = |layer: LayerNodeIdentifier, document: &DocumentMessageHandler, render_data: &RenderData| {
let (text, font, font_size) = graph_modification_utils::get_text(layer, &document.document_legacy)?;
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
Some(multiplied.bounding_box())
};
let bounds = document.metadata().selected_layers().filter_map(|layer| get_bounds(layer, document, render_data));
let bounds = bounds.collect::<Vec<_>>();
let new_len = bounds.len();
for (bounds, overlay_path) in bounds.iter().zip(&tool_data.overlays) {
let operation = Operation::SetLayerTransformInViewport {
path: overlay_path.to_vec(),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.add(DocumentMessage::Overlays(operation.into()));
}
resize_overlays(&mut tool_data.overlays, responses, new_len);
}
fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
let mut selected_layers = document.metadata().selected_layers();
@ -440,17 +373,35 @@ impl Fsm for TextToolFsmState {
return self;
};
match (self, event) {
(TextToolFsmState::Editing, TextToolMessage::DocumentIsDirty) => {
(TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => {
responses.add(FrontendMessage::DisplayEditableTextboxTransform {
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
});
tool_data.update_bounds_overlay(document, render_data, responses);
if let Some(editing_text) = tool_data.editing_text.as_ref() {
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.font_size, None);
if far.x != 0. && far.y != 0. {
let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
overlay_context.quad(transformed_quad);
}
}
TextToolFsmState::Editing
}
(state, TextToolMessage::DocumentIsDirty) => {
update_overlays(document, tool_data, responses, render_data);
(_, TextToolMessage::Overlays(mut overlay_context)) => {
for layer in document.metadata().selected_layers() {
let Some((text, font, font_size)) = graph_modification_utils::get_text(layer, &document.document_legacy) else {
continue;
};
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
let quad = Quad::from_box([DVec2::ZERO, far]);
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
overlay_context.quad(multiplied);
}
state
self
}
(state, TextToolMessage::Interact) => {
tool_data.editing_text = Some(EditingText {
@ -477,8 +428,6 @@ impl Fsm for TextToolFsmState {
tool_data.set_editing(false, render_data, document, responses);
}
resize_overlays(&mut tool_data.overlays, responses, 0);
TextToolFsmState::Ready
}
(TextToolFsmState::Editing, TextToolMessage::CommitText) => {
@ -497,13 +446,11 @@ impl Fsm for TextToolFsmState {
tool_data.set_editing(false, render_data, document, responses);
resize_overlays(&mut tool_data.overlays, responses, 0);
TextToolFsmState::Ready
}
(TextToolFsmState::Editing, TextToolMessage::UpdateBounds { new_text }) => {
tool_data.new_text = new_text;
tool_data.update_bounds_overlay(document, render_data, responses);
responses.add(OverlaysMessage::Draw);
TextToolFsmState::Editing
}
(_, TextToolMessage::WorkingColorChanged) => {

View File

@ -1,5 +1,5 @@
#![allow(clippy::too_many_arguments)]
use super::common_functionality::overlay_renderer::OverlayRenderer;
use super::common_functionality::shape_editor::ShapeState;
use super::tool_messages::*;
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
@ -8,6 +8,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGrou
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::prelude::*;
use crate::node_graph_executor::NodeGraphExecutor;
@ -23,7 +24,6 @@ pub struct ToolActionHandlerData<'a> {
pub global_tool_data: &'a DocumentToolData,
pub input: &'a InputPreprocessorMessageHandler,
pub render_data: &'a RenderData<'a>,
pub shape_overlay: &'a mut OverlayRenderer,
pub shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor,
}
@ -34,7 +34,6 @@ impl<'a> ToolActionHandlerData<'a> {
global_tool_data: &'a DocumentToolData,
input: &'a InputPreprocessorMessageHandler,
render_data: &'a RenderData<'a>,
shape_overlay: &'a mut OverlayRenderer,
shape_editor: &'a mut ShapeState,
node_graph: &'a NodeGraphExecutor,
) -> Self {
@ -44,7 +43,6 @@ impl<'a> ToolActionHandlerData<'a> {
global_tool_data,
input,
render_data,
shape_overlay,
shape_editor,
node_graph,
}
@ -174,6 +172,7 @@ pub struct EventToMessageMap {
pub selection_changed: Option<ToolMessage>,
pub tool_abort: Option<ToolMessage>,
pub working_color_changed: Option<ToolMessage>,
pub overlay_provider: Option<OverlayProvider>,
}
pub trait ToolTransition {
@ -195,6 +194,9 @@ pub trait ToolTransition {
subscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
subscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
subscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
responses.add(OverlaysMessage::AddProvider(overlay_provider));
}
}
fn deactivate(&self, responses: &mut VecDeque<Message>) {
@ -213,6 +215,9 @@ pub trait ToolTransition {
unsubscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
unsubscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
unsubscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
responses.add(OverlaysMessage::RemoveProvider(overlay_provider));
}
}
}

View File

@ -4,6 +4,7 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::node_graph::wrap_network_in_scope;
use crate::messages::portfolio::document::utility_types::misc::{LayerMetadata, LayerPanelEntry};
use crate::messages::prelude::*;
use document_legacy::document::Document as DocumentLegacy;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::layer_info::{LayerDataTypeDiscriminant, LegacyLayerType};
@ -21,7 +22,6 @@ use graphene_core::text::FontCache;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData;
use graphene_core::{Color, GraphicElement, SurfaceFrame, SurfaceId};
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::DynamicExecutor;
@ -654,7 +654,7 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(DocumentMessage::DirtyRenderDocument);
responses.add(DocumentMessage::Overlays(OverlaysMessage::Rerender));
responses.add(OverlaysMessage::Draw);
}
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::ResendActiveProperties))
@ -679,7 +679,7 @@ impl NodeGraphExecutor {
let svg = render.svg.to_string();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
@ -692,7 +692,7 @@ impl NodeGraphExecutor {
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) => {
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
responses.add(DocumentMessage::RenderScrollbars);
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::CanvasFrame(frame)) => {
@ -710,7 +710,7 @@ impl NodeGraphExecutor {
"#,
1920, 1080, matrix, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
TaggedValue::Bool(render_object) => Self::render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::render(render_object, transform, responses),

View File

@ -13,14 +13,11 @@
DisplayRemoveEditableTextbox,
TriggerTextCommit,
TriggerViewportResize,
UpdateDocumentArtboards,
UpdateDocumentArtwork,
UpdateDocumentOverlays,
UpdateDocumentRulers,
UpdateDocumentScrollbars,
UpdateEyedropperSamplingState,
UpdateMouseCursor,
UpdateDocumentNodeRender,
isWidgetSpanRow,
} from "@graphite/wasm-communication/messages";
@ -62,9 +59,6 @@
// Rendered SVG viewport data
let artworkSvg = "";
let nodeRenderSvg = "";
let artboardSvg = "";
let overlaysSvg = "";
// Rasterized SVG viewport data, or none if it's not up-to-date
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
@ -160,7 +154,12 @@
// Update rendered SVGs
export async function updateDocumentArtwork(svg: string) {
artworkSvg = svg;
// TODO: Sort this out so we're either sending only the SVG inner contents from the backend or not setting the width/height attributes here
// TODO: (but preserving the rounding-up-to-the-next-even-number to prevent antialiasing).
artworkSvg = svg
.trim()
.replace(/<svg[^>]*>/, "")
.slice(0, -"</svg>".length);
rasterizedCanvas = undefined;
await tick();
@ -177,20 +176,6 @@
});
}
export function updateDocumentOverlays(svg: string) {
overlaysSvg = svg;
}
export function updateDocumentArtboards(svg: string) {
artboardSvg = svg;
rasterizedCanvas = undefined;
}
export function updateDocumentNodeRender(svg: string) {
nodeRenderSvg = svg;
rasterizedCanvas = undefined;
}
export async function updateEyedropperSamplingState(mousePosition: XY | undefined, colorPrimary: string, colorSecondary: string): Promise<[number, number, number] | undefined> {
if (mousePosition === undefined) {
cursorEyedropper = false;
@ -211,7 +196,7 @@
const outsideArtboards = `<rect x="0" y="0" width="100%" height="100%" fill="${outsideArtboardsColor}" />`;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${outsideArtboards}${artboardSvg}${nodeRenderSvg}</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${outsideArtboards}${artworkSvg}</svg>
`.trim();
if (!rasterizedCanvas) {
@ -370,21 +355,6 @@
updateDocumentArtwork(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentOverlays, async (data) => {
await tick();
updateDocumentOverlays(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtboards, async (data) => {
await tick();
updateDocumentArtboards(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateDocumentNodeRender, async (data) => {
await tick();
updateDocumentNodeRender(data.svg);
});
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => {
await tick();
@ -509,22 +479,14 @@
{/if}
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} on:dragover={(e) => e.preventDefault()} on:drop={(e) => pasteFile(e)} bind:this={viewport} data-viewport>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artboardSvg}
</svg>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html nodeRenderSvg}
</svg>
<svg class="artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artworkSvg}
</svg>
<svg class="overlays" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html overlaysSvg}
</svg>
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
{#if showTextInput}
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" />
{/if}
</div>
<canvas class="overlays" style:width={canvasWidthCSS} style:height={canvasHeightCSS} data-overlays-canvas></canvas>
</div>
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork="80%" data-graph>
<Graph />
@ -701,19 +663,17 @@
position: relative;
overflow: hidden;
svg {
.artwork,
.text-input,
.overlays {
position: absolute;
top: 0;
// Fallback values if JS hasn't set these to integers yet
width: 100%;
height: 100%;
// Allows dev tools to select the artwork without being blocked by the SVG containers
pointer-events: none;
canvas {
width: 100%;
height: 100%;
}
// Prevent inheritance from reaching the child elements
> * {
pointer-events: auto;

View File

@ -449,18 +449,6 @@ export class UpdateDocumentArtwork extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentOverlays extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentArtboards extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentNodeRender extends JsMessage {
readonly svg!: string;
}
export class UpdateDocumentScrollbars extends JsMessage {
@TupleToVec2
readonly position!: XY;
@ -1416,14 +1404,11 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDialogButtons,
UpdateDialogColumn1,
UpdateDialogColumn2,
UpdateDocumentArtboards,
UpdateDocumentArtwork,
UpdateDocumentBarLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructureJs: newUpdateDocumentLayerTreeStructure,
UpdateDocumentModeLayout,
UpdateDocumentNodeRender,
UpdateDocumentOverlays,
UpdateDocumentRulers,
UpdateDocumentScrollbars,
UpdateEyedropperSamplingState,

View File

@ -1,16 +1,16 @@
mod quad;
use crate::raster::{BlendMode, Image, ImageFrame};
use crate::uuid::{generate_uuid, ManipulatorGroupId};
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
use base64::Engine;
use bezier_rs::Subpath;
pub use quad::Quad;
use bezier_rs::Subpath;
use base64::Engine;
use glam::{DAffine2, DVec2};
use usvg::TreeParsing;
mod quad;
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
pub struct ClickTarget {
@ -83,7 +83,7 @@ impl SvgRender {
self.svg.push("\t".repeat(self.indent));
}
/// Add an outer `<svg />` tag with a `viewBox` and the `<defs />`
/// Add an outer `<svg>...</svg>` tag with a `viewBox` and the `<defs />`
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
let (x, y) = bounds_min.into();
let (size_x, size_y) = (bounds_max - bounds_min).into();
@ -93,7 +93,7 @@ impl SvgRender {
self.svg.push("</svg>");
}
/// Wraps the SVG with `<svg><g transform="...">`, which allows for rotation
/// Wraps the SVG with `<svg><g transform="...">...</g></svg>`, which allows for rotation
pub fn wrap_with_transform(&mut self, transform: DAffine2, size: Option<DVec2>) {
let defs = &self.svg_defs;
let view_box = size

View File

@ -2,7 +2,7 @@ use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy)]
/// A quad defined by four vertices.
pub struct Quad([DVec2; 4]);
pub struct Quad(pub [DVec2; 4]);
impl Quad {
/// Create a zero sized quad at the point

View File

@ -1,18 +1,18 @@
use std::cell::RefCell;
use core::future::Future;
use dyn_any::StaticType;
use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportFormat, RenderConfig, ResourceFuture, SurfaceHandle, SurfaceHandleFrame, SurfaceId};
use graphene_core::raster::Image;
use graphene_core::raster::{color::SRGBA8, ImageFrame};
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
use graphene_core::transform::Footprint;
use graphene_core::Color;
use graphene_core::{
raster::{color::SRGBA8, ImageFrame},
Node,
};
use graphene_core::Node;
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
use core::future::Future;
#[cfg(target_arch = "wasm32")]
use js_sys::{Object, Reflect};
use std::cell::RefCell;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::pin::Pin;
@ -25,8 +25,6 @@ use wasm_bindgen::{Clamped, JsCast};
#[cfg(target_arch = "wasm32")]
use web_sys::window;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
pub struct Canvas(CanvasRenderingContext2d);
@ -293,7 +291,6 @@ pub struct RenderNode<Data, Surface, Parameter> {
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutput {
if !data.contains_artboard() && !render_params.hide_artboards {
render.leaf_tag("rect", |attributes| {
attributes.push("x", "0");
attributes.push("x", "0");
attributes.push("y", "0");
attributes.push("width", footprint.resolution.x.to_string());

View File

@ -107,10 +107,6 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 4" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Basic brush tool</span>
</div>
<div class="informational complete" title="Development Complete">
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Resolution-agnostic raster rendering</span>
</div>
<div class="informational complete" title="Development Complete">
<img class="atlas" style="--atlas-index: 2" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Graph-based layer stacks</span>
@ -119,14 +115,18 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 5" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Fully graph-driven documents</span>
</div>
<div class="informational complete" title="Development Complete">
<img class="atlas" style="--atlas-index: 13" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>New viewport overlays system</span>
</div>
<div class="informational ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Resolution-agnostic raster rendering</span>
</div>
<div class="informational ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 11" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>WebGPU accelerated rendering</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 13" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>New viewport overlays system</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 15" src="https://static.graphite.rs/icons/icon-atlas-roadmap.png" alt="" />
<span>Snapping between layers</span>