Merge branch 'master' into merge_point

This commit is contained in:
Adesh Gupta
2025-08-19 01:39:28 +05:30
committed by GitHub
214 changed files with 4701 additions and 19556 deletions
+4 -8
View File
@@ -12,18 +12,18 @@ license = "Apache-2.0"
[features]
default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"]
ron = ["dep:ron"]
ron = []
[dependencies]
# Local dependencies
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true }
graphene-std = { workspace = true } # NOTE: `graphene-core` should not be added here because `graphene-std` re-exports its contents
preprocessor = { workspace = true }
# Workspace dependencies
@@ -33,7 +33,6 @@ bitflags = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true }
glam = { workspace = true }
@@ -44,9 +43,8 @@ num_enum = { workspace = true }
usvg = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true }
bytemuck = { workspace = true }
vello = { workspace = true }
tracing = { workspace = true }
base64 = { workspace = true }
# Required dependencies
spin = "0.9.8"
@@ -56,8 +54,6 @@ wgpu-executor = { workspace = true, optional = true }
# Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
[dev-dependencies]
# Workspace dependencies
-4
View File
@@ -26,7 +26,6 @@ pub struct DispatcherMessageHandlers {
pub portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler,
workspace_message_handler: WorkspaceMessageHandler,
}
impl DispatcherMessageHandlers {
@@ -231,9 +230,6 @@ impl Dispatcher {
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
}
Message::Workspace(message) => {
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
}
Message::NoOp => {}
Message::Batched { messages } => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
@@ -38,9 +38,9 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
for (_, message) in elements.rev() {
responses.add_front(message);
}
for (id, messages) in self.after_graph_run.iter() {
for (&document_id, messages) in self.after_graph_run.iter() {
if !messages.is_empty() {
responses.add(PortfolioMessage::SubmitGraphRender { document_id: *id, ignore_hash: false });
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
}
}
}
@@ -149,11 +149,16 @@ pub enum FrontendMessage {
UpdateGraphViewOverlay {
open: bool,
},
UpdateSpreadsheetState {
UpdateDataPanelState {
open: bool,
node: Option<NodeId>,
},
UpdateSpreadsheetLayout {
UpdatePropertiesPanelState {
open: bool,
},
UpdateLayersPanelState {
open: bool,
},
UpdateDataPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
@@ -296,7 +301,7 @@ pub enum FrontendMessage {
#[serde(rename = "openDocuments")]
open_documents: Vec<FrontendDocumentDetails>,
},
UpdatePropertyPanelSectionsLayout {
UpdatePropertiesPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
@@ -427,6 +427,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
//
// FrontendMessage
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
@@ -308,6 +308,7 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::ImageLabel(_) => {}
Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = match action {
@@ -481,18 +482,18 @@ impl LayoutMessageHandler {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target {
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },
@@ -42,9 +42,9 @@ pub enum LayoutTarget {
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
NodeGraphControlBar,
/// The body of the Properties panel containing many collapsable sections.
PropertiesSections,
PropertiesPanel,
/// The spredsheet panel allows for the visualisation of data in the graph.
Spreadsheet,
DataPanel,
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
ToolOptions,
/// The vertical buttons for all of the tools on the left of the canvas.
@@ -369,6 +369,7 @@ impl LayoutGroup {
Widget::IconButton(x) => &mut x.tooltip,
Widget::IconLabel(x) => &mut x.tooltip,
Widget::ImageButton(x) => &mut x.tooltip,
Widget::ImageLabel(x) => &mut x.tooltip,
Widget::NumberInput(x) => &mut x.tooltip,
Widget::ParameterExposeButton(x) => &mut x.tooltip,
Widget::PopoverButton(x) => &mut x.tooltip,
@@ -546,6 +547,7 @@ pub enum Widget {
IconButton(IconButton),
IconLabel(IconLabel),
ImageButton(ImageButton),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput),
NodeCatalog(NodeCatalog),
NumberInput(NumberInput),
@@ -622,6 +624,7 @@ impl DiffUpdate {
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_)
| Widget::NodeCatalog(_)
@@ -168,8 +168,6 @@ pub struct ColorInput {
#[widget_builder(constructor)]
pub value: FillChoice,
pub disabled: bool,
// TODO: Implement
// #[serde(rename = "allowTransparency")]
// #[derivative(Default(value = "false"))]
@@ -179,9 +177,11 @@ pub struct ColorInput {
#[derivative(Default(value = "true"))]
pub allow_none: bool,
// TODO: Implement
// pub disabled: bool,
//
pub disabled: bool,
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub tooltip: String,
#[serde(skip)]
@@ -416,6 +416,9 @@ pub struct TextInput {
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -65,4 +65,17 @@ pub struct TextLabel {
pub value: String,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ImageLabel {
#[widget_builder(constructor)]
pub url: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
}
// TODO: Add UserInputLabel
+2 -4
View File
@@ -33,8 +33,6 @@ pub enum Message {
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
// Messages
NoOp,
@@ -71,7 +69,7 @@ mod test {
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
// Print the current node
let (branch, child_prefix) = if tree.has_message_handler_data_fields() || tree.has_message_handler_fields() {
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
("├── ", format!("{}│ ", prefix))
} else {
if is_last {
@@ -99,7 +97,7 @@ mod test {
// Print handler field if any
if let Some(data) = tree.message_handler_fields() {
let len = data.fields().len();
let (branch, child_prefix) = if tree.has_message_handler_data_fields() {
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
("├── ", format!("{}│ ", prefix))
} else {
("└── ", format!("{} ", prefix))
-1
View File
@@ -16,4 +16,3 @@ pub mod portfolio;
pub mod preferences;
pub mod prelude;
pub mod tool;
pub mod workspace;
@@ -1,16 +1,15 @@
use crate::messages::prelude::*;
use crate::node_graph_executor::InspectResult;
/// The spreadsheet UI allows for graph data to be previewed.
#[impl_message(Message, PortfolioMessage, Spreadsheet)]
/// The Data panel UI allows the user to visualize the output data of the selected node.
#[impl_message(Message, DocumentMessage, DataPanel)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SpreadsheetMessage {
ToggleOpen,
pub enum DataPanelMessage {
UpdateLayout {
#[serde(skip)]
inspect_result: InspectResult,
},
ClearLayout,
PushToElementPath {
index: usize,
@@ -19,14 +18,15 @@ pub enum SpreadsheetMessage {
len: usize,
},
ViewVectorDomain {
domain: VectorDomain,
ViewVectorTableTab {
tab: VectorTableTab,
},
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorDomain {
pub enum VectorTableTab {
#[default]
Properties,
Points,
Segments,
Regions,
@@ -0,0 +1,627 @@
use super::VectorTableTab;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::gradient::GradientStops;
use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice};
use graphene_std::{Artboard, Graphic};
use std::any::Any;
use std::sync::Arc;
#[derive(ExtractField)]
pub struct DataPanelMessageContext<'a> {
pub network_interface: &'a mut NodeNetworkInterface,
pub data_panel_open: bool,
}
/// The data panel allows for graph data to be previewed.
#[derive(Default, Debug, Clone, ExtractField)]
pub struct DataPanelMessageHandler {
introspected_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
element_path: Vec<usize>,
active_vector_table_tab: VectorTableTab,
}
#[message_handler_data]
impl MessageHandler<DataPanelMessage, DataPanelMessageContext<'_>> for DataPanelMessageHandler {
fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque<Message>, context: DataPanelMessageContext) {
match message {
DataPanelMessage::UpdateLayout { mut inspect_result } => {
self.introspected_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.take_data();
self.update_layout(responses, context);
}
DataPanelMessage::ClearLayout => {
self.introspected_node = None;
self.introspected_data = None;
self.element_path.clear();
self.active_vector_table_tab = VectorTableTab::default();
self.update_layout(responses, context);
}
DataPanelMessage::PushToElementPath { index } => {
self.element_path.push(index);
self.update_layout(responses, context);
}
DataPanelMessage::TruncateElementPath { len } => {
self.element_path.truncate(len);
self.update_layout(responses, context);
}
DataPanelMessage::ViewVectorTableTab { tab } => {
self.active_vector_table_tab = tab;
self.update_layout(responses, context);
}
}
}
fn actions(&self) -> ActionList {
actions!(DataPanelMessage;)
}
}
impl DataPanelMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>, context: DataPanelMessageContext<'_>) {
let DataPanelMessageContext { network_interface, .. } = context;
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.element_path,
breadcrumbs: Vec::new(),
vector_table_tab: self.active_vector_table_tab,
};
// Main data visualization
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data).unwrap_or_else(|| label("Visualization of this data type is not yet supported")))
.unwrap_or_default();
let mut widgets = Vec::new();
// Selected layer/node name
if let Some(node_id) = self.introspected_node {
let is_layer = network_interface.is_layer(&node_id, &[]);
widgets.extend([
if is_layer {
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder()
} else {
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder()
},
Separator::new(SeparatorType::Related).widget_holder(),
TextInput::new(network_interface.display_name(&node_id, &[]))
.tooltip(if is_layer { "Name of the selected layer" } else { "Name of the selected node" })
.on_update(move |text_input| {
NodeGraphMessage::SetDisplayName {
node_id,
alias: text_input.value.clone(),
skip_adding_history_step: false,
}
.into()
})
.max_width(200)
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
]);
}
// Element path breadcrumbs
if !layout_data.breadcrumbs.is_empty() {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
.widget_holder();
widgets.push(breadcrumb);
}
if !widgets.is_empty() {
layout.insert(0, LayoutGroup::Row { widgets });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::DataPanel,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_table_tab: VectorTableTab,
}
macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None }
$(
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<Context, $ty>>() {
Some(io.output.layout_with_breadcrumb($data))
}
)*
else { None }
}
}
// TODO: We simply try all these types sequentially. Find a better strategy.
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
generate_layout_downcast!(introspected_data, data, [
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
f64,
u32,
u64,
bool,
String,
Option<f64>,
DVec2,
DAffine2,
])
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait TableRowLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.element_page(data)
}
fn element_widget(&self, index: usize) -> WidgetHolder {
TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![]
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.element.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| {
vec![
TextLabel::new(format!("{index}")).widget_holder(),
row.element.element_widget(index),
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
}
}
impl TableRowLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.content.element_page(data)
}
}
impl TableRowLayout for Graphic {
fn type_name() -> &'static str {
"Graphic"
}
fn identifier(&self) -> String {
match self {
Self::Graphic(table) => table.identifier(),
Self::Vector(table) => table.identifier(),
Self::RasterCPU(table) => table.identifier(),
Self::RasterGPU(table) => table.identifier(),
Self::Color(table) => table.identifier(),
Self::Gradient(table) => table.identifier(),
}
}
// Don't put a breadcrumb for Graphic
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.element_page(data)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::Graphic(table) => table.layout_with_breadcrumb(data),
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(table) => table.layout_with_breadcrumb(data),
Self::RasterGPU(table) => table.layout_with_breadcrumb(data),
Self::Color(table) => table.layout_with_breadcrumb(data),
Self::Gradient(table) => table.layout_with_breadcrumb(data),
}
}
}
impl TableRowLayout for Vector {
fn type_name() -> &'static str {
"Vector"
}
fn identifier(&self) -> String {
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
.into_iter()
.map(|tab| {
RadioEntryData::new(format!("{tab:?}"))
.label(format!("{tab:?}"))
.on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into())
})
.collect();
let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_table_tab {
VectorTableTab::Properties => {
table_rows.push(column_headings(&["property", "value"]));
match self.style.fill.clone() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
}
}
if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").widget_holder(),
TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string()
} else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
})
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
]);
}
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)"
} else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
})
.widget_holder(),
]);
}
VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"]));
table_rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]),
);
}
VectorTableTab::Segments => {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{start}")).widget_holder(),
TextLabel::new(format!("{end}")).widget_holder(),
TextLabel::new(format!("{handles:?}")).widget_holder(),
]
}));
}
VectorTableTab::Regions => {
table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{segment_range:?}")).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }]
}
}
impl TableRowLayout for Raster<CPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.width, self.height)
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
let widgets = vec![ImageLabel::new(base64_string).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Raster<GPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.data().width(), self.data().height())
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Color {
fn type_name() -> &'static str {
"Color"
}
fn identifier(&self) -> String {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for GradientStops {
fn type_name() -> &'static str {
"Gradient"
}
fn identifier(&self) -> String {
format!("Gradient ({} stops)", self.0.len())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for f64 {
fn type_name() -> &'static str {
"Number (f64)"
}
fn identifier(&self) -> String {
"Number (f64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
}
fn identifier(&self) -> String {
"Number (u32)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u64 {
fn type_name() -> &'static str {
"Number (u64)"
}
fn identifier(&self) -> String {
"Number (u64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for bool {
fn type_name() -> &'static str {
"Bool"
}
fn identifier(&self) -> String {
"Bool".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for String {
fn type_name() -> &'static str {
"String"
}
fn identifier(&self) -> String {
"String".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Option<f64> {
fn type_name() -> &'static str {
"Option<f64>"
}
fn identifier(&self) -> String {
"Option<f64>".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for DVec2 {
fn type_name() -> &'static str {
"Vec2"
}
fn identifier(&self) -> String {
"Vec2".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for DAffine2 {
fn type_name() -> &'static str {
"Transform"
}
fn identifier(&self) -> String {
"Transform".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
fn format_transform_matrix(transform: &DAffine2) -> String {
let (scale, angle, translation) = transform.to_scale_angle_translation();
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
let round = |x: f64| (x * 1e3).round() / 1e3;
format!(
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
round(translation.x),
round(translation.y),
round(scale.x),
round(scale.y)
)
}
fn format_dvec2(value: DVec2) -> String {
let round = |x: f64| (x * 1e3).round() / 1e3;
format!("({} px, {} px)", round(value.x), round(value.y))
}
@@ -0,0 +1,7 @@
mod data_panel_message;
mod data_panel_message_handler;
#[doc(inline)]
pub use data_panel_message::*;
#[doc(inline)]
pub use data_panel_message_handler::*;
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use super::utility_types::misc::{GroupFolderType, SnappingState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -33,6 +34,8 @@ pub enum DocumentMessage {
Overlays(OverlaysMessage),
#[child]
PropertiesPanel(PropertiesPanelMessage),
#[child]
DataPanel(DataPanelMessage),
// Messages
AlignSelectedLayers {
@@ -9,6 +9,7 @@ use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
@@ -18,6 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
@@ -25,7 +27,6 @@ use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
@@ -33,10 +34,13 @@ use graphene_std::math::quad::Quad;
use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Raster;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::style::ViewMode;
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
use std::path::PathBuf;
use std::time::Duration;
@@ -49,6 +53,9 @@ pub struct DocumentMessageContext<'a> {
pub current_tool: &'a ToolType,
pub preferences: &'a PreferencesMessageHandler,
pub device_pixel_ratio: f64,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
@@ -63,9 +70,11 @@ pub struct DocumentMessageHandler {
#[serde(skip)]
pub node_graph_handler: NodeGraphMessageHandler,
#[serde(skip)]
overlays_message_handler: OverlaysMessageHandler,
pub overlays_message_handler: OverlaysMessageHandler,
#[serde(skip)]
properties_panel_message_handler: PropertiesPanelMessageHandler,
pub properties_panel_message_handler: PropertiesPanelMessageHandler,
#[serde(skip)]
pub data_panel_message_handler: DataPanelMessageHandler,
// ============================================
// Fields that are saved in the document format
@@ -144,6 +153,7 @@ impl Default for DocumentMessageHandler {
node_graph_handler: NodeGraphMessageHandler::default(),
overlays_message_handler: OverlaysMessageHandler::default(),
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
data_panel_message_handler: DataPanelMessageHandler::default(),
// ============================================
// Fields that are saved in the document format
// ============================================
@@ -186,6 +196,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
current_tool,
preferences,
device_pixel_ratio,
data_panel_open,
layers_panel_open,
properties_panel_open,
} = context;
match message {
@@ -223,9 +236,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
document_name: self.name.as_str(),
executor,
persistent_data,
properties_panel_open,
};
self.properties_panel_message_handler.process_message(message, responses, context);
}
DocumentMessage::DataPanel(message) => {
self.data_panel_message_handler.process_message(
message,
responses,
DataPanelMessageContext {
network_interface: &mut self.network_interface,
data_panel_open,
},
);
}
DocumentMessage::NodeGraph(message) => {
self.node_graph_handler.process_message(
message,
@@ -241,6 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
navigation_handler: &self.navigation_handler,
preferences,
layers_panel_open,
},
);
}
@@ -356,12 +381,15 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses),
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
DocumentMessage::DocumentStructureChanged => {
self.update_layers_panel_control_bar_widgets(responses);
self.update_layers_panel_bottom_bar_widgets(responses);
if layers_panel_open {
self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
}
}
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
if !overlay_context.visibility_settings.artboard_name() {
@@ -554,6 +582,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::UpdateHints);
} else {
responses.add(ToolMessage::ActivateTool { tool_type: *current_tool });
responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed
}
}
DocumentMessage::GraphViewOverlayToggle => {
@@ -1128,7 +1157,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}
}
DocumentMessage::SetActivePanel { active_panel: panel } => {
use crate::messages::portfolio::utility_types::PanelType;
match panel {
PanelType::Document => {
if self.graph_view_overlay_open {
@@ -2549,7 +2577,11 @@ impl DocumentMessageHandler {
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
}
pub fn update_layers_panel_control_bar_widgets(&self, responses: &mut VecDeque<Message>) {
pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
// Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode).
let selected_nodes = self.network_interface.selected_nodes();
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
@@ -2707,7 +2739,11 @@ impl DocumentMessageHandler {
});
}
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(self.metadata());
let selected_layer = selected_layers.next();
@@ -2949,10 +2985,10 @@ fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
}
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
let segment = |bezier: bezier_rs::Bezier| match bezier.handles {
bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end),
bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
let segment = |bezier: PathSeg| match bezier {
PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)),
PathSeg::Quad(quad_bez) => path_bool_lib::PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)),
PathSeg::Cubic(cubic_bez) => path_bool_lib::PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)),
};
click_targets
.filter_map(|target| {
@@ -2963,7 +2999,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
}
})
.flatten()
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
.map(|bezier| segment(Affine::new(transform.to_cols_array()) * bezier))
.collect()
}
@@ -2986,11 +3022,11 @@ impl<'a> ClickXRayIter<'a> {
/// Handles the checking of the layer where the target is a rect or path
fn check_layer_area_target(&mut self, click_targets: Option<&Vec<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, transform: DAffine2) -> XRayResult {
// Convert back to Bezier-rs types for intersections
// Convert back to Kurbo types for intersections
let segment = |bezier: &path_bool_lib::PathSegment| match *bezier {
path_bool_lib::PathSegment::Line(start, end) => bezier_rs::Bezier::from_linear_dvec2(start, end),
path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end),
path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, end),
path_bool_lib::PathSegment::Line(start, end) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(h2), dvec2_to_point(end))),
path_bool_lib::PathSegment::Quadratic(start, h1, end) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))),
path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(),
};
let get_clip = || path.iter().map(segment);
@@ -3039,7 +3075,10 @@ impl<'a> ClickXRayIter<'a> {
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform),
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
XRayTarget::Polygon(polygon) => {
let polygon = polygon.iter_closed().map(|line| path_bool_lib::PathSegment::Line(line.start, line.end)).collect();
let polygon = polygon
.iter_closed()
.map(|line| path_bool_lib::PathSegment::Line(point_to_dvec2(line.start()), point_to_dvec2(line.end())))
.collect();
self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
}
}
@@ -2,13 +2,13 @@ use super::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId;
use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId;
@@ -426,7 +426,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear,
stops,
})
@@ -453,7 +452,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial,
stops,
})
@@ -1,8 +1,8 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
@@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
@@ -12,6 +11,7 @@ use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::Vector;
@@ -135,7 +135,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.dimensions.into()), false)),
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)),
Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(artboard.background)), false)),
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
]);
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
@@ -329,11 +329,11 @@ impl<'a> ModifyInputsContext<'a> {
match &fill {
Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(None), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
}
Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(Some(*color)), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
}
Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
@@ -372,8 +372,10 @@ impl<'a> ModifyInputsContext<'a> {
pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
@@ -1,6 +1,7 @@
mod document_message;
mod document_message_handler;
pub mod data_panel;
pub mod graph_operation;
pub mod navigation;
pub mod node_graph;
@@ -399,7 +399,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
NodeInput::value(TaggedValue::Color(Color::WHITE), false),
NodeInput::value(TaggedValue::Color(Table::new_from_element(Color::WHITE)), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
@@ -42,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> {
pub graph_fade_artwork_percentage: f64,
pub navigation_handler: &'a NavigationMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
pub layers_panel_open: bool,
}
#[derive(Debug, Clone, ExtractField)]
@@ -88,7 +89,7 @@ pub struct NodeGraphMessageHandler {
reordering_import: Option<usize>,
/// The index of the export that is being moved
reordering_export: Option<usize>,
/// The end index of the moved port
/// The end index of the moved connector
end_index: Option<usize>,
/// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend
frontend_nodes: Vec<NodeId>,
@@ -111,6 +112,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
graph_fade_artwork_percentage,
navigation_handler,
preferences,
layers_panel_open,
} = context;
match message {
@@ -155,11 +157,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
responses.add(MenuBarMessage::SendLayout);
responses.add(NodeGraphMessage::UpdateLayerPanel);
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendSelectedNodes);
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(OverlaysMessage::Draw);
responses.add(NodeGraphMessage::SendGraph);
responses.add(PortfolioMessage::SubmitActiveGraphRender);
}
NodeGraphMessage::CreateWire { output_connector, input_connector } => {
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
@@ -1215,7 +1219,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
{
return None;
}
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
@@ -1485,7 +1489,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
selected_nodes.set_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(PropertiesPanelMessage::Refresh);
}
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
@@ -1677,6 +1680,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names
}
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
network_interface.set_display_name(&node_id, alias, selection_network_path);
@@ -1833,7 +1837,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
continue;
};
let quad = Quad::from_box([box_selection_start, box_selection_end_graph]);
if click_targets.node_click_target.intersect_path(|| quad.bezier_lines(), DAffine2::IDENTITY) {
if click_targets.node_click_target.intersect_path(|| quad.to_lines(), DAffine2::IDENTITY) {
nodes.insert(node_id);
}
}
@@ -1873,7 +1877,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
NodeGraphMessage::UpdateLayerPanel => {
Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses);
Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses);
}
NodeGraphMessage::UpdateEdges => {
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
@@ -2329,9 +2333,9 @@ impl NodeGraphMessageHandler {
.icon(Some("Node".to_string()))
.tooltip("Add an operation to the end of this layer's chain of nodes")
.popover_layout({
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface);
let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface);
let compatible_type = {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, &context.network_interface);
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type {
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]);
@@ -2413,7 +2417,7 @@ impl NodeGraphMessageHandler {
} else {
added_wires.push(WirePathUpdate {
id: NodeId(u64::MAX),
input_index: usize::MAX,
input_index: u32::MAX as usize,
wire_path_update: None,
})
}
@@ -2585,7 +2589,11 @@ impl NodeGraphMessageHandler {
Some(subgraph_names)
}
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_layers = network_interface
.selected_nodes()
.selected_layers(network_interface.document_metadata())
@@ -2668,7 +2676,7 @@ impl NodeGraphMessageHandler {
}
}
pub fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
// A wire is in progress and its start and end connectors are set
let wiring = self.wire_in_progress_from_connector.is_some();
@@ -20,6 +20,7 @@ use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
};
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
@@ -180,8 +181,8 @@ pub(crate) fn property_from_type(
// ============
// STRUCT TYPES
// ============
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info),
@@ -908,28 +909,39 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input
match &**tagged_value {
TaggedValue::Color(color) => widgets.push(
TaggedValue::Color(color_table) => widgets.push(
color_button
.value(FillChoice::Solid(*color))
.on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColor(color) => widgets.push(
color_button
.value(match color {
Some(color) => FillChoice::Solid(*color),
.value(match color_table.iter().next() {
Some(color) => FillChoice::Solid(*color.element),
None => FillChoice::None,
})
.on_update(update_value(|x: &ColorInput| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
.on_update(update_value(
|input: &ColorInput| TaggedValue::Color(input.value.as_solid().iter().map(|&color| TableRow::new_from_element(color)).collect()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(x) => widgets.push(
TaggedValue::GradientTable(gradient_table) => widgets.push(
color_button
.value(FillChoice::Gradient(x.clone()))
.value(match gradient_table.iter().next() {
Some(row) => FillChoice::Gradient(row.element.clone()),
None => FillChoice::None,
})
.on_update(update_value(
|x: &ColorInput| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
|input: &ColorInput| TaggedValue::GradientTable(input.value.as_gradient().iter().map(|&gradient| TableRow::new_from_element(gradient.clone())).collect()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(gradient_stops) => widgets.push(
color_button
.value(FillChoice::Gradient(gradient_stops.clone()))
.on_update(update_value(
|input: &ColorInput| TaggedValue::GradientStops(input.value.as_gradient().cloned().unwrap_or_default()),
node_id,
index,
))
@@ -1559,7 +1571,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
};
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), &Some(&TaggedValue::OptionalColor(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
&document_node.inputs[BackupColorInput::INDEX].as_value(),
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
@@ -1569,7 +1581,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
return vec![LayoutGroup::Row { widgets: widgets_first_row }];
};
let fill2 = fill.clone();
let backup_color_fill: Fill = backup_color.into();
let backup_color_fill: Fill = backup_color.clone().into();
let backup_gradient_fill: Fill = backup_gradient.clone().into();
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -1582,13 +1594,13 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Fill::None => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(None),
value: TaggedValue::Color(Table::new()),
}
.into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(Some(*color)),
value: TaggedValue::Color(Table::new_from_element(*color)),
}
.into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
@@ -1750,7 +1762,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
let color = color_widget(
ParameterWidgetsInfo::new(node_id, ColorInput::<Option<Color>>::INDEX, true, context),
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
);
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
@@ -8,18 +8,19 @@ use std::borrow::Cow;
pub enum FrontendGraphDataType {
#[default]
General,
Number,
Artboard,
Graphic,
Raster,
Vector,
Number,
Graphic,
Artboard,
Color,
Gradient,
Typography,
}
impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F64(_)
@@ -28,8 +29,13 @@ impl FrontendGraphDataType {
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
_ => Self::General,
}
}
@@ -177,8 +183,8 @@ pub struct FrontendClickTargets {
pub node_click_targets: Vec<String>,
#[serde(rename = "layerClickTargets")]
pub layer_click_targets: Vec<String>,
#[serde(rename = "portClickTargets")]
pub port_click_targets: Vec<String>,
#[serde(rename = "connectorClickTargets")]
pub connector_click_targets: Vec<String>,
#[serde(rename = "iconClickTargets")]
pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")]
@@ -3,8 +3,8 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId};
use wasm_bindgen::JsCast;
@@ -125,7 +125,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
}
// Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector.segment_bezier_iter() {
for (segment_id, bezier, _, _) in vector.segment_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue;
};
@@ -5,14 +5,16 @@ use crate::consts::{
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
};
use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
@@ -571,11 +573,7 @@ impl OverlayContext {
let handle_start = start + start_vec.perp() * radius * factor;
let handle_end = end - end_vec.perp() * radius * factor;
let bezier = Bezier {
start,
end,
handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
};
let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end)));
self.bezier_command(bezier, DAffine2::IDENTITY, i == 0);
}
@@ -762,7 +760,7 @@ impl OverlayContext {
self.render_context.begin_path();
let mut last_point = None;
for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() {
for (_, bezier, start_id, end_id) in vector.segment_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
@@ -776,7 +774,7 @@ impl OverlayContext {
}
/// Used by the Pen tool in order to show how the bezier curve would look like.
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
@@ -788,7 +786,7 @@ impl OverlayContext {
}
/// Used by the path tool segment mode in order to show the selected segments.
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
@@ -802,7 +800,7 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
@@ -816,18 +814,18 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
fn bezier_command(&self, bezier: PathSeg, transform: DAffine2, move_to: bool) {
self.start_dpi_aware_transform();
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
let bezier = Affine::new(transform.to_cols_array()) * bezier;
if move_to {
self.render_context.move_to(start.x, start.y);
self.render_context.move_to(bezier.start().x, bezier.start().y);
}
match handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y),
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y),
match bezier.as_path_el() {
kurbo::PathEl::LineTo(point) => self.render_context.line_to(point.x, point.y),
kurbo::PathEl::QuadTo(point, point1) => self.render_context.quadratic_curve_to(point.x, point.y, point1.x, point1.y),
kurbo::PathEl::CurveTo(point, point1, point2) => self.render_context.bezier_curve_to(point.x, point.y, point1.x, point1.y, point2.x, point2.y),
_ => unreachable!(),
}
self.end_dpi_aware_transform();
@@ -841,36 +839,35 @@ impl OverlayContext {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {
let Some(&first) = curves.peek() else {
continue;
};
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
self.render_context.move_to(start_point.x, start_point.y);
self.render_context.line_to(a.x, a.y)
for curve in curves {
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5);
self.render_context.line_to(a.x, a.y);
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
}
}
}
@@ -885,7 +882,7 @@ impl OverlayContext {
/// Used by the Select tool to outline a path or a free point when selected or hovered.
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
let mut subpaths: Vec<Subpath<PointId>> = vec![];
target_types.for_each(|target_type| match target_type.borrow() {
ClickTargetType::FreePoint(point) => {
@@ -4,20 +4,22 @@ use crate::consts::{
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
};
use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, BezPath, ParamCurve};
use kurbo::{Affine, PathSeg};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};
use vello::Scene;
use vello::kurbo::{self, BezPath};
use vello::peniko;
pub type OverlayProvider = fn(OverlayContext) -> Message;
@@ -200,6 +202,7 @@ impl core::hash::Hash for OverlayContext {
}
impl OverlayContext {
#[allow(dead_code)]
pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self {
Self {
internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))),
@@ -345,16 +348,16 @@ impl OverlayContext {
}
/// Used by the Pen tool in order to show how the bezier curve would look like.
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_bezier(bezier, transform);
}
/// Used by the path tool segment mode in order to show the selected segments.
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_select_bezier(bezier, transform);
}
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.internal().outline_overlay_bezier(bezier, transform);
}
@@ -842,7 +845,7 @@ impl OverlayContextInternal {
let mut path = BezPath::new();
let mut last_point = None;
for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() {
for (_, bezier, start_id, end_id) in vector.segment_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
@@ -853,7 +856,7 @@ impl OverlayContextInternal {
}
/// Used by the Pen tool in order to show how the bezier curve would look like.
fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform();
let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path);
@@ -862,7 +865,7 @@ impl OverlayContextInternal {
}
/// Used by the path tool segment mode in order to show the selected segments.
fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform();
let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path);
@@ -870,7 +873,7 @@ impl OverlayContextInternal {
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
}
fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
let vello_transform = self.get_transform();
let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path);
@@ -878,21 +881,12 @@ impl OverlayContextInternal {
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path);
}
fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) {
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) {
let bezier = Affine::new(transform.to_cols_array()) * bezier;
if move_to {
path.move_to(kurbo::Point::new(start.x, start.y));
}
match handles {
bezier_rs::BezierHandles::Linear => path.line_to(kurbo::Point::new(end.x, end.y)),
bezier_rs::BezierHandles::Quadratic { handle } => path.quad_to(kurbo::Point::new(handle.x, handle.y), kurbo::Point::new(end.x, end.y)),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(
kurbo::Point::new(handle_start.x, handle_start.y),
kurbo::Point::new(handle_end.x, handle_end.y),
kurbo::Point::new(end.x, end.y),
),
path.move_to(bezier.start());
}
path.push(bezier.as_path_el());
}
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
@@ -906,27 +900,27 @@ impl OverlayContextInternal {
continue;
};
let start_point = transform.transform_point2(first.start());
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5);
path.line_to(kurbo::Point::new(a.x, a.y));
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
@@ -945,7 +939,7 @@ impl OverlayContextInternal {
/// Used by the Select tool to outline a path or a free point when selected or hovered.
fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
let mut subpaths: Vec<subpath::Subpath<PointId>> = vec![];
for target_type in target_types {
match target_type.borrow() {
@@ -1118,13 +1112,13 @@ impl OverlayContextInternal {
// Add handle points if they exist
match transformed_bezier.handles {
bezier_rs::BezierHandles::Quadratic { handle } => {
subpath::BezierHandles::Quadratic { handle } => {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y);
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
subpath::BezierHandles::Cubic { handle_start, handle_end } => {
for handle in [handle_start, handle_end] {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
@@ -1154,7 +1148,7 @@ impl OverlayContextInternal {
let mut path = BezPath::new();
let mut last_point = None;
for (_, bezier, start_id, end_id) in row.element.segment_bezier_iter() {
for (_, bezier, start_id, end_id) in row.element.segment_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
@@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> {
pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor,
pub persistent_data: &'a PersistentData,
pub properties_panel_open: bool,
}
#[derive(Debug, Clone, Default, ExtractField)]
@@ -28,16 +29,22 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
document_name,
executor,
persistent_data,
properties_panel_open,
} = context;
match message {
PropertiesPanelMessage::Clear => {
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections,
layout_target: LayoutTarget::PropertiesPanel,
});
}
PropertiesPanelMessage::Refresh => {
if !properties_panel_open {
responses.add(PropertiesPanelMessage::Clear);
return;
}
let mut node_properties_context = NodePropertiesContext {
persistent_data,
responses,
@@ -50,7 +57,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
node_properties_context.responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
layout_target: LayoutTarget::PropertiesSections,
layout_target: LayoutTarget::PropertiesPanel,
});
}
}
@@ -6,6 +6,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, Vector};
@@ -196,7 +197,7 @@ impl DocumentMetadata {
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().filter_map(|target| match target.target_type() {
@@ -8,13 +8,13 @@ use crate::messages::portfolio::document::node_graph::utility_types::{Direction,
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete};
use graphene_std::Artboard;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
@@ -1287,7 +1287,7 @@ impl NodeNetworkInterface {
let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]);
let clip_input = artboard.unwrap().inputs.get(5).unwrap();
if let NodeInput::Value { tagged_value, .. } = clip_input {
if tagged_value.to_primitive_string() == "true" {
if tagged_value.clone().into_inner() == TaggedValue::Bool(true) {
return Some(Quad::clip(
self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
@@ -2635,7 +2635,7 @@ impl NodeNetworkInterface {
InputConnector::Node { node_id, input_index } => (*node_id, *input_index),
InputConnector::Export(export_index) => (NodeId(u64::MAX), *export_index),
})
.chain(std::iter::once((NodeId(u64::MAX), usize::MAX)))
.chain(std::iter::once((NodeId(u64::MAX), u32::MAX as usize)))
.collect()
}
@@ -2725,7 +2725,7 @@ impl NodeNetworkInterface {
Some(WirePathUpdate {
id: NodeId(u64::MAX),
input_index: usize::MAX,
input_index: u32::MAX as usize,
wire_path_update,
})
}
@@ -2832,7 +2832,7 @@ impl NodeNetworkInterface {
let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64);
let radius = 3.;
let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let subpath = Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets {
@@ -2871,7 +2871,7 @@ impl NodeNetworkInterface {
let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64);
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
let radius = 10.;
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
let subpath = Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
DocumentNodeClickTargets {
@@ -3048,7 +3048,7 @@ impl NodeNetworkInterface {
pub fn collect_frontend_click_targets(&mut self, network_path: &[NodeId]) -> FrontendClickTargets {
let mut all_node_click_targets = Vec::new();
let mut port_click_targets = Vec::new();
let mut connector_click_targets = Vec::new();
let mut icon_click_targets = Vec::new();
let Some(network_metadata) = self.network_metadata(network_path) else {
log::error!("Could not get nested network_metadata in collect_frontend_click_targets");
@@ -3059,27 +3059,21 @@ impl NodeNetworkInterface {
let mut node_path = String::new();
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
node_path.push_str(subpath.to_bezpath().to_svg().as_str())
}
all_node_click_targets.push((node_id, node_path));
for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) {
if let ClickTargetType::Subpath(subpath) = port.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
port_click_targets.push(port_path);
connector_click_targets.push(subpath.to_bezpath().to_svg());
}
}
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
icon_click_targets.push(subpath.to_bezpath().to_svg());
}
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
let mut port_path = String::new();
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
icon_click_targets.push(port_path);
icon_click_targets.push(subpath.to_bezpath().to_svg());
}
}
}
@@ -3095,9 +3089,8 @@ impl NodeNetworkInterface {
});
let bounds = self.all_nodes_bounding_box(network_path).cloned().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let rect = bezier_rs::Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
let mut all_nodes_bounding_box = String::new();
let _ = rect.subpath_to_svg(&mut all_nodes_bounding_box, DAffine2::IDENTITY);
let rect = Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
let all_nodes_bounding_box = rect.to_bezpath().to_svg();
let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else {
log::error!("Could not get rounded_network_edge_distance in collect_frontend_click_targets");
@@ -3123,9 +3116,8 @@ impl NodeNetworkInterface {
.inverse()
.transform_point2(import_exports_viewport_bottom_right);
let import_exports_target = bezier_rs::Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right);
let mut import_exports_bounding_box = String::new();
let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY);
let import_exports_target = Subpath::<PointId>::new_rect(node_graph_top_left, node_graph_bottom_right);
let import_exports_bounding_box = import_exports_target.to_bezpath().to_svg();
let mut modify_import_export = Vec::new();
if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) {
@@ -3136,16 +3128,14 @@ impl NodeNetworkInterface {
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
{
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
let mut remove_string = String::new();
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
modify_import_export.push(remove_string);
modify_import_export.push(subpath.to_bezpath().to_svg());
}
}
}
FrontendClickTargets {
node_click_targets,
layer_click_targets,
port_click_targets,
connector_click_targets,
icon_click_targets,
all_nodes_bounding_box,
import_exports_bounding_box,
@@ -3407,7 +3397,7 @@ impl NodeNetworkInterface {
return None;
};
let bounding_box_subpath = bezier_rs::Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
let bounding_box_subpath = Subpath::<PointId>::new_rect(bounds[0], bounds[1]);
bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport)
}
@@ -6047,7 +6037,7 @@ impl Iterator for FlowIter<'_> {
} else {
0
};
let take = if self.flow_type == FlowType::UpstreamFlow { usize::MAX } else { 1 };
let take = if self.flow_type == FlowType::UpstreamFlow { u32::MAX as usize } else { 1 };
let inputs = document_node.inputs.iter().skip(skip).take(take);
let node_ids = inputs.filter_map(|input| match input {
@@ -5,11 +5,11 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use bezier_rs::Subpath;
use glam::IVec2;
use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::uuid::NodeId;
@@ -300,10 +300,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
"graphene_core::raster::BlendNode",
],
},
NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::blend_color_pair::IDENTIFIER,
aliases: &["graphene_raster_nodes::adjustments::BlendColorPairNode", "graphene_core::raster::BlendColorPairNode"],
},
NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER,
aliases: &[
@@ -637,9 +633,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
};
let vector = Vector::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
// Retrieve the output connectors linked to the "Spline" node's output connector
let Some(spline_outputs) = document.network_interface.outward_wires(network_path)?.get(&OutputConnector::node(*node_id, 0)).cloned() else {
log::error!("Vec of InputConnector Spline node is connected to its output port 0.");
log::error!("Vec of InputConnector Spline node is connected to its output connector 0.");
return None;
};
@@ -684,7 +680,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
// Redirect each output connection from the old node to the new "Spline" node's output connector
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
@@ -16,10 +16,12 @@ pub struct MenuBarMessageHandler {
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
pub has_selection_history: (bool, bool),
pub spreadsheet_view_open: bool,
pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool,
pub make_path_editable_is_allowed: bool,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
}
#[message_handler_data]
@@ -585,18 +587,40 @@ impl LayoutHolder for MenuBarMessageHandler {
disabled: no_active_document,
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Window".into(),
false,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Properties".into(),
icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Layers".into(),
icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Window: Spreadsheet".into(),
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()),
disabled: no_active_document,
label: "Data".into(),
icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Help".into(),
true,
false,
MenuBarEntryChildren(vec![
vec![MenuBarEntry {
label: "About Graphite…".into(),
-1
View File
@@ -4,7 +4,6 @@ mod portfolio_message_handler;
pub mod document;
pub mod document_migration;
pub mod menu_bar;
pub mod spreadsheet;
pub mod utility_types;
#[doc(inline)]
@@ -15,8 +15,6 @@ pub enum PortfolioMessage {
MenuBar(MenuBarMessage),
#[child]
Document(DocumentMessage),
#[child]
Spreadsheet(SpreadsheetMessage),
// Messages
Init,
@@ -128,6 +126,9 @@ pub enum PortfolioMessage {
document_id: DocumentId,
ignore_hash: bool,
},
ToggleDataPanelOpen,
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,
ToggleRulers,
UpdateDocumentWidgets,
UpdateOpenDocumentsList,
@@ -1,6 +1,5 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
@@ -23,11 +22,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use bezier_rs::BezierHandles;
use derivative::*;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::subpath::BezierHandles;
use graphene_std::text::Font;
use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
@@ -37,14 +37,15 @@ use std::vec;
pub struct PortfolioMessageContext<'a> {
pub ipp: &'a InputPreprocessorMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
pub animation: &'a AnimationMessageHandler,
pub current_tool: &'a ToolType,
pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool,
pub timing_information: TimingInformation,
pub animation: &'a AnimationMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
#[derive(Debug, Derivative, ExtractField)]
#[derivative(Default)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
@@ -55,10 +56,13 @@ pub struct PortfolioMessageHandler {
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
/// The spreadsheet UI allows for graph data to be previewed.
pub spreadsheet: SpreadsheetMessageHandler,
device_pixel_ratio: Option<f64>,
pub reset_node_definitions_on_open: bool,
pub data_panel_open: bool,
#[derivative(Default(value = "true"))]
pub layers_panel_open: bool,
#[derivative(Default(value = "true"))]
pub properties_panel_open: bool,
}
#[message_handler_data]
@@ -67,11 +71,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let PortfolioMessageContext {
ipp,
preferences,
animation,
current_tool,
message_logging_verbosity,
reset_node_definitions_on_open,
timing_information,
animation,
} = context;
match message {
@@ -86,7 +90,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.menu_bar_message_handler.has_selected_layers = false;
self.menu_bar_message_handler.has_selection_history = (false, false);
self.menu_bar_message_handler.make_path_editable_is_allowed = false;
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
self.menu_bar_message_handler.data_panel_open = self.data_panel_open;
self.menu_bar_message_handler.layers_panel_open = self.layers_panel_open;
self.menu_bar_message_handler.properties_panel_open = self.properties_panel_open;
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
@@ -108,9 +114,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.menu_bar_message_handler.process_message(message, responses, ());
}
PortfolioMessage::Spreadsheet(message) => {
self.spreadsheet.process_message(message, responses, ());
}
PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id {
if let Some(document) = self.documents.get_mut(&document_id) {
@@ -122,6 +125,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
};
document.process_message(message, responses, document_inputs)
}
@@ -156,6 +162,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
};
document.process_message(message, responses, document_inputs)
}
@@ -201,6 +210,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Clear relevant UI layouts if there are no documents
responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
@@ -225,6 +235,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Clear UI layouts that assume the existence of a document
responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
@@ -344,13 +355,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.persistent_data.font_cache.insert(font, preview_url, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
for document_id in self.document_ids.iter() {
let inspect_node = self.inspect_node_id();
let node_to_inspect = self.node_to_inspect();
if let Ok(message) = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
*document_id,
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
node_to_inspect,
true,
) {
responses.add_front(message);
@@ -388,7 +399,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
}
self.load_document(new_document, document_id, &mut new_responses, false);
self.load_document(new_document, document_id, self.layers_panel_open, &mut new_responses, false);
new_responses.add(PortfolioMessage::SelectDocument { document_id });
new_responses.extend(responses.drain(..));
*responses = new_responses;
@@ -504,7 +515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.set_save_state(document_is_saved);
// Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, responses, to_front);
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
}
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
let mut all_new_ids = Vec::new();
@@ -770,7 +781,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
});
}
@@ -801,7 +812,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
});
}
@@ -918,13 +929,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
}
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
let inspect_node = self.inspect_node_id();
let node_to_inspect = self.node_to_inspect();
let result = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
document_id,
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
node_to_inspect,
ignore_hash,
);
@@ -938,6 +949,58 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
Ok(message) => responses.add_front(message),
}
}
PortfolioMessage::ToggleDataPanelOpen => {
self.data_panel_open = !self.data_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.data_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the data panel next time it is opened
responses.add(DataPanelMessage::ClearLayout);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
}
}
PortfolioMessage::TogglePropertiesPanelOpen => {
self.properties_panel_open = !self.properties_panel_open;
responses.add(MenuBarMessage::SendLayout);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
// Run the graph to grab the data
if self.properties_panel_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(PropertiesPanelMessage::Refresh);
}
PortfolioMessage::ToggleLayersPanelOpen => {
self.layers_panel_open = !self.layers_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.layers_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
responses.add(DocumentMessage::ClearLayersPanel);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
}
}
PortfolioMessage::ToggleRulers => {
if let Some(document) = self.active_document_mut() {
document.rulers_visible = !document.rulers_visible;
@@ -987,6 +1050,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PasteIntoFolder,
PrevDocument,
ToggleRulers,
ToggleDataPanelOpen,
);
// Extend with actions that require an active document
@@ -1056,14 +1120,14 @@ impl PortfolioMessageHandler {
}
}
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front {
self.document_ids.push_front(document_id);
} else {
self.document_ids.push_back(document_id);
}
new_document.update_layers_panel_control_bar_widgets(responses);
new_document.update_layers_panel_bottom_bar_widgets(responses);
new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
self.documents.insert(document_id, new_document);
@@ -1111,17 +1175,17 @@ impl PortfolioMessageHandler {
result
}
/// Get the id of the node that should be used as the target for the spreadsheet
pub fn inspect_node_id(&self) -> Option<NodeId> {
// Spreadsheet not open, skipping
if !self.spreadsheet.spreadsheet_view_open {
/// Get the ID of the selected node that should be used as the current source for the Data panel.
pub fn node_to_inspect(&self) -> Option<NodeId> {
// Skip if the Data panel is not open
if !self.data_panel_open {
return None;
}
let document = self.documents.get(&self.active_document_id?)?;
let selected_nodes = document.network_interface.selected_nodes().0;
// Selected nodes != 1, skipping
// Skip if there is not exactly one selected node
if selected_nodes.len() != 1 {
return None;
}
@@ -1,7 +0,0 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;
@@ -1,326 +0,0 @@
use super::VectorDomain;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::memo::IORecord;
use graphene_std::raster::Image;
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Graphic};
use std::any::Any;
use std::sync::Arc;
/// The spreadsheet UI allows for graph data to be previewed.
#[derive(Default, Debug, Clone, ExtractField)]
pub struct SpreadsheetMessageHandler {
/// Sets whether or not the spreadsheet is drawn.
pub spreadsheet_view_open: bool,
inspect_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
element_path: Vec<usize>,
viewing_vector_domain: VectorDomain,
}
#[message_handler_data]
impl MessageHandler<SpreadsheetMessage, ()> for SpreadsheetMessageHandler {
fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
SpreadsheetMessage::ToggleOpen => {
self.spreadsheet_view_open = !self.spreadsheet_view_open;
// Run the graph to grab the data
if self.spreadsheet_view_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
// Update checked UI state for open
responses.add(MenuBarMessage::SendLayout);
self.update_layout(responses);
}
SpreadsheetMessage::UpdateLayout { mut inspect_result } => {
self.inspect_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.take_data();
self.update_layout(responses)
}
SpreadsheetMessage::PushToElementPath { index } => {
self.element_path.push(index);
self.update_layout(responses);
}
SpreadsheetMessage::TruncateElementPath { len } => {
self.element_path.truncate(len);
self.update_layout(responses);
}
SpreadsheetMessage::ViewVectorDomain { domain } => {
self.viewing_vector_domain = domain;
self.update_layout(responses);
}
}
}
fn actions(&self) -> ActionList {
actions!(SpreadsheetMessage;)
}
}
impl SpreadsheetMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateSpreadsheetState {
node: self.inspect_node,
open: self.spreadsheet_view_open,
});
if !self.spreadsheet_view_open {
return;
}
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.element_path,
breadcrumbs: Vec::new(),
vector_domain: self.viewing_vector_domain,
};
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data))
.unwrap_or_else(|| Some(label("No data")))
.unwrap_or_else(|| label("Failed to downcast data"));
if layout_data.breadcrumbs.len() > 1 {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| SpreadsheetMessage::TruncateElementPath { len: len as usize }.into())
.widget_holder();
layout.insert(0, LayoutGroup::Row { widgets: vec![breadcrumb] });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::Spreadsheet,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_domain: VectorDomain,
}
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// We simply try random types. TODO: better strategy.
#[allow(clippy::manual_map)]
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Artboard>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Vector>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), Table<Graphic>>>() {
Some(io.output.layout_with_breadcrumb(data))
} else {
None
}
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait TableRowLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup>;
}
impl TableRowLayout for Graphic {
fn type_name() -> &'static str {
"Graphic"
}
fn identifier(&self) -> String {
match self {
Self::Graphic(graphic) => graphic.identifier(),
Self::Vector(vector) => vector.identifier(),
Self::RasterCPU(_) => "Raster (on CPU)".to_string(),
Self::RasterGPU(_) => "Raster (on GPU)".to_string(),
}
}
// Don't put a breadcrumb for Graphic
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::Graphic(table) => table.layout_with_breadcrumb(data),
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(_) => label("Raster is not supported"),
Self::RasterGPU(_) => label("Raster is not supported"),
}
}
}
impl TableRowLayout for Vector {
fn type_name() -> &'static str {
"Vector"
}
fn identifier(&self) -> String {
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "None" } else { &colinear };
let style = vec![
TextLabel::new(format!(
"{}\n\nColinear Handle IDs: {}\nPreserves Reference to Upstream Nested Layers for Editing by Tools: {}",
self.style,
colinear,
if self.upstream_nested_layers.is_some() { "Yes" } else { "No" }
))
.multiline(true)
.widget_holder(),
];
let domain_entries = [VectorDomain::Points, VectorDomain::Segments, VectorDomain::Regions]
.into_iter()
.map(|domain| {
RadioEntryData::new(format!("{domain:?}"))
.label(format!("{domain:?}"))
.on_update(move |_| SpreadsheetMessage::ViewVectorDomain { domain }.into())
})
.collect();
let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_domain as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_domain {
VectorDomain::Points => {
table_rows.push(column_headings(&["", "position"]));
table_rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]),
);
}
VectorDomain::Segments => {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{}", start)).widget_holder(),
TextLabel::new(format!("{}", end)).widget_holder(),
TextLabel::new(format!("{:?}", handles)).widget_holder(),
]
}));
}
VectorDomain::Regions => {
table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{:?}", segment_range)).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }]
}
}
impl TableRowLayout for Image<Color> {
fn type_name() -> &'static str {
"Image"
}
fn identifier(&self) -> String {
format!("Image ({}x{})", self.width, self.height)
}
fn compute_layout(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let rows = vec![vec![TextLabel::new(format!("Image ({}x{})", self.width, self.height)).widget_holder()]];
vec![LayoutGroup::Table { rows }]
}
}
impl TableRowLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.content.compute_layout(data)
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.element.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| {
let (scale, angle, translation) = row.transform.to_scale_angle_translation();
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
let round = |x: f64| (x * 1e3).round() / 1e3;
vec![
TextLabel::new(format!("{index}")).widget_holder(),
TextButton::new(row.element.identifier())
.on_update(move |_| SpreadsheetMessage::PushToElementPath { index }.into())
.widget_holder(),
TextLabel::new(format!(
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
round(translation.x),
round(translation.y),
round(scale.x),
round(scale.y)
))
.widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
}
}
@@ -43,7 +43,7 @@ pub enum PanelType {
Document,
Layers,
Properties,
Spreadsheet,
DataPanel,
}
impl From<String> for PanelType {
@@ -52,7 +52,7 @@ impl From<String> for PanelType {
"Document" => PanelType::Document,
"Layers" => PanelType::Layers,
"Properties" => PanelType::Properties,
"Spreadsheet" => PanelType::Spreadsheet,
"Data" => PanelType::DataPanel,
_ => panic!("Unknown panel type: {value}"),
}
}
+1 -2
View File
@@ -17,6 +17,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler};
pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant};
pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler};
pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler};
pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler};
@@ -24,12 +25,10 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
// Message, MessageDiscriminant
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
@@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
@@ -12,10 +11,11 @@ use graphene_std::Color;
use graphene_std::NodeInputDecleration;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::Gradient;
use graphene_std::vector::style::{Fill, Gradient};
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
use std::collections::VecDeque;
@@ -273,7 +273,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
let TaggedValue::Fill(Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(gradient.clone())
@@ -284,7 +284,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
let TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(color.to_linear_srgb())
@@ -43,18 +43,19 @@ fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2
}
}
/// Draws a dashed outline around a rectangle to visualize the AABB
/// Draws a dashed outline around the given rectangle (assumed to be in document space).
/// The provided transform is applied to convert coordinates (e.g., to viewport space) during rendering.
fn draw_dashed_rect_outline(rect: Rect, transform: DAffine2, overlay_context: &mut OverlayContext) {
let min = rect.min();
let max = rect.max();
// Create the four corners of the rectangle
let top_left = transform.transform_point2(DVec2::new(min.x, min.y));
let top_right = transform.transform_point2(DVec2::new(max.x, min.y));
let bottom_right = transform.transform_point2(DVec2::new(max.x, max.y));
let bottom_left = transform.transform_point2(DVec2::new(min.x, max.y));
// Define corners in document space
let top_left = DVec2::new(min.x, min.y);
let top_right = DVec2::new(max.x, min.y);
let bottom_right = DVec2::new(max.x, max.y);
let bottom_left = DVec2::new(min.x, max.y);
// Draw the four sides as dashed lines
// Draw each edge using document-space coordinates; transform is applied inside draw_dashed_line
draw_dashed_line(top_left, top_right, transform, overlay_context);
draw_dashed_line(top_right, bottom_right, transform, overlay_context);
draw_dashed_line(bottom_right, bottom_left, transform, overlay_context);
@@ -1,6 +1,6 @@
use super::graph_modification_utils::merge_layers;
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
use crate::consts::HANDLE_LENGTH_FACTOR;
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
@@ -9,12 +9,15 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point};
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::subpath::{BezierHandles, Subpath};
use graphene_std::subpath::{PathSegPoints, pathseg_points};
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType};
use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape};
use std::f64::consts::TAU;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -26,7 +29,7 @@ pub enum SelectionChange {
#[derive(Clone, Copy, Debug)]
pub enum SelectionShape<'a> {
Box([DVec2; 2]),
Box(Rect),
Lasso(&'a Vec<DVec2>),
}
@@ -185,7 +188,7 @@ pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId,
pub struct ClosestSegment {
layer: LayerNodeIdentifier,
segment: SegmentId,
bezier: Bezier,
bezier: PathSeg,
points: [PointId; 2],
colinear: [Option<HandleId>; 2],
t: f64,
@@ -205,12 +208,12 @@ impl ClosestSegment {
self.points
}
pub fn bezier(&self) -> Bezier {
pub fn pathseg(&self) -> PathSeg {
self.bezier
}
pub fn closest_point_document(&self) -> DVec2 {
self.bezier.evaluate(TValue::Parametric(self.t))
point_to_dvec2(self.bezier.eval(self.t))
}
pub fn closest_point_to_viewport(&self) -> DVec2 {
@@ -219,7 +222,7 @@ impl ClosestSegment {
pub fn closest_point(&self, document_metadata: &DocumentMetadata, network_interface: &NodeNetworkInterface) -> DVec2 {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
let bezier_point = self.bezier.evaluate(TValue::Parametric(self.t));
let bezier_point = point_to_dvec2(self.bezier.eval(self.t));
transform.transform_point2(bezier_point)
}
@@ -228,10 +231,10 @@ impl ClosestSegment {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
let layer_mouse_pos = transform.inverse().transform_point2(mouse_position);
let t = self.bezier.project(layer_mouse_pos).clamp(0., 1.);
let t = self.bezier.nearest(dvec2_to_point(layer_mouse_pos), DEFAULT_ACCURACY).t.clamp(0., 1.);
self.t = t;
let bezier_point = self.bezier.evaluate(TValue::Parametric(t));
let bezier_point = point_to_dvec2(self.bezier.eval(t));
let bezier_point = transform.transform_point2(bezier_point);
self.bezier_point_to_viewport = bezier_point;
}
@@ -249,22 +252,24 @@ impl ClosestSegment {
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
// Split the Bezier at the parameter `t`
let [first, second] = self.bezier.split(TValue::Parametric(self.t));
let first = self.bezier.subsegment(0_f64..self.t);
let second = self.bezier.subsegment(self.t..1.);
// Transform the handle positions to viewport space
let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle));
let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle));
let first_handle = pathseg_points(first).p2.map(|handle| transform.transform_point2(handle));
let second_handle = pathseg_points(second).p1.map(|handle| transform.transform_point2(handle));
(first_handle, second_handle)
}
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> (PointId, [SegmentId; 2]) {
let layer = self.layer;
let [first, second] = self.bezier.split(TValue::Parametric(self.t));
let first = pathseg_points(self.bezier.subsegment(0_f64..self.t));
let second = pathseg_points(self.bezier.subsegment(self.t..1.));
// Point
let midpoint = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.end };
let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.p3 };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// First segment
@@ -272,7 +277,7 @@ impl ClosestSegment {
let modification_type = VectorModificationType::InsertSegment {
id: segment_ids[0],
points: [self.points[0], midpoint],
handles: [first.handle_start().map(|handle| handle - first.start), first.handle_end().map(|handle| handle - first.end)],
handles: [first.p1.map(|handle| handle - first.p0), first.p2.map(|handle| handle - first.p3)],
};
responses.add(GraphOperationMessage::Vector { layer, modification_type });
@@ -280,12 +285,12 @@ impl ClosestSegment {
let modification_type = VectorModificationType::InsertSegment {
id: segment_ids[1],
points: [midpoint, self.points[1]],
handles: [second.handle_start().map(|handle| handle - second.start), second.handle_end().map(|handle| handle - second.end)],
handles: [second.p1.map(|handle| handle - second.p0), second.p2.map(|handle| handle - second.p3)],
};
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// G1 continuous on new handles
if self.bezier.handle_end().is_some() {
if pathseg_points(self.bezier).p2.is_some() {
let handles = [HandleId::end(segment_ids[0]), HandleId::primary(segment_ids[1])];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
@@ -353,8 +358,8 @@ impl ClosestSegment {
) -> Option<[Option<HandleId>; 2]> {
let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface);
let start = self.bezier.start;
let end = self.bezier.end;
let start = point_to_dvec2(self.bezier.start());
let end = point_to_dvec2(self.bezier.end());
// Apply the drag delta to the segment's handles
let b = self.bezier_point_to_viewport;
@@ -1686,9 +1691,9 @@ impl ShapeState {
let vector = network_interface.compute_modified_vector(layer)?;
for (segment, mut bezier, start, end) in vector.segment_bezier_iter() {
let t = bezier.project(layer_pos);
let layerspace = bezier.evaluate(TValue::Parametric(t));
for (segment_id, mut segment, start, end) in vector.segment_iter() {
let t = segment.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let layerspace = point_to_dvec2(segment.eval(t));
let screenspace = transform.transform_point2(layerspace);
let distance_squared = screenspace.distance_squared(position);
@@ -1697,20 +1702,22 @@ impl ShapeState {
closest_distance_squared = distance_squared;
// Convert to linear if handes are on top of control points
if let bezier_rs::BezierHandles::Cubic { handle_start, handle_end } = bezier.handles {
if handle_start.abs_diff_eq(bezier.start(), f64::EPSILON * 100.) && handle_end.abs_diff_eq(bezier.end(), f64::EPSILON * 100.) {
bezier = Bezier::from_linear_dvec2(bezier.start, bezier.end);
let PathSegPoints { p0: _, p1, p2, p3: _ } = pathseg_points(segment);
if let (Some(p1), Some(p2)) = (p1, p2) {
let segment_points = pathseg_points(segment);
if p1.abs_diff_eq(segment_points.p0, f64::EPSILON * 100.) && p2.abs_diff_eq(segment_points.p3, f64::EPSILON * 100.) {
segment = PathSeg::Line(Line::new(segment.start(), segment.end()));
}
}
let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment)));
let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment)));
let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment));
let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment));
let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment_id)));
let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment_id)));
let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id));
let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id));
closest = Some(ClosestSegment {
segment,
bezier,
segment: segment_id,
bezier: segment,
points: [start, end],
colinear: [primary_handle, end_handle],
t,
@@ -2076,21 +2083,24 @@ impl ShapeState {
};
// Selection segments
for (id, bezier, _, _) in vector.segment_bezier_iter() {
for (id, segment, _, _) in vector.segment_iter() {
if select_segments {
// Select segments if they lie inside the bounding box or lasso polygon
let segment_bbox = calculate_bezier_bbox(bezier);
let bottom_left = transform.transform_point2(segment_bbox[0]);
let top_right = transform.transform_point2(segment_bbox[1]);
let transformed_segment = Affine::new(transform.to_cols_array()) * segment;
let segment_bbox = transformed_segment.bounding_box();
let select = match selection_shape {
SelectionShape::Box(quad) => {
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all();
SelectionShape::Box(rect) => {
let enclosed = segment_bbox.contains_rect(rect);
match selection_mode {
SelectionMode::Enclosed => enclosed,
_ => {
// Check for intersection with the segment
enclosed || is_intersecting(bezier, quad, transform)
enclosed
|| rect
.path_segments(DEFAULT_ACCURACY)
.map(|seg| seg.as_line().unwrap())
.any(|line| !transformed_segment.intersect_line(line).is_empty())
}
}
}
@@ -2098,7 +2108,7 @@ impl ShapeState {
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
// Sample 10 points on the bezier and check if all or some lie inside the polygon
let points = bezier.compute_lookup_table(Some(10), None);
let points = pathseg_compute_lookup_table(segment, Some(10), false);
match selection_mode {
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)),
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)),
@@ -2111,13 +2121,15 @@ impl ShapeState {
}
}
let segment_points = pathseg_points(segment);
// Selecting handles
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] {
for (position, id) in [(segment_points.p1, ManipulatorPointId::PrimaryHandle(id)), (segment_points.p2, ManipulatorPointId::EndHandle(id))] {
let Some(position) = position else { continue };
let transformed_position = transform.transform_point2(position);
let select = match selection_shape {
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(),
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath
.as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
@@ -2139,7 +2151,7 @@ impl ShapeState {
let transformed_position = transform.transform_point2(position);
let select = match selection_shape {
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(),
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath
.as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
@@ -11,10 +11,10 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::*;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::{self, Subpath};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape};
@@ -363,9 +363,9 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
start_angle / 360. * std::f64::consts::TAU,
sweep_angle / 360. * std::f64::consts::TAU,
match arc_type {
ArcType::Open => bezier_rs::ArcType::Open,
ArcType::Closed => bezier_rs::ArcType::Closed,
ArcType::PieSlice => bezier_rs::ArcType::PieSlice,
ArcType::Open => subpath::ArcType::Open,
ArcType::Closed => subpath::ArcType::Closed,
ArcType::PieSlice => subpath::ArcType::PieSlice,
},
))];
let viewport = document.metadata().transform_to_viewport(layer);
@@ -10,14 +10,16 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget};
use crate::messages::prelude::*;
pub use alignment_snapper::*;
use bezier_rs::TValue;
pub use distribution_snapper::*;
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::vector::NoHashBuilder;
use graphene_std::vector::PointId;
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::point_to_dvec2;
pub use grid_snapper::*;
use kurbo::ParamCurve;
pub use layer_snapper::*;
pub use snap_results::*;
use std::cmp::Ordering;
@@ -81,6 +83,7 @@ impl SnapConstraint {
}
}
}
pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 {
document.snapping_state.tolerance / document.document_ptz.zoom()
}
@@ -127,13 +130,16 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
}
}
}
fn get_closest_curve(curves: &[SnappedCurve], exclude_paths: bool) -> Option<&SnappedPoint> {
let keep_curve = |curve: &&SnappedCurve| !exclude_paths || curve.point.target != SnapTarget::Path(PathSnapTarget::AlongPath);
curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points)
}
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
lines.iter().map(|curve| &curve.point).min_by(compare_points)
}
fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> {
let mut best = None;
for curve_i in curves {
@@ -141,8 +147,8 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<S
if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer {
continue;
}
for curve_i_t in curve_i.document_curve.intersections(&curve_j.document_curve, None, None) {
let snapped_point_document = curve_i.document_curve.evaluate(TValue::Parametric(curve_i_t));
for curve_i_t in filtered_segment_intersections(curve_i.document_curve, curve_j.document_curve, None, None) {
let snapped_point_document = point_to_dvec2(curve_i.document_curve.eval(curve_i_t));
let distance = snap_to.distance(snapped_point_document);
let i_closer = curve_i.point.distance < curve_j.point.distance;
let close = if i_closer { curve_i } else { curve_j };
@@ -165,6 +171,7 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<S
}
best
}
fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> {
let mut best = None;
for line_i in lines {
@@ -237,6 +244,7 @@ impl<'a> SnapData<'a> {
self.node_snap_cache.is_some_and(|cache| !cache.manipulators.is_empty())
}
}
impl SnapManager {
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
self.indicator = snapped_point.is_snapped().then_some(snapped_point);
@@ -3,11 +3,17 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad;
use graphene_std::subpath::pathseg_points;
use graphene_std::subpath::{Identifier, ManipulatorGroup, Subpath};
use graphene_std::vector::PointId;
use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_point, pathseg_tangents_to_point};
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::misc::point_to_dvec2;
use kurbo::{Affine, DEFAULT_ACCURACY, Nearest, ParamCurve, ParamCurveNearest, PathSeg};
#[derive(Clone, Debug, Default)]
pub struct LayerSnapper {
@@ -37,7 +43,7 @@ impl LayerSnapper {
return;
}
for document_curve in bounds.bezier_lines() {
for document_curve in bounds.to_lines() {
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
@@ -70,7 +76,7 @@ impl LayerSnapper {
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) {
for subpath in document.metadata().layer_outline(layer) {
for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = curve.apply_transformation(|p| transform.transform_point2(p));
let document_curve = Affine::new(transform.to_cols_array()) * curve;
let start = subpath.manipulator_groups()[start_index].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
continue;
@@ -98,13 +104,12 @@ impl LayerSnapper {
for path in &self.paths_to_snap {
// Skip very short paths
if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. {
if path.document_curve.start().distance_squared(path.document_curve.end()) < tolerance * tolerance * 2. {
continue;
}
let time = path.document_curve.project(point.document_point);
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
let distance = snapped_point_document.distance(point.document_point);
let Nearest { distance_sq, t } = path.document_curve.nearest(dvec2_to_point(point.document_point), DEFAULT_ACCURACY);
let snapped_point_document = point_to_dvec2(path.document_curve.eval(t));
let distance = distance_sq.sqrt();
if distance < tolerance {
snap_results.curves.push(SnappedCurve {
@@ -144,8 +149,8 @@ impl LayerSnapper {
for path in &self.paths_to_snap {
for constraint_path in constraint_path.iter() {
for time in path.document_curve.intersections(&constraint_path, None, None) {
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
for time in filtered_segment_intersections(path.document_curve, constraint_path, None, None) {
let snapped_point_document = point_to_dvec2(path.document_curve.eval(time));
let distance = snapped_point_document.distance(point.document_point);
@@ -266,8 +271,8 @@ impl LayerSnapper {
fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) {
if normals && path.bounds.is_none() {
for &neighbor in &point.neighbors {
for t in path.document_curve.normals_to_point(neighbor) {
let normal_point = path.document_curve.evaluate(TValue::Parametric(t));
for t in pathseg_normals_to_point(path.document_curve, dvec2_to_point(neighbor)) {
let normal_point = point_to_dvec2(path.document_curve.eval(t));
let distance = normal_point.distance(point.document_point);
if distance > tolerance {
continue;
@@ -287,8 +292,8 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
}
if tangents && path.bounds.is_none() {
for &neighbor in &point.neighbors {
for t in path.document_curve.tangents_to_point(neighbor) {
let tangent_point = path.document_curve.evaluate(TValue::Parametric(t));
for t in pathseg_tangents_to_point(path.document_curve, dvec2_to_point(neighbor)) {
let tangent_point = point_to_dvec2(path.document_curve.eval(t));
let distance = tangent_point.distance(point.document_point);
if distance > tolerance {
continue;
@@ -310,7 +315,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
#[derive(Clone, Debug)]
struct SnapCandidatePath {
document_curve: Bezier,
document_curve: PathSeg,
layer: LayerNodeIdentifier,
start: PointId,
target: SnapTarget,
@@ -440,12 +445,13 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
return;
}
let curve = pathseg_points(curve);
let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document));
let out_handle = curve.handle_end().map(|handle| handle - curve.end).filter(handle_not_under(to_document));
let in_handle = curve.p1.map(|handle| handle - curve.p0).filter(handle_not_under(to_document));
let out_handle = curve.p2.map(|handle| handle - curve.p3).filter(handle_not_under(to_document));
if in_handle.is_none() && out_handle.is_none() {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(curve.start() * 0.5 + curve.end * 0.5),
to_document.transform_point2(curve.p0 * 0.5 + curve.p3 * 0.5),
SnapSource::Path(PathSnapSource::LineMidpoint),
SnapTarget::Path(PathSnapTarget::LineMidpoint),
Some(layer),
@@ -487,7 +493,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
}
}
pub fn are_manipulator_handles_colinear(manipulators: &bezier_rs::ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
let anchor = manipulators.anchor;
let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = manipulators.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
@@ -2,11 +2,11 @@ use super::DistributionMatch;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget};
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::vector::PointId;
use kurbo::PathSeg;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
@@ -120,5 +120,5 @@ pub struct SnappedCurve {
pub layer: LayerNodeIdentifier,
pub start: PointId,
pub point: SnappedPoint,
pub document_curve: Bezier,
pub document_curve: PathSeg,
}
@@ -9,16 +9,17 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{Node
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType;
use bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::Quad;
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::table::Table;
use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
use kurbo::{CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, PathSeg, Point, QuadBez, Shape};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(
@@ -208,25 +209,6 @@ pub fn is_visible_point(
}
}
/// Function to find the bounding box of bezier (uses method from kurbo)
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
let start = Point::new(bezier.start.x, bezier.start.y);
let end = Point::new(bezier.end.x, bezier.end.y);
let bbox = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end } => {
let p1 = Point::new(handle_start.x, handle_start.y);
let p2 = Point::new(handle_end.x, handle_end.y);
CubicBez::new(start, p1, p2, end).bounding_box()
}
BezierHandles::Quadratic { handle } => {
let p1 = Point::new(handle.x, handle.y);
QuadBez::new(start, p1, end).bounding_box()
}
BezierHandles::Linear => Line::new(start, end).bounding_box(),
};
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
}
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
let to_layerspace = transform.inverse();
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
@@ -496,19 +478,19 @@ pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVe
let c1 = p1 + d1 * start_handle_length;
let c2 = p3 + d2 * end_handle_length;
let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y);
let new_curve = PathSeg::Cubic(CubicBez::new(Point::new(p1.x, p1.y), Point::new(c1.x, c1.y), Point::new(c2.x, c2.y), Point::new(p3.x, p3.y)));
// Sample 2*n points from new curve and get the L2 metric between all of points
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
let points = pathseg_compute_lookup_table(new_curve, Some(2 * n), false);
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
let dist = points1.iter().zip(points).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
dist / (2 * n) as f64
}
/// Calculates optimal handle lengths with adam optimization.
#[allow(clippy::too_many_arguments)]
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, further_segment: PathSeg, other_segment: PathSeg) -> (DVec2, DVec2) {
let h = 1e-6;
let tol = 1e-6;
let max_iter = 200;
@@ -530,21 +512,25 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec
let n = 20;
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
farther_segment.reverse()
let further_segment = if further_segment.start().distance(dvec2_to_point(p1)) >= f64::EPSILON {
further_segment.reverse()
} else {
farther_segment
further_segment
};
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment };
let other_segment = if other_segment.end().distance(dvec2_to_point(p3)) >= f64::EPSILON {
other_segment.reverse()
} else {
other_segment
};
// Now we sample points proportional to the lengths of the beziers
let l1 = farther_segment.length(None);
let l2 = other_segment.length(None);
let l1 = further_segment.perimeter(DEFAULT_ACCURACY);
let l2 = other_segment.perimeter(DEFAULT_ACCURACY);
let ratio = l1 / (l1 + l2);
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
let mut points1 = pathseg_compute_lookup_table(further_segment, Some(n_points1), false).collect::<Vec<_>>();
let mut points2 = pathseg_compute_lookup_table(other_segment, Some(n), false).collect::<Vec<_>>();
points1.append(&mut points2);
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };
@@ -156,6 +156,9 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
// Subscribe new tool
tool_data.tools.get(&tool_type).unwrap().activate(responses);
// Re-add the artboard overlay provider when tools are reactivated
responses.add(OverlaysMessage::AddProvider(ARTBOARD_OVERLAY_PROVIDER));
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated
responses.add(BroadcastEvent::SelectionChanged);
@@ -204,7 +204,6 @@ impl SelectedGradient {
/// Update the layer fill to the current gradient
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
self.gradient.transform = self.transform;
if let Some(layer) = self.layer {
responses.add(GraphOperationMessage::FillSet {
layer,
@@ -436,14 +435,7 @@ impl Fsm for GradientToolFsmState {
gradient.clone()
} else {
// Generate a new gradient
Gradient::new(
DVec2::ZERO,
global_tool_data.secondary_color,
DVec2::ONE,
global_tool_data.primary_color,
DAffine2::IDENTITY,
tool_options.gradient_type,
)
Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type)
};
let selected_gradient = SelectedGradient::new(gradient, layer, document).with_gradient_start(input.mouse.position);
@@ -21,14 +21,16 @@ use crate::messages::tool::common_functionality::shape_editor::{
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed};
use bezier_rs::{Bezier, BezierHandles, TValue};
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::subpath::pathseg_points;
use graphene_std::transform::ReferencePoint;
use graphene_std::uuid::NodeId;
use graphene_std::vector::algorithms::util::pathseg_tangent;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType};
use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};
use std::vec;
#[derive(Default, ExtractField)]
@@ -126,6 +128,7 @@ pub enum PathToolMessage {
overlay_context: OverlayContext,
},
MergeSelectedPoints,
StartSlidingPoint,
Copy {
clipboard: Clipboard,
},
@@ -429,6 +432,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
ClosePath,
PointerMove,
MergeSelectedPoints,
StartSlidingPoint,
Copy,
Cut,
DeleteSelected,
@@ -447,6 +451,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
BreakPath,
DeleteAndBreakPath,
SwapSelectedHandles,
StartSlidingPoint,
Copy,
Cut,
DeleteSelected,
@@ -465,6 +470,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath,
Escape,
RightClick,
StartSlidingPoint,
TogglePointEditing,
ToggleSegmentEditing
),
@@ -505,7 +511,7 @@ pub enum PointSelectState {
#[derive(Clone, Copy)]
pub struct SlidingSegmentData {
segment_id: SegmentId,
bezier: Bezier,
bezier: PathSeg,
start: PointId,
}
@@ -910,13 +916,12 @@ impl PathToolData {
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Dragging(self.dragging_state)
} else {
let start_pos = segment.bezier().start;
let end_pos = segment.bezier().end;
let points = pathseg_points(segment.pathseg());
let [pos1, pos2] = match segment.bezier().handles {
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end],
BezierHandles::Quadratic { handle } => [handle, end_pos],
BezierHandles::Linear => [start_pos + (end_pos - start_pos) / 3., end_pos + (start_pos - end_pos) / 3.],
let [pos1, pos2] = match (points.p1, points.p2) {
(Some(p1), Some(p2)) => [p1, p2],
(Some(p1), None) | (None, Some(p1)) => [p1, points.p3],
(None, None) => [points.p0 + (points.p3 - points.p0) / 3., points.p3 + (points.p0 - points.p3) / 3.],
};
self.molding_info = Some((pos1, pos2));
PathToolFsmState::Dragging(self.dragging_state)
@@ -1277,15 +1282,10 @@ impl PathToolData {
};
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return false };
// Check that the handles of anchor point are also colinear
if !vector.colinear(*anchor) {
return false;
};
let Some(point_id) = anchor.as_anchor() else { return false };
let mut connected_segments = [None, None];
for (segment, bezier, start, end) in vector.segment_bezier_iter() {
for (segment, bezier, start, end) in vector.segment_iter() {
if start == point_id || end == point_id {
match (connected_segments[0], connected_segments[1]) {
(None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }),
@@ -1325,13 +1325,13 @@ impl PathToolData {
let segments = sliding_point_info.connected_segments;
let t1 = segments[0].bezier.project(layer_pos);
let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1));
let t1 = segments[0].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let position1 = point_to_dvec2(segments[0].bezier.eval(t1));
let t2 = segments[1].bezier.project(layer_pos);
let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2));
let t2 = segments[1].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t;
let position2 = point_to_dvec2(segments[1].bezier.eval(t2));
let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) {
let (closer_segment, further_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) {
(segments[1], segments[0], t2, position2)
} else {
(segments[0], segments[1], t1, position1)
@@ -1346,50 +1346,59 @@ impl PathToolData {
shape_editor.move_anchor(anchor, &vector, delta, layer, None, responses);
// Make a split at the t_value
let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value));
let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start };
let first = closer_segment.bezier.subsegment(0_f64..t_value);
let second = closer_segment.bezier.subsegment(t_value..1.);
let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) };
let closer_segment_other_point = if anchor == closer_segment.start {
closer_segment.bezier.end()
} else {
closer_segment.bezier.start()
};
let (split_segment, other_segment) = if first.start() == closer_segment_other_point { (first, second) } else { (second, first) };
let split_segment_points = pathseg_points(split_segment);
// Primary handle maps to primary handle and secondary maps to secondary
let closer_primary_handle = HandleId::primary(closer_segment.segment_id);
let Some(handle_position) = split_segment.handle_start() else { return };
let relative_position1 = handle_position - split_segment.start;
let Some(handle_position) = split_segment_points.p1 else { return };
let relative_position1 = handle_position - split_segment_points.p0;
let modification_type = closer_primary_handle.set_relative_position(relative_position1);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let closer_secondary_handle = HandleId::end(closer_segment.segment_id);
let Some(handle_position) = split_segment.handle_end() else { return };
let relative_position2 = handle_position - split_segment.end;
let Some(handle_position) = split_segment_points.p2 else { return };
let relative_position2 = handle_position - split_segment_points.p3;
let modification_type = closer_secondary_handle.set_relative_position(relative_position2);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 };
let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start {
let further_segment_points = pathseg_points(further_segment.bezier);
let (further_other_point, start_handle, end_handle, start_handle_pos) = if anchor == further_segment.start {
(
farther_segment.bezier.end,
HandleId::end(farther_segment.segment_id),
HandleId::primary(farther_segment.segment_id),
farther_segment.bezier.handle_end(),
further_segment_points.p3,
HandleId::end(further_segment.segment_id),
HandleId::primary(further_segment.segment_id),
further_segment_points.p2,
)
} else {
(
farther_segment.bezier.start,
HandleId::primary(farther_segment.segment_id),
HandleId::end(farther_segment.segment_id),
farther_segment.bezier.handle_start(),
further_segment_points.p0,
HandleId::primary(further_segment.segment_id),
HandleId::end(further_segment.segment_id),
further_segment_points.p1,
)
};
let Some(start_handle_position) = start_handle_pos else { return };
let start_handle_direction = start_handle_position - farther_other_point;
let start_handle_direction = start_handle_position - further_other_point;
// Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent
let d1 = start_handle_direction.try_normalize().unwrap_or({
if anchor == farther_segment.start {
-farther_segment.bezier.tangent(TValue::Parametric(0.99))
if anchor == further_segment.start {
-pathseg_tangent(further_segment.bezier, 1.)
} else {
farther_segment.bezier.tangent(TValue::Parametric(0.01))
pathseg_tangent(further_segment.bezier, 0.)
}
});
@@ -1398,7 +1407,7 @@ impl PathToolData {
let min_len1 = start_handle_direction.length() * 0.4;
let min_len2 = end_handle_direction.length() * 0.4;
let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment);
let (relative_pos1, relative_pos2) = find_two_param_best_approximate(further_other_point, new_position, d1, d2, min_len1, min_len2, further_segment.bezier, other_segment);
// Now set those handles to these handle lengths keeping the directions d1, d2
let modification_type = start_handle.set_relative_position(relative_pos1);
@@ -1807,13 +1816,13 @@ impl Fsm for PathToolFsmState {
if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier {
let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface);
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
overlay_context.outline_overlay_bezier(closest_segment.pathseg(), transform);
// Draw the anchors again
let display_anchors = overlay_context.visibility_settings.anchors();
if display_anchors {
let start_pos = transform.transform_point2(closest_segment.bezier().start);
let end_pos = transform.transform_point2(closest_segment.bezier().end);
let start_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().start()));
let end_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().end()));
let start_id = closest_segment.points()[0];
let end_id = closest_segment.points()[1];
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) {
@@ -1890,7 +1899,7 @@ impl Fsm for PathToolFsmState {
let (points_inside, segments_inside) = match selection_shape {
SelectionShapeType::Box => {
let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position);
let bbox = [tool_data.drag_start_pos, previous_mouse];
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
shape_editor.get_inside_points_and_segments(
&document.network_interface,
SelectionShape::Box(bbox),
@@ -1935,7 +1944,7 @@ impl Fsm for PathToolFsmState {
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
for (segment, bezier, _, _) in vector.segment_bezier_iter() {
for (segment, bezier, _, _) in vector.segment_iter() {
if segments.contains(&segment) {
overlay_context.outline_overlay_bezier(bezier, transform);
}
@@ -2143,10 +2152,6 @@ impl Fsm for PathToolFsmState {
}
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
if snap_angle_state && lock_angle_state && tool_data.start_sliding_point(tool_action_data.shape_editor, tool_action_data.document) {
return PathToolFsmState::SlidingPoint;
}
tool_data.drag(
equidistant_state,
lock_angle_state,
@@ -2311,7 +2316,8 @@ impl Fsm for PathToolFsmState {
match selection_shape {
SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse];
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
shape_editor.select_all_in_shape(
&document.network_interface,
SelectionShape::Box(bbox),
@@ -2407,7 +2413,8 @@ impl Fsm for PathToolFsmState {
} else {
match selection_shape {
SelectionShapeType::Box => {
let bbox = [tool_data.drag_start_pos, previous_mouse];
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
shape_editor.select_all_in_shape(
&document.network_interface,
SelectionShape::Box(bbox),
@@ -2440,6 +2447,8 @@ impl Fsm for PathToolFsmState {
tool_data.ghost_outline.clear();
let extend_selection = input.keyboard.get(extend_selection as usize);
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD;
let mut segment_dissolved = false;
let mut point_inserted = false;
let nearest_point = shape_editor.find_nearest_visible_point_indices(
&document.network_interface,
@@ -2460,6 +2469,7 @@ impl Fsm for PathToolFsmState {
if tool_data.delete_segment_pressed {
if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) {
shape_editor.dissolve_segment(responses, segment.layer(), &vector, segment.segment(), segment.points());
segment_dissolved = true;
}
} else {
let is_segment_selected = shape_editor
@@ -2468,11 +2478,7 @@ impl Fsm for PathToolFsmState {
.is_some_and(|state| state.is_segment_selected(segment.segment()));
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode, is_segment_selected);
tool_data.segment = None;
tool_data.molding_info = None;
tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None;
return PathToolFsmState::Ready;
point_inserted = true;
}
}
@@ -2480,6 +2486,11 @@ impl Fsm for PathToolFsmState {
tool_data.molding_info = None;
tool_data.molding_segment = false;
tool_data.temporary_adjacent_handles_while_molding = None;
if segment_dissolved || point_inserted {
responses.add(DocumentMessage::EndTransaction);
return PathToolFsmState::Ready;
}
}
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
@@ -2636,6 +2647,14 @@ impl Fsm for PathToolFsmState {
shape_editor.delete_point_and_break_path(document, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::StartSlidingPoint) => {
responses.add(DocumentMessage::StartTransaction);
if tool_data.start_sliding_point(shape_editor, document) {
PathToolFsmState::SlidingPoint
} else {
PathToolFsmState::Ready
}
}
(_, PathToolMessage::Copy { clipboard }) => {
// TODO: Add support for selected segments
@@ -2767,16 +2786,13 @@ impl Fsm for PathToolFsmState {
// Create new segment ids and add the segments into the existing vector content
let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() {
for (segment_id, bezier, start, end) in new_vector.segment_iter() {
let new_segment_id = SegmentId::generate();
segments_map.insert(segment_id, new_segment_id);
let handles = match bezier.handles {
BezierHandles::Linear => [None, None],
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = pathseg_points(bezier);
let handles = [points.p1, points.p2];
let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles };
@@ -2872,7 +2888,7 @@ impl Fsm for PathToolFsmState {
let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in old_vector.segment_bezier_iter() {
for (segment_id, bezier, start, end) in old_vector.segment_iter() {
let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end));
let segment_selected = layer_selection_state.is_segment_selected(segment_id);
@@ -2881,11 +2897,8 @@ impl Fsm for PathToolFsmState {
let new_id = SegmentId::generate();
segments_map.insert(segment_id, new_id);
let handles = match bezier.handles {
BezierHandles::Linear => [None, None],
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = pathseg_points(bezier);
let handles = [points.p1, points.p2];
let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_id, points, handles };
@@ -3449,7 +3462,7 @@ fn update_dynamic_hints(
}
}
let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
let drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")];
if at_least_one_anchor_selected {
@@ -3457,10 +3470,6 @@ fn update_dynamic_hints(
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus());
}
if single_colinear_anchor_selected {
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
}
let segment_edit = tool_options.path_editing_mode.segment_editing_mode;
let point_edit = tool_options.path_editing_mode.point_editing_mode;
@@ -3525,9 +3534,15 @@ fn update_dynamic_hints(
let mut groups = vec![
HintGroup(drag_selected_hints),
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
HintGroup(delete_selected_hints),
];
if single_colinear_anchor_selected {
groups.push(HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyG]], "Slide")]));
}
groups.push(HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]));
groups.push(HintGroup(delete_selected_hints));
hint_data.append(&mut groups);
}
@@ -11,11 +11,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::subpath::pathseg_points;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
use kurbo::{CubicBez, PathSeg};
#[derive(Default, ExtractField)]
pub struct PenTool {
@@ -1257,7 +1258,7 @@ impl PenToolData {
let vector = document.network_interface.compute_modified_vector(layer);
let (handle_start, in_segment) = if let Some(vector) = &vector {
vector
.segment_bezier_iter()
.segment_iter()
.find_map(|(segment_id, bezier, start, end)| {
let is_end = point == end;
let is_start = point == start;
@@ -1265,15 +1266,11 @@ impl PenToolData {
return None;
}
let handle = match bezier.handles {
BezierHandles::Cubic { handle_start, handle_end, .. } => {
if is_start {
handle_start
} else {
handle_end
}
}
BezierHandles::Quadratic { handle } => handle,
let points = pathseg_points(bezier);
let handle = match (points.p1, points.p2) {
(Some(p1), Some(_)) if is_start => p1,
(Some(_), Some(p2)) if !is_start => p2,
(Some(p1), None) | (None, Some(p1)) => p1,
_ => return None,
};
Some((segment_id, is_end, handle))
@@ -1599,9 +1596,8 @@ impl Fsm for PenToolFsmState {
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start));
if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) {
let handles = BezierHandles::Cubic { handle_start, handle_end };
let end = tool_data.next_point;
let bezier = Bezier { start, handles, end };
let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end)));
if (end - start).length_squared() > f64::EPSILON {
// Draw the curve for the currently-being-placed segment
overlay_context.outline_bezier(bezier, transform);
@@ -1723,7 +1719,7 @@ impl Fsm for PenToolFsmState {
// We have the point. Join the 2 vertices and check if any path is closed.
if let Some(end) = closest_point {
let segment_id = SegmentId::generate();
vector.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO);
vector.push(segment_id, start, end, (Some(handle_start), Some(handle_end)), StrokeId::ZERO);
let grouped_segments = vector.auto_join_paths();
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));
@@ -20,12 +20,12 @@ use crate::messages::tool::common_functionality::shape_editor::SelectionShapeTyp
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
use bezier_rs::Subpath;
use glam::DMat2;
use graph_craft::document::NodeId;
use graphene_std::path_bool::BooleanOperation;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::subpath::Subpath;
use graphene_std::transform::ReferencePoint;
use std::fmt;
@@ -92,6 +92,7 @@ pub enum SelectToolMessage {
remove_from_selection: Key,
},
EditLayer,
EditLayerExec,
Enter,
PointerMove(SelectToolPointerKeys),
PointerOutsideViewport(SelectToolPointerKeys),
@@ -323,6 +324,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
PointerMove,
Abort,
EditLayer,
EditLayerExec,
Enter,
);
@@ -689,32 +691,28 @@ impl Fsm for SelectToolFsmState {
// Measure with Alt held down
// TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places
if overlay_context.visibility_settings.quick_measurement() && !matches!(self, Self::ResizingBounds { .. }) && input.keyboard.get(Key::Alt as usize) {
// Get all selected layers and compute their viewport-aligned AABB
let selected_bounds_viewport = document
// Compute document-space bounding box (AABB) of all selected visible & unlocked layers
let selected_bounds_doc_space = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
// Exclude layers that are artboards
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
// Get the layer's bounding box in its local space
let local_bounds = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY)?;
// Transform the bounds directly to viewport space
let viewport_quad = document.metadata().transform_to_viewport(layer) * Quad::from_box(local_bounds);
// Convert the quad to an AABB in viewport space
Some(Rect::from_box(viewport_quad.bounding_box()))
})
// For each remaining layer, try to get its document-space bounding box and convert it to a Rect
.filter_map(|layer| document.metadata().bounding_box_document(layer).map(Rect::from_box))
// Combine all individual bounding boxes into one overall bounding box that contains all selected layers
.reduce(Rect::combine_bounds);
// Get the hovered layer's viewport-aligned AABB
let hovered_bounds_viewport = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY).map(|bounds| {
let viewport_quad = document.metadata().transform_to_viewport(layer) * Quad::from_box(bounds);
Rect::from_box(viewport_quad.bounding_box())
});
// Compute document-space bounding box (AABB) of the currently hovered layer
let hovered_bounds_doc_space = document.metadata().bounding_box_document(layer);
// Use the viewport-aligned AABBs for measurement
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_viewport, hovered_bounds_viewport) {
// Since we're already in viewport space, use identity transform
measure::overlay(selected_bounds, hovered_bounds, DAffine2::IDENTITY, DAffine2::IDENTITY, &mut overlay_context);
// If both selected and hovered bounds exist, overlay measurement lines
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_bounds_doc_space, hovered_bounds_doc_space.map(Rect::from_box)) {
// Both `selected_bounds` and `hovered_bounds` are in document space.
// To correctly render overlay lines in the UI (which is in viewport space), we need to transform both rectangles from document to viewport space.
// Therefore, we pass `document_to_viewport` as both the `transform` and `document_to_viewport` parameters.
let document_to_viewport = document.metadata().document_to_viewport;
measure::overlay(selected_bounds, hovered_bounds, document_to_viewport, document_to_viewport, &mut overlay_context);
}
}
}
@@ -985,14 +983,19 @@ impl Fsm for SelectToolFsmState {
self
}
(_, SelectToolMessage::EditLayer) => {
// Edit the clicked layer
responses.add(DeferMessage::AfterGraphRun {
messages: vec![SelectToolMessage::EditLayerExec.into()],
});
self
}
(_, SelectToolMessage::EditLayerExec) => {
if let Some(intersect) = document.click(input) {
match tool_data.nested_selection_behavior {
NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses),
NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network_interface, responses),
}
}
self
}
(
@@ -65,6 +65,8 @@ pub struct TransformLayerMessageHandler {
// Ghost outlines for Path Tool
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
was_grabbing: bool,
}
impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for TransformLayerMessageHandler {
@@ -122,7 +124,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = self.local_pivot;
}
// Here vector data from all layers is not considered which can be a problem in pivot calculation
// TODO: Here vector data from all layers is not considered which can be a problem in pivot calculation
else if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
*selected.original_transforms = OriginalTransforms::default();
@@ -301,6 +303,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(SelectToolMessage::PivotShift { offset: None, flush: true });
if final_transform {
self.was_grabbing = false;
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
}
@@ -356,34 +359,64 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
if (selected_points.is_empty() && selected_segments.is_empty())
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|| selected_layers.is_empty()
|| transform_type.equivalent_to(self.transform_operation)
|| (transform_type.equivalent_to(self.transform_operation) && !self.was_grabbing)
{
return;
}
if using_path_tool && transform_type == TransformType::Grab {
// Check if a single point is selected and it's a colinear point
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| point.as_anchor().is_some());
if single_anchor_selected && transform_type.equivalent_to(self.transform_operation) && self.was_grabbing {
let selected_nodes = &document.network_interface.selected_nodes();
let Some(layer) = selected_nodes.selected_layers(document.metadata()).next() else { return };
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
let Some(anchor) = shape_editor.selected_points().next() else { return };
if vector_data.colinear(*anchor) {
responses.add(TransformLayerMessage::CancelTransformOperation);
// Start sliding point
responses.add(DeferMessage::AfterGraphRun {
messages: vec![PathToolMessage::StartSlidingPoint.into()],
});
}
return;
}
if transform_type.equivalent_to(self.transform_operation) {
return;
}
self.was_grabbing = true;
}
}
if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
if let [point] = selected_points.as_slice() {
if matches!(point, ManipulatorPointId::Anchor(_)) {
if let Some([handle1, handle2]) = point.get_handle_pair(&vector) {
let handle1_length = handle1.length(&vector);
let handle2_length = handle2.length(&vector);
if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer))
&& let [point] = selected_points.as_slice()
{
if matches!(point, ManipulatorPointId::Anchor(_)) {
if let Some([handle1, handle2]) = point.get_handle_pair(&vector) {
let handle1_length = handle1.length(&vector);
let handle2_length = handle2.length(&vector);
if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) {
// G should work for this point but not R and S
if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
selected.original_transforms.clear();
return;
}
if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) {
// G should work for this point but not R and S
if matches!(transform_type, TransformType::Rotate | TransformType::Scale) {
selected.original_transforms.clear();
return;
}
}
} else {
let handle_length = point.as_handle().map(|handle| handle.length(&vector));
}
} else {
let handle_length = point.as_handle().map(|handle| handle.length(&vector));
if handle_length == Some(0.) {
selected.original_transforms.clear();
return;
}
if handle_length == Some(0.) {
selected.original_transforms.clear();
return;
}
}
}
@@ -409,6 +442,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
TransformLayerMessage::CancelTransformOperation => {
if using_path_tool {
self.ghost_outline.clear();
self.was_grabbing = false;
}
if using_pen_tool {
@@ -499,7 +533,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
match self.transform_operation {
TransformOperation::None => unreachable!(),
TransformOperation::None => {}
TransformOperation::Grabbing(translation) => {
let delta_pos = input.mouse.position - self.mouse_position;
let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos);
-7
View File
@@ -1,7 +0,0 @@
mod workspace_message;
mod workspace_message_handler;
#[doc(inline)]
pub use workspace_message::{WorkspaceMessage, WorkspaceMessageDiscriminant};
#[doc(inline)]
pub use workspace_message_handler::WorkspaceMessageHandler;
@@ -1,8 +0,0 @@
use crate::messages::prelude::*;
#[impl_message(Message, Workspace)]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum WorkspaceMessage {
// Messages
NodeGraphToggleVisibility,
}
@@ -1,24 +0,0 @@
use crate::messages::prelude::*;
#[derive(Debug, Clone, Default, ExtractField)]
pub struct WorkspaceMessageHandler {
node_graph_visible: bool,
}
#[message_handler_data]
impl MessageHandler<WorkspaceMessage, ()> for WorkspaceMessageHandler {
fn process_message(&mut self, message: WorkspaceMessage, _responses: &mut VecDeque<Message>, _: ()) {
match message {
// Messages
WorkspaceMessage::NodeGraphToggleVisibility => {
self.node_graph_visible = !self.node_graph_visible;
}
}
}
fn actions(&self) -> ActionList {
actions!(WorkspaceMessageDiscriminant;
NodeGraphToggleVisibility,
)
}
}
+48 -88
View File
@@ -8,13 +8,11 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graphene_std::application_io::TimingInformation;
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig};
use graphene_std::renderer::RenderSvgSegmentList;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderMetadata, format_transform_matrix};
use graphene_std::text::FontCache;
use graphene_std::transform::Footprint;
use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::RenderOutputType;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
mod runtime_io;
@@ -33,7 +31,6 @@ pub struct ExecutionResponse {
execution_id: u64,
result: Result<TaggedValue, String>,
responses: VecDeque<FrontendMessage>,
transform: DAffine2,
vector_modify: HashMap<NodeId, Vector>,
/// The resulting value from the temporary inspected during execution
inspect_result: Option<InspectResult>,
@@ -57,7 +54,7 @@ pub struct NodeGraphExecutor {
current_execution_id: u64,
futures: HashMap<u64, ExecutionContext>,
node_graph_hash: u64,
old_inspect_node: Option<NodeId>,
previous_node_to_inspect: Option<NodeId>,
}
#[derive(Debug, Clone)]
@@ -79,7 +76,7 @@ impl NodeGraphExecutor {
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
node_graph_hash: 0,
current_execution_id: 0,
old_inspect_node: None,
previous_node_to_inspect: None,
};
(node_runtime, node_executor)
}
@@ -112,24 +109,25 @@ impl NodeGraphExecutor {
let instrumented = Instrumented::new(&mut network);
self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None }))
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
.map_err(|e| e.to_string())?;
Ok(instrumented)
}
/// Update the cached network if necessary.
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
let network_hash = document.network_interface.document_network().current_hash();
// Refresh the graph when it changes or the inspect node changes
if network_hash != self.node_graph_hash || self.old_inspect_node != inspect_node || ignore_hash {
if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash {
let network = document.network_interface.document_network().clone();
self.old_inspect_node = inspect_node;
self.previous_node_to_inspect = node_to_inspect;
self.node_graph_hash = network_hash;
self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node }))
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect }))
.map_err(|e| e.to_string())?;
}
Ok(())
}
@@ -172,10 +170,10 @@ impl NodeGraphExecutor {
document_id: DocumentId,
viewport_resolution: UVec2,
time: TimingInformation,
inspect_node: Option<NodeId>,
node_to_inspect: Option<NodeId>,
ignore_hash: bool,
) -> Result<Message, String> {
self.update_node_graph(document, inspect_node, ignore_hash)?;
self.update_node_graph(document, node_to_inspect, ignore_hash)?;
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, time)
}
@@ -209,7 +207,7 @@ impl NodeGraphExecutor {
// Execute the node graph
self.runtime_io
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None }))
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None }))
.map_err(|e| e.to_string())?;
let execution_id = self.queue_execution(render_config);
let execution_context = ExecutionContext {
@@ -223,7 +221,7 @@ impl NodeGraphExecutor {
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(RenderOutput {
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. },
data: RenderOutputType::Svg { svg, .. },
..
}) = node_graph_output
else {
@@ -251,6 +249,7 @@ impl NodeGraphExecutor {
let size = (size * scale_factor).into();
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
}
Ok(())
}
@@ -263,7 +262,6 @@ impl NodeGraphExecutor {
execution_id,
result,
responses: existing_responses,
transform,
vector_modify,
inspect_result,
} = execution_response;
@@ -286,17 +284,17 @@ impl NodeGraphExecutor {
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config {
// Special handling for exporting the artwork
self.export(node_graph_output, export_config, responses)?
self.export(node_graph_output, export_config, responses)?;
} else {
self.process_node_graph_output(node_graph_output, transform, responses)?
self.process_node_graph_output(node_graph_output, responses)?;
}
responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
responses.add(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
// Update the spreadsheet on the frontend using the value of the inspect result.
if self.old_inspect_node.is_some() {
if let Some(inspect_result) = inspect_result {
responses.add(SpreadsheetMessage::UpdateLayout { inspect_result });
}
// Update the Data panel on the frontend using the value of the inspect result.
if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() {
responses.add(DataPanelMessage::UpdateLayout { inspect_result });
} else {
responses.add(DataPanelMessage::ClearLayout);
}
}
NodeGraphUpdate::CompilationResponse(execution_response) => {
@@ -329,84 +327,45 @@ impl NodeGraphExecutor {
}
}
}
Ok(())
}
fn debug_render(render_object: impl Render, transform: DAffine2, responses: &mut VecDeque<Message>) {
// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams {
view_mode: ViewMode::Normal,
culling_bounds: None,
thumbnail: false,
hide_artboards: false,
for_export: false,
for_mask: false,
alignment_parent_transform: None,
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(render_output) = node_graph_output else {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
};
// Render SVG
render_object.render_svg(&mut render, &render_params);
// Concatenate the defs and the SVG into one string
render.wrap_with_transform(transform, None);
let svg = render.svg.to_svg_string();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
let mut render_output_metadata = RenderMetadata::default();
match node_graph_output {
TaggedValue::RenderOutput(render_output) => {
match render_output.data {
graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {}
_ => {
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
}
}
render_output_metadata = render_output.metadata;
match render_output.data {
RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::DVec2(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Vector(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Graphic(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Raster(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
_ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
RenderOutputType::CanvasFrame(frame) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
};
let graphene_std::renderer::RenderMetadata {
upstream_footprints: footprints,
RenderOutputType::Texture { .. } => {}
_ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)),
}
let RenderMetadata {
upstream_footprints,
local_transforms,
first_element_source_id,
click_targets,
clip_targets,
} = render_output_metadata;
} = render_output.metadata;
// Run these update state messages immediately
responses.add(DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints,
upstream_footprints,
local_transforms,
first_element_source_id,
});
@@ -415,6 +374,7 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);
Ok(())
}
}
+46 -45
View File
@@ -8,13 +8,14 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_std::bounds::RenderBoundingBox;
use graphene_std::memo::IORecord;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_std::table::{Table, TableRow};
use graphene_std::text::FontCache;
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
use graphene_std::{Artboard, Context, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
@@ -42,7 +43,7 @@ pub struct NodeRuntime {
node_graph_errors: GraphErrors,
monitor_nodes: Vec<Vec<NodeId>>,
/// Which node is inspected and which monitor node is used (if any) for the current execution
/// Which node is inspected and which monitor node is used (if any) for the current execution.
inspect_state: Option<InspectState>,
/// Mapping of the fully-qualified node paths to their preprocessor substitutions.
@@ -67,7 +68,7 @@ pub enum GraphRuntimeRequest {
pub struct GraphUpdate {
pub(super) network: NodeNetwork,
/// The node that should be temporary inspected during execution
pub(super) inspect_node: Option<NodeId>,
pub(super) node_to_inspect: Option<NodeId>,
}
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -188,22 +189,21 @@ impl NodeRuntime {
let _ = self.update_network(graph).await;
}
}
GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, inspect_node }) => {
GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, node_to_inspect }) => {
// Insert the monitor node to manage the inspection
self.inspect_state = inspect_node.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
self.inspect_state = node_to_inspect.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
self.old_graph = Some(network.clone());
self.node_graph_errors.clear();
let result = self.update_network(network).await;
let node_graph_errors = self.node_graph_errors.clone();
self.update_thumbnails = true;
self.sender.send_generation_response(CompilationResponse {
result,
node_graph_errors: self.node_graph_errors.clone(),
});
self.sender.send_generation_response(CompilationResponse { result, node_graph_errors });
}
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => {
let transform = render_config.viewport.transform;
let result = self.execute_network(render_config).await;
let mut responses = VecDeque::new();
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
@@ -227,7 +227,6 @@ impl NodeRuntime {
execution_id,
result,
responses,
transform,
vector_modify: self.vector_modify.clone(),
inspect_result,
});
@@ -292,51 +291,49 @@ impl NodeRuntime {
if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) {
continue;
}
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
warn!("Monitor node has invalid node id");
continue;
};
// Extract the monitor node's stored `Graphic` data.
// Extract the monitor node's stored `Graphic` data
let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else {
// TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds)
#[cfg(debug_assertions)]
warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err());
continue;
};
// Graphic table: thumbnail
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
// Insert the vector modify if we are dealing with vector data
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
if update_thumbnails {
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
}
}
// Artboard table: thumbnail
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
if update_thumbnails {
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
}
}
// Vector table: vector modifications
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
// Insert the vector modify
let default = TableRow::default();
self.vector_modify
.insert(parent_network_node_id, record.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
} else {
.insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
}
// Other
else {
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
}
}
}
// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn process_graphic(
thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>,
parent_network_node_id: NodeId,
graphic: &impl Render,
responses: &mut VecDeque<FrontendMessage>,
update_thumbnails: bool,
) {
// RENDER THUMBNAIL
if !update_thumbnails {
return;
}
/// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn render_thumbnail(thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque<FrontendMessage>) {
// Skip thumbnails if the layer is too complex (for performance)
if graphic.render_complexity() > 1000 {
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
@@ -349,24 +346,28 @@ impl NodeRuntime {
return;
}
let bounds = graphic.bounding_box(DAffine2::IDENTITY, true);
let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) {
RenderBoundingBox::None => return,
RenderBoundingBox::Infinite => [DVec2::ZERO, DVec2::new(300., 200.)],
RenderBoundingBox::Rectangle(bounds) => bounds,
};
let footprint = Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
quality: RenderQuality::Full,
};
// Render the thumbnail from a `Graphic` into an SVG string
let render_params = RenderParams {
view_mode: ViewMode::Normal,
culling_bounds: bounds,
footprint,
thumbnail: true,
hide_artboards: false,
for_export: false,
for_mask: false,
alignment_parent_transform: None,
..Default::default()
};
let mut render = SvgRender::new();
graphic.render_svg(&mut render, &render_params);
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
let [min, max] = bounds.unwrap_or_default();
render.format_svg(min, max);
render.format_svg(bounds[0], bounds[1]);
// UPDATE FRONTEND THUMBNAIL
-14
View File
@@ -82,18 +82,4 @@ impl DebugMessageTree {
pub fn message_handler_fields(&self) -> Option<&MessageData> {
self.message_handler.as_ref()
}
pub fn has_message_handler_data_fields(&self) -> bool {
match self.message_handler_data_fields() {
Some(_) => true,
None => false,
}
}
pub fn has_message_handler_fields(&self) -> bool {
match self.message_handler_fields() {
Some(_) => true,
None => false,
}
}
}