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