mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 06:08:13 +08:00
Merge origin/master into the async record refactor
Scaffolding merge for the reconcile; the final series to master is authored fresh. Rank plumbing resolves to our axis-IR model, the node macro and the LaneSource render walk stay ours, master's vector restructure and gradient vocabulary are adopted, and the paint and appearance adoption is deliberately deferred behind our fill and stroke markers.
This commit is contained in:
+4
-1
@@ -8,7 +8,7 @@ edition = "2024"
|
||||
readme = "../README.md"
|
||||
homepage = "https://graphite.art"
|
||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||
license = "Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["gpu"]
|
||||
@@ -44,6 +44,7 @@ tsify = { workspace = true }
|
||||
dyn-any = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
simplecss = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
@@ -71,3 +72,5 @@ wasm-bindgen-futures = { workspace = true }
|
||||
# Workspace dependencies
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
const GRAPHITE_RELEASE_SERIES: &str = "Alpha 4";
|
||||
const GRAPHITE_RELEASE_SERIES: &str = "Alpha 5";
|
||||
|
||||
fn main() {
|
||||
// Instruct Cargo to rerun this build script if any of these environment variables change.
|
||||
|
||||
@@ -135,7 +135,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
// BRUSH TOOL
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||
pub const BRUSH_SIZE_DEFAULT: f64 = 40.;
|
||||
pub const BRUSH_HARDNESS_DEFAULT: f64 = 0.;
|
||||
pub const BRUSH_FLOW_DEFAULT: f64 = 100.;
|
||||
|
||||
// EYEDROPPER TOOL
|
||||
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct DispatcherMessageHandlers {
|
||||
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
|
||||
preferences_message_handler: PreferencesMessageHandler,
|
||||
pub(crate) resource_storage_message_handler: ResourceStorageMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
pub(crate) tool_message_handler: ToolMessageHandler,
|
||||
viewport_message_handler: ViewportMessageHandler,
|
||||
}
|
||||
|
||||
@@ -96,8 +96,9 @@ impl Dispatcher {
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
||||
let mut s = Self::default();
|
||||
let mut s = Self::new(Arc::new(graph_craft::application_io::resource::HashMapResourceStorage::new()), None);
|
||||
s.message_handlers.portfolio_message_handler = PortfolioMessageHandler::with_executor(executor);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::messages::prelude::*;
|
||||
pub enum AppWindowMessage {
|
||||
PointerLock,
|
||||
PointerLockMove { x: f64, y: f64 },
|
||||
DirectInput { enabled: bool },
|
||||
Restart,
|
||||
Close,
|
||||
Minimize,
|
||||
|
||||
@@ -17,6 +17,12 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
AppWindowMessage::PointerLockMove { x, y } => {
|
||||
responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) });
|
||||
}
|
||||
AppWindowMessage::DirectInput { enabled } => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
responses.add(FrontendMessage::WindowUpdateDirectInput { enabled });
|
||||
#[cfg(target_family = "wasm")]
|
||||
let _ = enabled;
|
||||
}
|
||||
AppWindowMessage::Close => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
responses.add(FrontendMessage::WindowClose);
|
||||
|
||||
@@ -8,5 +8,7 @@ pub enum EventMessage {
|
||||
CanvasTransformed,
|
||||
ToolAbort,
|
||||
SelectionChanged,
|
||||
/// The document graph's nodes or wires changed, so state derived from the selection's chains may be stale
|
||||
GraphChanged,
|
||||
WorkingColorChanged,
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ use graph_craft::application_io::resource::{DataSource, ResourceHash};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::subpath::BezierHandles;
|
||||
use graphene_std::vector::misc::HandleId;
|
||||
use graphene_std::vector::misc::{BezierHandles, HandleId, point_to_dvec2, segment_to_handles};
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
use kurbo::ParamCurve;
|
||||
use std::sync::Arc;
|
||||
|
||||
const CLIPBOARD_PREFIX: &str = "graphite: ";
|
||||
@@ -182,13 +182,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
||||
let mut resource_ids = HashSet::new();
|
||||
for item in &items {
|
||||
match item {
|
||||
ClipboardItem::Layer(entry) => entry
|
||||
.nodes
|
||||
.iter()
|
||||
.for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)),
|
||||
ClipboardItem::Nodes(nodes) => nodes
|
||||
.iter()
|
||||
.for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)),
|
||||
ClipboardItem::Layer(entry) => entry.nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
|
||||
ClipboardItem::Nodes(nodes) => nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
|
||||
ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -252,9 +247,20 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
||||
let mut vectors = Vec::new();
|
||||
let mut resources = Vec::new();
|
||||
for item in items {
|
||||
// Pasted templates skip document migration, so stored types normalize here
|
||||
match item {
|
||||
ClipboardItem::Layer(entry) => layers.push(entry),
|
||||
ClipboardItem::Nodes(nodes) => node_groups.push(nodes),
|
||||
ClipboardItem::Layer(mut entry) => {
|
||||
for (_, template) in &mut entry.nodes {
|
||||
template.normalize_stored_types();
|
||||
}
|
||||
layers.push(entry);
|
||||
}
|
||||
ClipboardItem::Nodes(mut nodes) => {
|
||||
for (_, template) in &mut nodes {
|
||||
template.normalize_stored_types();
|
||||
}
|
||||
node_groups.push(nodes);
|
||||
}
|
||||
ClipboardItem::Vector(vector) => vectors.push(vector),
|
||||
ClipboardItem::Resource(resource) => resources.push(resource),
|
||||
}
|
||||
@@ -419,7 +425,7 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
||||
|
||||
// Create new segment ids and add the segments into the existing Vector path
|
||||
let mut segments_map = HashMap::new();
|
||||
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() {
|
||||
for (segment_id, segment, start, end) in new_vector.segment_iter() {
|
||||
let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else {
|
||||
warn!("Skipping pasted vector segment with an unknown endpoint");
|
||||
continue;
|
||||
@@ -428,10 +434,12 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
||||
let new_segment_id = SegmentId::generate();
|
||||
segments_map.insert(segment_id, new_segment_id);
|
||||
|
||||
let handles = match bezier.handles {
|
||||
let segment_start = point_to_dvec2(segment.start());
|
||||
let segment_end = point_to_dvec2(segment.end());
|
||||
let handles = match segment_to_handles(&segment) {
|
||||
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)],
|
||||
BezierHandles::Quadratic { handle } => [Some(handle - segment_start), None],
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - segment_start), Some(handle_end - segment_end)],
|
||||
};
|
||||
|
||||
let points = [start_point, end_point];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread};
|
||||
|
||||
/// Identifies which RGB channel a numeric input change targets.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
@@ -47,6 +47,16 @@ pub enum ColorPickerMessage {
|
||||
|
||||
/// `SpectrumInput` change: marker move/insert/delete, midpoint move/reset, or active marker selection changed.
|
||||
GradientUpdate { update: SpectrumInputUpdate },
|
||||
/// Gradient spread choice from the gradient "Ends" selection.
|
||||
SetGradientSpread { gradient_spread: GradientSpread },
|
||||
/// Gradient cyclic choice: whether the stops wrap as a cycle, from the "Cyclic" checkbox.
|
||||
SetGradientCyclic { gradient_cyclic: bool },
|
||||
/// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
|
||||
SetGradientSpace { gradient_space: GradientSpace },
|
||||
/// Gradient hue direction choice: which way around the hue wheel the stops interpolate in a polar space, from the "Arc" dropdown.
|
||||
SetGradientHueDirection { gradient_hue_direction: GradientHueDirection },
|
||||
/// Gradient interpolation choice: the path the stops interpolate along, from the "Intrp." dropdown.
|
||||
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
|
||||
|
||||
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
|
||||
StartTransaction,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::core_types::misc::parse_css_color;
|
||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientStops, GradientStopsUI};
|
||||
use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops};
|
||||
|
||||
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
||||
const MIN_MIDPOINT: f64 = 0.01;
|
||||
@@ -28,7 +28,12 @@ pub struct ColorPickerMessageHandler {
|
||||
old_is_none: bool,
|
||||
|
||||
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
|
||||
gradient: Option<GradientStops>,
|
||||
gradient: Option<Gradient>,
|
||||
gradient_spread: GradientSpread,
|
||||
gradient_space: GradientSpace,
|
||||
gradient_cyclic: bool,
|
||||
gradient_hue_direction: GradientHueDirection,
|
||||
gradient_interpolation: GradientInterpolation,
|
||||
active_marker_index: Option<u32>,
|
||||
active_marker_is_midpoint: bool,
|
||||
|
||||
@@ -50,6 +55,11 @@ impl Default for ColorPickerMessageHandler {
|
||||
old_alpha: 1.,
|
||||
old_is_none: true,
|
||||
gradient: None,
|
||||
gradient_spread: GradientSpread::default(),
|
||||
gradient_space: GradientSpace::default(),
|
||||
gradient_cyclic: false,
|
||||
gradient_hue_direction: GradientHueDirection::default(),
|
||||
gradient_interpolation: GradientInterpolation::default(),
|
||||
active_marker_index: None,
|
||||
active_marker_is_midpoint: false,
|
||||
allow_none: true,
|
||||
@@ -70,20 +80,36 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
FillChoice::None => {
|
||||
self.set_new_hsva(0., 0., 0., 1., true);
|
||||
self.gradient = None;
|
||||
self.gradient_spread = GradientSpread::default();
|
||||
self.gradient_space = GradientSpace::default();
|
||||
self.gradient_cyclic = false;
|
||||
self.gradient_hue_direction = GradientHueDirection::default();
|
||||
self.gradient_interpolation = GradientInterpolation::default();
|
||||
self.active_marker_index = None;
|
||||
self.active_marker_is_midpoint = false;
|
||||
}
|
||||
FillChoice::Solid(color) => {
|
||||
self.gradient = None;
|
||||
self.gradient_spread = GradientSpread::default();
|
||||
self.gradient_space = GradientSpace::default();
|
||||
self.gradient_cyclic = false;
|
||||
self.gradient_hue_direction = GradientHueDirection::default();
|
||||
self.gradient_interpolation = GradientInterpolation::default();
|
||||
self.active_marker_index = None;
|
||||
self.active_marker_is_midpoint = false;
|
||||
self.adopt_color(color);
|
||||
}
|
||||
FillChoice::Gradient(stops) => {
|
||||
FillChoice::Gradient(ramp) => {
|
||||
self.active_marker_index = Some(0);
|
||||
self.active_marker_is_midpoint = false;
|
||||
let first_color = stops.color.first().copied().unwrap_or(Color::BLACK);
|
||||
self.gradient = Some(stops);
|
||||
self.gradient_spread = ramp.gradient_spread;
|
||||
self.gradient_space = ramp.gradient_space;
|
||||
self.gradient_cyclic = ramp.gradient_cyclic;
|
||||
self.gradient_hue_direction = ramp.gradient_hue_direction;
|
||||
self.gradient_interpolation = ramp.gradient_interpolation;
|
||||
let gradient = Gradient::from(ramp);
|
||||
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
|
||||
self.gradient = Some(gradient);
|
||||
self.adopt_color(first_color);
|
||||
}
|
||||
}
|
||||
@@ -154,7 +180,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
match preset {
|
||||
FillChoice::None => {
|
||||
self.set_new_hsva(0., 0., 0., 1., true);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::<SRGBA8>::None });
|
||||
}
|
||||
FillChoice::Solid(color) => {
|
||||
self.adopt_color(color);
|
||||
@@ -179,13 +205,62 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
|
||||
|
||||
if self.is_none {
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::<SRGBA8>::None });
|
||||
} else {
|
||||
self.emit_color(responses);
|
||||
}
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::GradientUpdate { update } => self.apply_gradient_update(update, responses),
|
||||
ColorPickerMessage::SetGradientSpread { gradient_spread } => {
|
||||
let Some(gradient) = &self.gradient else { return };
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
self.gradient_spread = gradient_spread;
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::SetGradientCyclic { gradient_cyclic } => {
|
||||
let Some(gradient) = &mut self.gradient else { return };
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
|
||||
let previous_cyclic = std::mem::replace(&mut self.gradient_cyclic, gradient_cyclic);
|
||||
gradient.hold_positions_across_cyclic_change(previous_cyclic, gradient_cyclic);
|
||||
|
||||
let ramp = GradientRamp::from(&*gradient);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::SetGradientSpace { gradient_space } => {
|
||||
let Some(gradient) = &self.gradient else { return };
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
self.gradient_space = gradient_space;
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction } => {
|
||||
let Some(gradient) = &self.gradient else { return };
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
self.gradient_hue_direction = gradient_hue_direction;
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
|
||||
let Some(gradient) = &self.gradient else { return };
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
self.gradient_interpolation = gradient_interpolation;
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(GradientRamp::from(gradient).with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
ColorPickerMessage::StartTransaction => {
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
}
|
||||
@@ -234,6 +309,17 @@ impl ColorPickerMessageHandler {
|
||||
self.old_is_none = is_none;
|
||||
}
|
||||
|
||||
/// The whole-ramp settings the picker is currently editing with, bundled for sampling and for emitting ramps.
|
||||
fn gradient_settings(&self) -> GradientSettings {
|
||||
GradientSettings {
|
||||
spread: self.gradient_spread,
|
||||
cyclic: self.gradient_cyclic,
|
||||
space: self.gradient_space,
|
||||
hue_direction: self.gradient_hue_direction,
|
||||
interpolation: self.gradient_interpolation,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_old(&mut self) {
|
||||
self.old_hue = self.hue;
|
||||
self.old_saturation = self.saturation;
|
||||
@@ -266,18 +352,16 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
if let Some(gradient) = &mut self.gradient
|
||||
&& let Some(active_index) = self.active_marker_index
|
||||
&& let Some(stop_color) = gradient.color.get_mut(active_index as usize)
|
||||
&& (active_index as usize) < gradient.len()
|
||||
{
|
||||
*stop_color = color;
|
||||
let stops = gradient.clone();
|
||||
let fill_choice = FillChoice::Gradient(stops);
|
||||
gradient.set_color(active_index as usize, color);
|
||||
let ramp = GradientRamp::from(&*gradient);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoiceUI::from(&fill_choice),
|
||||
value: FillChoice::Gradient(ramp.with_settings(self.gradient_settings())),
|
||||
});
|
||||
} else {
|
||||
let fill_choice = FillChoice::Solid(color);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoiceUI::from(&fill_choice),
|
||||
value: FillChoice::Solid(SRGBA8::from(color)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -305,7 +389,7 @@ impl ColorPickerMessageHandler {
|
||||
self.active_marker_is_midpoint = active_marker_is_midpoint;
|
||||
if let Some(index) = active_marker_index
|
||||
&& let Some(gradient) = &self.gradient
|
||||
&& let Some(color) = gradient.color.get(index as usize).copied()
|
||||
&& let Some(color) = gradient.color(index as usize)
|
||||
{
|
||||
self.adopt_color(color);
|
||||
self.snapshot_old();
|
||||
@@ -318,30 +402,31 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
match update {
|
||||
SpectrumInputUpdate::MoveMarker { index, position } => {
|
||||
let new_index = gradient.move_stop(index as usize, position);
|
||||
let new_index = gradient.move_stop(index as usize, position, self.gradient_cyclic);
|
||||
if Some(index) == self.active_marker_index {
|
||||
self.active_marker_index = Some(new_index as u32);
|
||||
}
|
||||
}
|
||||
SpectrumInputUpdate::MoveMidpoint { index, position } => {
|
||||
if let Some(midpoint) = gradient.midpoint.get_mut(index as usize) {
|
||||
*midpoint = position.clamp(MIN_MIDPOINT, MAX_MIDPOINT);
|
||||
} else {
|
||||
if (index as usize) >= gradient.len() {
|
||||
return;
|
||||
}
|
||||
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
|
||||
}
|
||||
SpectrumInputUpdate::InsertMarker { position } => {
|
||||
let new_index = gradient.insert_stop(position);
|
||||
let new_index = gradient.insert_stop(position, self.gradient_settings());
|
||||
self.active_marker_index = Some(new_index as u32);
|
||||
self.active_marker_is_midpoint = false;
|
||||
if let Some(color) = gradient.color.get(new_index).copied() {
|
||||
if let Some(color) = gradient.color(new_index) {
|
||||
self.adopt_color(color);
|
||||
self.snapshot_old();
|
||||
}
|
||||
}
|
||||
SpectrumInputUpdate::InsertDuplicate { index, position } => {
|
||||
let source = index as usize;
|
||||
let Some(insert_index) = gradient.duplicate_stop(source, position) else { return };
|
||||
let Some(insert_index) = gradient.duplicate_stop(source, position, self.gradient_cyclic) else {
|
||||
return;
|
||||
};
|
||||
// The dragged stop (the duplication source) stays active. Its index shifts up if the frozen copy landed at or before it.
|
||||
let dragged_index = if insert_index <= source { source + 1 } else { source };
|
||||
self.active_marker_index = Some(dragged_index as u32);
|
||||
@@ -349,7 +434,7 @@ impl ColorPickerMessageHandler {
|
||||
}
|
||||
SpectrumInputUpdate::RemoveDuplicate { index } => {
|
||||
let anchor = index as usize;
|
||||
if anchor >= gradient.position.len() || gradient.position.len() <= 2 {
|
||||
if anchor >= gradient.len() || gradient.len() <= 2 {
|
||||
return;
|
||||
}
|
||||
// Never remove the active (dragged) stop itself, this should only ever target the frozen copy.
|
||||
@@ -366,14 +451,14 @@ impl ColorPickerMessageHandler {
|
||||
}
|
||||
SpectrumInputUpdate::DeleteMarker { index } => {
|
||||
// Enforce minimum stop count. The gradient editor needs at least 2 stops to remain meaningful.
|
||||
if gradient.position.len() <= 2 || (index as usize) >= gradient.position.len() {
|
||||
if gradient.len() <= 2 || (index as usize) >= gradient.len() {
|
||||
return;
|
||||
}
|
||||
gradient.remove(index as usize);
|
||||
let new_active = (index as usize).min(gradient.position.len() - 1);
|
||||
let new_active = (index as usize).min(gradient.len() - 1);
|
||||
self.active_marker_index = Some(new_active as u32);
|
||||
self.active_marker_is_midpoint = false;
|
||||
if let Some(color) = gradient.color.get(new_active).copied() {
|
||||
if let Some(color) = gradient.color(new_active) {
|
||||
self.adopt_color(color);
|
||||
self.snapshot_old();
|
||||
}
|
||||
@@ -383,16 +468,17 @@ impl ColorPickerMessageHandler {
|
||||
}
|
||||
SpectrumInputUpdate::ResetMarker { index } => {
|
||||
let i = index as usize;
|
||||
let count = gradient.position.len();
|
||||
let count = gradient.len();
|
||||
if i >= count {
|
||||
return;
|
||||
}
|
||||
// Each stop's "natural" position is its evenly-spaced fraction along 0..1, e.g., for 5 stops: 0, 0.25, 0.5, 0.75, 1. Falls back to the midpoint between neighbors when the natural position would push the stop past another.
|
||||
let left = if i == 0 { 0. } else { gradient.position[i - 1] };
|
||||
let right = gradient.position.get(i + 1).copied().unwrap_or(1.);
|
||||
let natural = if count <= 1 { 0. } else { i as f64 / (count - 1) as f64 };
|
||||
// Each stop's "natural" position is its evenly-spaced fraction along 0..1, e.g., for 5 stops: 0, 0.25, 0.5, 0.75, 1 (or fifths when cyclic, leaving the wrapped interval its share). Falls back to the midpoint between neighbors when the natural position would push the stop past another.
|
||||
let left = if i == 0 { 0. } else { gradient.position(i - 1, self.gradient_cyclic) };
|
||||
let right = if i + 1 < count { gradient.position(i + 1, self.gradient_cyclic) } else { 1. };
|
||||
let denominator = if self.gradient_cyclic { count } else { count - 1 };
|
||||
let natural = if count <= 1 { 0. } else { i as f64 / denominator as f64 };
|
||||
let new_position = if (left..=right).contains(&natural) { natural } else { (left + right) / 2. };
|
||||
let new_index = gradient.move_stop(i, new_position);
|
||||
let new_index = gradient.move_stop(i, new_position, self.gradient_cyclic);
|
||||
if Some(index) == self.active_marker_index {
|
||||
self.active_marker_index = Some(new_index as u32);
|
||||
}
|
||||
@@ -400,11 +486,10 @@ impl ColorPickerMessageHandler {
|
||||
SpectrumInputUpdate::ActiveMarker { .. } => unreachable!("handled above"),
|
||||
}
|
||||
|
||||
self.gradient = Some(gradient.clone());
|
||||
let fill_choice = FillChoice::Gradient(gradient);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoiceUI::from(&fill_choice),
|
||||
value: FillChoice::Gradient(GradientRamp::from(&gradient).with_settings(self.gradient_settings())),
|
||||
});
|
||||
self.gradient = Some(gradient);
|
||||
self.send_layouts(responses);
|
||||
}
|
||||
|
||||
@@ -428,9 +513,15 @@ impl ColorPickerMessageHandler {
|
||||
// Gradient editor (only present when the picker is in gradient mode)
|
||||
if let Some(gradient) = &self.gradient {
|
||||
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
||||
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
||||
let markers = (0..gradient.len())
|
||||
.filter_map(|i| Some(SpectrumMarker::new(gradient.position(i, self.gradient_cyclic), gradient.midpoint(i), gradient.color(i)?)))
|
||||
.collect();
|
||||
let mut row_widgets = vec![
|
||||
SpectrumInput::new(GradientStopsUI::from(gradient))
|
||||
SpectrumInput::new(GradientStops::from(gradient))
|
||||
.track_space(self.gradient_space)
|
||||
.track_cyclic(self.gradient_cyclic)
|
||||
.track_hue_direction(self.gradient_hue_direction)
|
||||
.track_interpolation(self.gradient_interpolation)
|
||||
.markers(markers)
|
||||
.active_marker_index(self.active_marker_index)
|
||||
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
||||
@@ -445,10 +536,10 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
if let Some(active) = self.active_marker_index {
|
||||
let active_index = active as usize;
|
||||
let position_value = if self.active_marker_is_midpoint {
|
||||
gradient.midpoint.get(active_index).copied().unwrap_or(0.)
|
||||
} else {
|
||||
gradient.position.get(active_index).copied().unwrap_or(0.)
|
||||
let position_value = match (self.active_marker_is_midpoint, active_index < gradient.len()) {
|
||||
(_, false) => 0.,
|
||||
(true, true) => gradient.midpoint(active_index),
|
||||
(false, true) => gradient.position(active_index, self.gradient_cyclic),
|
||||
};
|
||||
let is_midpoint = self.active_marker_is_midpoint;
|
||||
let captured_index = active;
|
||||
@@ -524,7 +615,11 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
// Hex
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Hex").tooltip_label("Hex Color Code").tooltip_description(HEX_DESCRIPTION).widget_instance(),
|
||||
TextLabel::new("Hex")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Hex Color Code")
|
||||
.tooltip_description(HEX_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
TextInput::new(hex_value)
|
||||
.centered(true)
|
||||
@@ -537,7 +632,11 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
// RGB
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("RGB").tooltip_label("Red/Green/Blue").tooltip_description("Integers 0–255.").widget_instance(),
|
||||
TextLabel::new("RGB")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Red/Green/Blue")
|
||||
.tooltip_description("Integers 0–255.")
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
rgb_input(RgbChannel::Red, rgb_255.map(|(r, _, _)| r), "Red Channel", self.disabled),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
@@ -549,8 +648,9 @@ impl ColorPickerMessageHandler {
|
||||
// HSV
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("HSV")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Hue/Saturation/Value")
|
||||
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB). Not to be confused with Hue/Saturation/Lightness (HSL), a different color model.")
|
||||
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB), but distinct from Hue/Saturation/Lightness (HSL), a different color model.")
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
hsv_input(
|
||||
@@ -586,7 +686,11 @@ impl ColorPickerMessageHandler {
|
||||
|
||||
// Alpha
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Alpha").tooltip_label("Alpha").tooltip_description(ALPHA_DESCRIPTION).widget_instance(),
|
||||
TextLabel::new("Alpha")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Alpha")
|
||||
.tooltip_description(ALPHA_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
NumberInput::new(if self.is_none { None } else { Some(self.alpha * 100.) })
|
||||
.disabled(self.disabled)
|
||||
@@ -602,6 +706,98 @@ impl ColorPickerMessageHandler {
|
||||
.widget_instance(),
|
||||
]));
|
||||
|
||||
// Gradient spread, trailed by the cyclic wrap toggle (only present when the picker is in gradient mode)
|
||||
if self.gradient.is_some() {
|
||||
let entries = RadioEntryData::list_from_choice_type(|gradient_spread| ColorPickerMessage::SetGradientSpread { gradient_spread }.into());
|
||||
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Ends")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Spread / Cyclic")
|
||||
.tooltip_description(ENDS_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
RadioInput::new(entries)
|
||||
.narrow(true)
|
||||
.selected_index(Some(self.gradient_spread as u32))
|
||||
.disabled(self.disabled)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(self.gradient_cyclic)
|
||||
.icon("Link")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Cyclic")
|
||||
.tooltip_description(CYCLIC_DESCRIPTION)
|
||||
.on_update(|checkbox_input: &CheckboxInput| {
|
||||
ColorPickerMessage::SetGradientCyclic {
|
||||
gradient_cyclic: checkbox_input.checked,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
]));
|
||||
}
|
||||
|
||||
// Gradient interpolation (only present when the picker is in gradient mode)
|
||||
if self.gradient.is_some() {
|
||||
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
|
||||
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Intrp.")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Interpolation")
|
||||
.tooltip_description(INTERPOLATION_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
DropdownInput::new(entries)
|
||||
.selected_index(Some(self.gradient_interpolation as u32))
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Interpolation")
|
||||
.tooltip_description(INTERPOLATION_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
]));
|
||||
}
|
||||
|
||||
// Gradient color space (only present when the picker is in gradient mode)
|
||||
if self.gradient.is_some() {
|
||||
let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into());
|
||||
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Space")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Space")
|
||||
.tooltip_description(SPACE_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
DropdownInput::new(entries)
|
||||
.selected_index(Some(self.gradient_space as u32))
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Space")
|
||||
.tooltip_description(SPACE_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
]));
|
||||
}
|
||||
|
||||
// Gradient hue direction (only present when the chosen space is polar)
|
||||
if self.gradient.is_some() && self.gradient_space.is_polar() {
|
||||
let entries = MenuListEntry::sections_from_choice_type(|gradient_hue_direction| ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction }.into());
|
||||
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
TextLabel::new("Arc")
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Hue Direction")
|
||||
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
DropdownInput::new(entries)
|
||||
.selected_index(Some(self.gradient_hue_direction as u32))
|
||||
.disabled(self.disabled)
|
||||
.tooltip_label("Gradient Hue Direction")
|
||||
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
|
||||
.widget_instance(),
|
||||
]));
|
||||
}
|
||||
|
||||
// Color presets (None / Black / White / pure colors / eyedropper)
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
ColorPresetsInput::default()
|
||||
@@ -655,6 +851,11 @@ const HUE_DESCRIPTION: &str = "The shade along the spectrum of the rainbow.";
|
||||
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
||||
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
||||
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
||||
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends, and whether its stops cycle back around from last to first.";
|
||||
const CYCLIC_DESCRIPTION: &str = "Treats the stops as a cycle, interpolating from the last stop back around to the first.";
|
||||
const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
|
||||
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
|
||||
const INTERPOLATION_DESCRIPTION: &str = "The path the stops interpolate along, deciding whether the gradient jumps, turns corners, or flows smoothly through them.";
|
||||
|
||||
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
||||
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
||||
|
||||
@@ -275,11 +275,7 @@ impl PreferencesDialogMessageHandler {
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let brush_tool_description = "
|
||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||
\n\
|
||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\
|
||||
\n\
|
||||
Content created with the Brush tool may not be compatible with future versions of Graphite.\n\
|
||||
Enable the Brush tool to support basic layer painting.\n\
|
||||
\n\
|
||||
*Default: Off.*
|
||||
"
|
||||
|
||||
@@ -46,11 +46,15 @@ impl LayoutHolder for LicensesDialog {
|
||||
let year = &self.localized_commit_year;
|
||||
let description = format!(
|
||||
"
|
||||
Graphite source code is copyright © {year} Graphite contrib-\nutors and is available under the Apache License 2.0. See\n\"Source Code License\" for details.\n\
|
||||
Graphite source code is copyright © {year} Graphite contrib-\n\
|
||||
utors and is available under both the MIT and Apache 2.0\n\
|
||||
licenses. See \"Source Code License\" for details.\n\
|
||||
\n\
|
||||
The Graphite logo, icons, and visual identity are copyright ©\n{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
|
||||
The Graphite logo, icons, and visual identity are copyright ©\n\
|
||||
{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
|
||||
\n\
|
||||
Graphite is distributed with third-party open source code\ndependencies. See \"Dependency Licenses\" for details.
|
||||
Graphite is distributed with third-party open source code\n\
|
||||
dependencies. See \"Dependency Licenses\" for details.
|
||||
"
|
||||
);
|
||||
let description = description.trim();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::IconName;
|
||||
use super::utility_types::{MouseCursorIcon, PersistedState};
|
||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, RasterizedImage};
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FileFilter, RasterizedImage};
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||
@@ -14,7 +14,7 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::vector::style::FillChoiceUI;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -87,16 +87,24 @@ pub enum FrontendMessage {
|
||||
commit_date: String,
|
||||
},
|
||||
TriggerDisplayThirdPartyLicensesDialog,
|
||||
TriggerOpen {
|
||||
filters: Vec<FileFilter>,
|
||||
},
|
||||
TriggerImport {
|
||||
filters: Vec<FileFilter>,
|
||||
},
|
||||
TriggerSaveDocument {
|
||||
document_id: DocumentId,
|
||||
name: String,
|
||||
path: Option<PathBuf>,
|
||||
folder: Option<PathBuf>,
|
||||
filters: Vec<FileFilter>,
|
||||
content: serde_bytes::ByteBuf,
|
||||
},
|
||||
TriggerSaveFile {
|
||||
name: String,
|
||||
folder: Option<PathBuf>,
|
||||
filters: Vec<FileFilter>,
|
||||
content: serde_bytes::ByteBuf,
|
||||
},
|
||||
TriggerExportImage {
|
||||
@@ -128,8 +136,6 @@ pub enum FrontendMessage {
|
||||
},
|
||||
TriggerOpenLaunchDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerOpen,
|
||||
TriggerImport,
|
||||
TriggerSavePreferences {
|
||||
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
|
||||
preferences: PreferencesMessageHandler,
|
||||
@@ -165,7 +171,7 @@ pub enum FrontendMessage {
|
||||
},
|
||||
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker />` forwards this as its `colorOrGradient` event.
|
||||
ColorPickerColorChanged {
|
||||
value: FillChoiceUI,
|
||||
value: FillChoice<SRGBA8>,
|
||||
},
|
||||
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker />` forwards this as its `startHistoryTransaction` event.
|
||||
ColorPickerStartHistoryTransaction,
|
||||
@@ -344,6 +350,10 @@ pub enum FrontendMessage {
|
||||
position: (f64, f64),
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
WindowUpdateDirectInput {
|
||||
enabled: bool,
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
WindowClose,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
WindowMinimize,
|
||||
|
||||
@@ -64,6 +64,26 @@ impl FileType {
|
||||
FileType::Svg => "image/svg+xml",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension(self) -> &'static str {
|
||||
match self {
|
||||
FileType::Png => "png",
|
||||
FileType::Jpg => "jpg",
|
||||
FileType::Svg => "svg",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_filter(self) -> FileFilter {
|
||||
let name = match self {
|
||||
FileType::Png => "PNG Image",
|
||||
FileType::Jpg => "JPEG Image",
|
||||
FileType::Svg => "SVG Image",
|
||||
};
|
||||
FileFilter {
|
||||
name: name.into(),
|
||||
extensions: vec![self.extension().into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
@@ -91,3 +111,10 @@ pub struct RasterizedImage {
|
||||
pub height: u32,
|
||||
pub pixels: serde_bytes::ByteBuf,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FileFilter {
|
||||
pub name: String,
|
||||
pub extensions: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseButton;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::pointer::MouseButton;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, KeyMappingMessage, Lookup)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::utility_types::input_keyboard::KeysGroup;
|
||||
use super::utility_types::keyboard::KeysGroup;
|
||||
use super::utility_types::misc::Mapping;
|
||||
use crate::application::Editor;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{self, Key};
|
||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -58,7 +58,7 @@ impl InputMapperMessageHandler {
|
||||
.map(|i| {
|
||||
// TODO: Use a safe solution eventually
|
||||
assert!(
|
||||
i < input_keyboard::NUMBER_OF_KEYS,
|
||||
i < keyboard::NUMBER_OF_KEYS,
|
||||
"Attempting to convert a Key with enum index {i}, which is larger than the number of Key enums",
|
||||
);
|
||||
(i as u8).try_into().unwrap()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::application::Editor;
|
||||
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
|
||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::MouseButton;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates};
|
||||
use crate::messages::input_mapper::utility_types::macros::*;
|
||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
||||
use crate::messages::input_mapper::utility_types::pointer::MouseButton;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::messages::input_mapper::input_mapper_message_handler::InputMapperMessageContext;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
|
||||
@@ -162,7 +162,7 @@ macro_rules! action_shortcut {
|
||||
macro_rules! action_shortcut_manual {
|
||||
($($keys:expr),*) => {
|
||||
Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Shortcut(
|
||||
crate::messages::input_mapper::utility_types::input_keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(),
|
||||
crate::messages::input_mapper::utility_types::keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::input_keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed};
|
||||
use super::keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed};
|
||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{KeyStates, NUMBER_OF_KEYS};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::NUMBER_OF_MOUSE_BUTTONS;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{KeyStates, NUMBER_OF_KEYS};
|
||||
use crate::messages::input_mapper::utility_types::pointer::NUMBER_OF_MOUSE_BUTTONS;
|
||||
use crate::messages::prelude::*;
|
||||
use core::time::Duration;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod input_keyboard;
|
||||
pub mod input_mouse;
|
||||
pub mod keyboard;
|
||||
pub mod pointer;
|
||||
|
||||
pub mod macros;
|
||||
pub mod misc;
|
||||
|
||||
+27
-9
@@ -50,15 +50,21 @@ impl ScrollDelta {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Document the difference between this and EditorMouseState
|
||||
// TODO: Document the difference between this and EditorPointerState
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MouseState {
|
||||
pub struct PointerState {
|
||||
pub position: ViewportPosition,
|
||||
pub mouse_keys: MouseKeys,
|
||||
pub scroll_delta: ScrollDelta,
|
||||
pub time: Option<f64>,
|
||||
pub pressure: Option<f64>,
|
||||
pub tilt: Option<DVec2>,
|
||||
pub twist: Option<f64>,
|
||||
pub wheel: Option<f64>,
|
||||
pub eraser: bool,
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
impl PointerState {
|
||||
pub fn finish_transaction(&self, drag_start: DVec2, responses: &mut VecDeque<Message>) {
|
||||
let drag_too_small = drag_start.distance(self.position) <= DRAG_THRESHOLD;
|
||||
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
|
||||
@@ -66,15 +72,21 @@ impl MouseState {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Document the difference between this and MouseState
|
||||
// TODO: Document the difference between this and PointerState
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorMouseState {
|
||||
pub struct EditorPointerState {
|
||||
pub editor_position: EditorPosition,
|
||||
pub mouse_keys: MouseKeys,
|
||||
pub scroll_delta: ScrollDelta,
|
||||
pub time: Option<f64>,
|
||||
pub pressure: Option<f64>,
|
||||
pub tilt: Option<DVec2>,
|
||||
pub twist: Option<f64>,
|
||||
pub wheel: Option<f64>,
|
||||
pub eraser: bool,
|
||||
}
|
||||
|
||||
impl EditorMouseState {
|
||||
impl EditorPointerState {
|
||||
pub fn from_keys_and_editor_position(keys: u8, editor_position: EditorPosition) -> Self {
|
||||
// TODO: Some graphic tablets send key codes not mentioned in the spec. In the future we would like to support these as well.
|
||||
let mouse_keys = MouseKeys::from_bits_truncate(keys);
|
||||
@@ -82,15 +94,21 @@ impl EditorMouseState {
|
||||
Self {
|
||||
editor_position,
|
||||
mouse_keys,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mouse_state(&self, viewport: &ViewportMessageHandler) -> MouseState {
|
||||
MouseState {
|
||||
pub fn to_pointer_state(&self, viewport: &ViewportMessageHandler) -> PointerState {
|
||||
PointerState {
|
||||
position: (viewport.logical(self.editor_position) - viewport.offset()).into(),
|
||||
mouse_keys: self.mouse_keys,
|
||||
scroll_delta: self.scroll_delta,
|
||||
time: self.time,
|
||||
pressure: self.pressure,
|
||||
tilt: self.tilt,
|
||||
twist: self.twist,
|
||||
wheel: self.wheel,
|
||||
eraser: self.eraser,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::pointer::EditorPointerState;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, InputPreprocessor)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InputPreprocessorMessage {
|
||||
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
DoubleClick { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
KeyDown { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
||||
KeyUp { key: Key, key_repeat: bool, modifier_keys: ModifierKeys },
|
||||
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerShake { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
PointerDown { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
PointerMove { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
PointerUp { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
PointerShake { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
CurrentTime { timestamp: u64 },
|
||||
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||
WheelScroll { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::application::Editor;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::{MouseButton, MouseKeys, MouseState};
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo;
|
||||
use crate::messages::input_mapper::utility_types::pointer::{MouseButton, MouseKeys, PointerState};
|
||||
use crate::messages::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -15,7 +15,8 @@ pub struct InputPreprocessorMessageHandler {
|
||||
pub frame_time: FrameTimeInfo,
|
||||
pub time: u64,
|
||||
pub keyboard: KeyStates,
|
||||
pub mouse: MouseState,
|
||||
pub mouse: PointerState,
|
||||
pointer_down_time: f64,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
@@ -27,10 +28,10 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
||||
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
|
||||
for key in mouse_state.mouse_keys {
|
||||
for key in pointer_state.mouse_keys {
|
||||
responses.add(InputMapperMessage::DoubleClick(match key {
|
||||
MouseKeys::LEFT => MouseButton::Left,
|
||||
MouseKeys::RIGHT => MouseButton::Right,
|
||||
@@ -60,35 +61,35 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
||||
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
|
||||
self.translate_mouse_event(mouse_state, true, responses);
|
||||
self.translate_mouse_event(pointer_state, true, responses);
|
||||
}
|
||||
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
|
||||
responses.add(InputMapperMessage::PointerMove);
|
||||
|
||||
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events
|
||||
self.translate_mouse_event(mouse_state, false, responses);
|
||||
self.translate_mouse_event(pointer_state, false, responses);
|
||||
}
|
||||
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
|
||||
self.translate_mouse_event(mouse_state, false, responses);
|
||||
self.translate_mouse_event(pointer_state, false, responses);
|
||||
}
|
||||
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
|
||||
responses.add(InputMapperMessage::PointerShake);
|
||||
}
|
||||
@@ -100,9 +101,9 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
||||
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
|
||||
self.update_states_of_modifier_keys(modifier_keys, responses);
|
||||
|
||||
let mouse_state = editor_mouse_state.to_mouse_state(viewport);
|
||||
self.mouse.position = mouse_state.position;
|
||||
self.mouse.scroll_delta = mouse_state.scroll_delta;
|
||||
let pointer_state = editor_mouse_state.to_pointer_state(viewport);
|
||||
self.mouse.position = pointer_state.position;
|
||||
self.mouse.scroll_delta = pointer_state.scroll_delta;
|
||||
|
||||
responses.add(InputMapperMessage::WheelScroll);
|
||||
}
|
||||
@@ -116,7 +117,7 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex
|
||||
}
|
||||
|
||||
impl InputPreprocessorMessageHandler {
|
||||
fn translate_mouse_event(&mut self, mut new_state: MouseState, allow_first_button_down: bool, responses: &mut VecDeque<Message>) {
|
||||
fn translate_mouse_event(&mut self, mut new_state: PointerState, allow_first_button_down: bool, responses: &mut VecDeque<Message>) {
|
||||
let click_mappings = [
|
||||
(MouseKeys::LEFT, Key::MouseLeft),
|
||||
(MouseKeys::RIGHT, Key::MouseRight),
|
||||
@@ -144,6 +145,13 @@ impl InputPreprocessorMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_time = new_state.time.unwrap_or(self.time as f64);
|
||||
if self.mouse.mouse_keys == MouseKeys::empty() {
|
||||
self.pointer_down_time = raw_time;
|
||||
}
|
||||
let interacting = (self.mouse.mouse_keys | new_state.mouse_keys) != MouseKeys::empty();
|
||||
new_state.time = interacting.then_some(raw_time - self.pointer_down_time);
|
||||
|
||||
self.mouse = new_state;
|
||||
}
|
||||
|
||||
@@ -185,18 +193,17 @@ impl InputPreprocessorMessageHandler {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
|
||||
use crate::messages::input_mapper::utility_types::pointer::EditorPointerState;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn process_action_mouse_move_handle_modifier_keys() {
|
||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||
|
||||
let editor_mouse_state = EditorMouseState {
|
||||
let editor_mouse_state = EditorPointerState {
|
||||
editor_position: (4., 809.).into(),
|
||||
mouse_keys: MouseKeys::default(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
..Default::default()
|
||||
};
|
||||
let modifier_keys = ModifierKeys::ALT;
|
||||
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
|
||||
@@ -216,7 +223,7 @@ mod test {
|
||||
fn process_action_mouse_down_handle_modifier_keys() {
|
||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||
|
||||
let editor_mouse_state = EditorMouseState::default();
|
||||
let editor_mouse_state = EditorPointerState::default();
|
||||
let modifier_keys = ModifierKeys::CONTROL;
|
||||
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
|
||||
|
||||
@@ -235,7 +242,7 @@ mod test {
|
||||
fn process_action_mouse_up_handle_modifier_keys() {
|
||||
let mut input_preprocessor = InputPreprocessorMessageHandler::default();
|
||||
|
||||
let editor_mouse_state = EditorMouseState::default();
|
||||
let editor_mouse_state = EditorPointerState::default();
|
||||
let modifier_keys = ModifierKeys::SHIFT;
|
||||
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::vector::style::FillChoiceUI;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -117,23 +117,26 @@ impl LayoutMessageHandler {
|
||||
}
|
||||
LayoutGroup::Table(WidgetTable { rows, .. }) => {
|
||||
for (row_index, row) in rows.iter().enumerate() {
|
||||
for (value_index, value) in row.iter().enumerate() {
|
||||
// Return if this is the correct ID
|
||||
if value.widget_id == widget_id {
|
||||
widget_path.push(row_index);
|
||||
widget_path.push(value_index);
|
||||
return Some((value, widget_path));
|
||||
}
|
||||
for (cell_index, cell) in row.iter().enumerate() {
|
||||
for (value_index, value) in cell.iter().enumerate() {
|
||||
// Return if this is the correct ID
|
||||
if value.widget_id == widget_id {
|
||||
widget_path.push(row_index);
|
||||
widget_path.push(cell_index);
|
||||
widget_path.push(value_index);
|
||||
return Some((value, widget_path));
|
||||
}
|
||||
|
||||
if let Widget::PopoverButton(popover) = &*value.widget {
|
||||
stack.extend(
|
||||
popover
|
||||
.popover_layout
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, value_index, child]].concat(), val)),
|
||||
);
|
||||
if let Widget::PopoverButton(popover) = &*value.widget {
|
||||
stack.extend(
|
||||
popover
|
||||
.popover_layout
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, cell_index, value_index, child]].concat(), val)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,12 +197,16 @@ impl LayoutMessageHandler {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
||||
WidgetValueAction::Update => {
|
||||
let Ok(fill_choice_ui) = serde_json::from_value::<FillChoiceUI>(value) else {
|
||||
warn!("ColorInput update was not able to be parsed as FillChoiceUI: {color_button:?}");
|
||||
let Ok(fill_choice) = serde_json::from_value::<FillChoice<SRGBA8>>(value) else {
|
||||
warn!("ColorInput update was not able to be parsed as FillChoice<SRGBA8>: {color_button:?}");
|
||||
return;
|
||||
};
|
||||
color_button.value = fill_choice_ui;
|
||||
(color_button.on_update.callback)(color_button)
|
||||
// The stored copy has to keep mirroring what the frontend was last sent, so the rebuilt layout still
|
||||
// diffs against it and syncs picks that leave the swatch unchanged; the callback borrows the new one
|
||||
let previous_value = std::mem::replace(&mut color_button.value, fill_choice);
|
||||
let update_message = (color_button.on_update.callback)(color_button);
|
||||
color_button.value = previous_value;
|
||||
update_message
|
||||
}
|
||||
};
|
||||
|
||||
@@ -528,9 +535,28 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
||||
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
||||
}
|
||||
Widget::SpectrumInput(spectrum_input) => {
|
||||
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
|
||||
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
||||
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
||||
// The track strip spans exactly 0 to 1, which no spread affects, so the widget carries no spread of its own
|
||||
let settings = graphene_std::vector::style::GradientSettings {
|
||||
spread: Default::default(),
|
||||
cyclic: spectrum_input.track_cyclic,
|
||||
space: spectrum_input.track_space,
|
||||
hue_direction: spectrum_input.track_hue_direction,
|
||||
interpolation: spectrum_input.track_interpolation,
|
||||
};
|
||||
let track_gradient = graphene_std::vector::style::Gradient::from(&spectrum_input.track);
|
||||
spectrum_input.track_samples = track_gradient
|
||||
.interpolated_samples_or_black(settings)
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| SpectrumSample::new(position, color))
|
||||
.collect();
|
||||
// The end caps sample the track's boundary colors, which a cyclic wrap makes the wrapped interval's boundary-crossing color rather than the outermost stops'
|
||||
let track_evaluator = track_gradient.evaluator(settings);
|
||||
let cap = |t: f64| {
|
||||
let color = track_evaluator.evaluate(t);
|
||||
SRGBA8::from(color).to_css_hex()
|
||||
};
|
||||
spectrum_input.track_start_css = cap(0.);
|
||||
spectrum_input.track_end_css = cap(1.);
|
||||
}
|
||||
Widget::ColorComparisonInput(comparison) => {
|
||||
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
||||
@@ -550,7 +576,6 @@ fn evaluate_and_validate_number_input(expression: &str, number_input: &NumberInp
|
||||
let value = math_parser::evaluate(expression)
|
||||
.inspect_err(|err| error!("Math parser error on \"{expression}\": {err}"))
|
||||
.ok()?
|
||||
.0
|
||||
.inspect_err(|err| error!("Math evaluate error on \"{expression}\": {err}"))
|
||||
.ok()?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::widgets::button_widgets::*;
|
||||
use super::widgets::input_widgets::*;
|
||||
use super::widgets::label_widgets::*;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::KeysGroup;
|
||||
use crate::messages::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -273,7 +273,7 @@ impl<'a> Iterator for WidgetIter<'a> {
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
||||
self.table.extend(rows.iter().flatten().rev());
|
||||
self.table.extend(rows.iter().flatten().flatten().rev());
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
||||
@@ -325,7 +325,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Table(WidgetTable { rows, .. })) => {
|
||||
self.table.extend(rows.iter_mut().flatten().rev());
|
||||
self.table.extend(rows.iter_mut().flatten().flatten().rev());
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Section(WidgetSection { layout, .. })) => {
|
||||
@@ -365,8 +365,9 @@ pub struct WidgetRow {
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WidgetTable {
|
||||
/// Rows of cells, each cell holding one or more widgets laid out inline.
|
||||
#[serde(rename = "tableWidgets")]
|
||||
pub rows: Vec<Vec<WidgetInstance>>,
|
||||
pub rows: Vec<Vec<Vec<WidgetInstance>>>,
|
||||
pub unstyled: bool,
|
||||
}
|
||||
|
||||
@@ -412,7 +413,14 @@ impl LayoutGroup {
|
||||
Self::Column(WidgetColumn { widgets })
|
||||
}
|
||||
|
||||
/// A table with one widget per cell.
|
||||
pub fn table(rows: Vec<Vec<WidgetInstance>>, unstyled: bool) -> Self {
|
||||
let rows = rows.into_iter().map(|row| row.into_iter().map(|widget| vec![widget]).collect()).collect();
|
||||
Self::Table(WidgetTable { rows, unstyled })
|
||||
}
|
||||
|
||||
/// A table whose cells may each hold multiple widgets laid out inline.
|
||||
pub fn table_of_cells(rows: Vec<Vec<Vec<WidgetInstance>>>, unstyled: bool) -> Self {
|
||||
Self::Table(WidgetTable { rows, unstyled })
|
||||
}
|
||||
|
||||
@@ -600,13 +608,17 @@ impl Diffable for LayoutGroup {
|
||||
}
|
||||
}
|
||||
Self::Table(WidgetTable { rows, .. }) => {
|
||||
for (row_idx, row) in rows.iter().enumerate() {
|
||||
for (col_idx, widget) in row.iter().enumerate() {
|
||||
widget_path.push(row_idx);
|
||||
widget_path.push(col_idx);
|
||||
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
for (row_index, row) in rows.iter().enumerate() {
|
||||
for (column_index, cell) in row.iter().enumerate() {
|
||||
for (widget_index, widget) in cell.iter().enumerate() {
|
||||
widget_path.push(row_index);
|
||||
widget_path.push(column_index);
|
||||
widget_path.push(widget_index);
|
||||
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,13 +638,17 @@ impl Diffable for LayoutGroup {
|
||||
}
|
||||
}
|
||||
Self::Table(WidgetTable { rows, .. }) => {
|
||||
for (row_idx, row) in rows.iter_mut().enumerate() {
|
||||
for (col_idx, widget) in row.iter_mut().enumerate() {
|
||||
widget_path.push(row_idx);
|
||||
widget_path.push(col_idx);
|
||||
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
for (row_index, row) in rows.iter_mut().enumerate() {
|
||||
for (column_index, cell) in row.iter_mut().enumerate() {
|
||||
for (widget_index, widget) in cell.iter_mut().enumerate() {
|
||||
widget_path.push(row_index);
|
||||
widget_path.push(column_index);
|
||||
widget_path.push(widget_index);
|
||||
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
widget_path.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
|
||||
use derivative::*;
|
||||
use graphene_std::vector::style::FillChoiceUI;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphite_proc_macros::WidgetBuilder;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
@@ -191,9 +192,9 @@ pub struct ImageButton {
|
||||
pub struct ColorInput {
|
||||
// Content
|
||||
#[widget_builder(constructor)]
|
||||
pub value: FillChoiceUI,
|
||||
pub value: FillChoice<SRGBA8>,
|
||||
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
||||
/// `None` when `value` is `FillChoiceUI::None`, in which case the frontend uses its "none" fallback styling.
|
||||
/// `None` when `value` is `FillChoice::<SRGBA8>::None`, in which case the frontend uses its "none" fallback styling.
|
||||
#[serde(rename = "chosenGradient")]
|
||||
#[widget_builder(skip)]
|
||||
pub chosen_gradient: Option<String>,
|
||||
@@ -232,6 +233,24 @@ pub struct ColorInput {
|
||||
pub on_commit: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
/// Shortens a breadcrumb label to fit the trail, replacing the tail with an ellipsis.
|
||||
/// Quotes delimit a string value, so a quoted label budgets only the text between them.
|
||||
pub fn truncate_breadcrumb_label(label: &str) -> String {
|
||||
const MAX_CHARACTERS: usize = 40;
|
||||
|
||||
let quoted = label.len() >= 2 && label.starts_with('"') && label.ends_with('"');
|
||||
let content = if quoted { &label[1..label.len() - 1] } else { label };
|
||||
|
||||
if content.chars().count() <= MAX_CHARACTERS {
|
||||
return label.to_string();
|
||||
}
|
||||
|
||||
let mut truncated: String = content.chars().take(MAX_CHARACTERS - 1).collect();
|
||||
truncated.push('…');
|
||||
|
||||
if quoted { format!("\"{truncated}\"") } else { truncated }
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::messages::frontend::IconName;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use derivative::*;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use graphene_std::vector::style::{FillChoiceUI, GradientStopsUI};
|
||||
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientInterpolation, GradientSpace, GradientStops};
|
||||
use graphite_proc_macros::WidgetBuilder;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
@@ -150,6 +151,35 @@ impl std::hash::Hash for MenuListEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuListEntry {
|
||||
/// One entry per variant of a choice type enum, keeping the choice type's section groupings, shown as the variant's label alongside its icon when it has one.
|
||||
pub fn sections_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> MenuListEntrySections
|
||||
where
|
||||
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
|
||||
{
|
||||
E::list()
|
||||
.iter()
|
||||
.map(|section| {
|
||||
section
|
||||
.iter()
|
||||
.map(|(variant, metadata)| {
|
||||
let to_message = to_message.clone();
|
||||
let variant = *variant;
|
||||
|
||||
let entry = MenuListEntry::new(metadata.name)
|
||||
.label(metadata.label)
|
||||
.tooltip_label(metadata.label)
|
||||
.tooltip_description(metadata.description.unwrap_or_default())
|
||||
.on_update(move |_| to_message(variant));
|
||||
|
||||
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
@@ -344,6 +374,30 @@ pub struct RadioEntryData {
|
||||
pub on_commit: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
impl RadioEntryData {
|
||||
/// One entry per variant of a choice type enum, shown as the variant's icon when it has one and its text label otherwise.
|
||||
pub fn list_from_choice_type<E>(to_message: impl Fn(E) -> Message + Clone + Send + Sync + 'static) -> Vec<Self>
|
||||
where
|
||||
E: graphene_std::choice_type::ChoiceTypeStatic + 'static,
|
||||
{
|
||||
E::list()
|
||||
.iter()
|
||||
.flat_map(|section| section.iter())
|
||||
.map(|(variant, metadata)| {
|
||||
let to_message = to_message.clone();
|
||||
let variant = *variant;
|
||||
|
||||
let entry = RadioEntryData::new(metadata.name)
|
||||
.tooltip_label(metadata.label)
|
||||
.tooltip_description(metadata.description.unwrap_or_default())
|
||||
.on_update(move |_| to_message(variant));
|
||||
|
||||
if let Some(icon) = metadata.icon { entry.icon(icon) } else { entry.label(metadata.label) }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
@@ -520,7 +574,7 @@ pub struct ColorPresetsInput {
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ColorPresetsInputUpdate {
|
||||
Preset(FillChoiceUI),
|
||||
Preset(FillChoice<SRGBA8>),
|
||||
EyedropperColorCode(String),
|
||||
}
|
||||
|
||||
@@ -531,16 +585,28 @@ pub struct SpectrumInput {
|
||||
// Content
|
||||
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
||||
#[widget_builder(constructor)]
|
||||
pub track: GradientStopsUI,
|
||||
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
||||
#[serde(rename = "trackCSS")]
|
||||
pub track: GradientStops<SRGBA8>,
|
||||
/// The color space the track's stops interpolate in, used to bake `track_samples`. Not sent to the frontend.
|
||||
#[serde(skip)]
|
||||
pub track_space: GradientSpace,
|
||||
/// Whether the track's stops wrap as a cycle, used to bake `track_samples` and by the frontend to draw the wrapped interval's midpoint diamond.
|
||||
#[serde(rename = "trackCyclic")]
|
||||
pub track_cyclic: bool,
|
||||
/// The hue direction the track's stops interpolate with in a polar space, used to bake `track_samples`. Not sent to the frontend.
|
||||
#[serde(skip)]
|
||||
pub track_hue_direction: GradientHueDirection,
|
||||
/// The path the track's stops interpolate along, used to bake `track_samples` and by the frontend to suppress the midpoint diamonds when stepped.
|
||||
#[serde(rename = "trackInterpolation")]
|
||||
pub track_interpolation: GradientInterpolation,
|
||||
/// Straight-alpha samples the frontend draws as the stops of an SVG gradient filling the track strip. Auto-populated from `track` at layout-send time.
|
||||
#[serde(rename = "trackSamples")]
|
||||
#[widget_builder(skip)]
|
||||
pub track_css: String,
|
||||
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated from `track`'s first stop.
|
||||
pub track_samples: Vec<SpectrumSample>,
|
||||
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated by evaluating `track` at position 0.
|
||||
#[serde(rename = "trackStartCSS")]
|
||||
#[widget_builder(skip)]
|
||||
pub track_start_css: String,
|
||||
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated from `track`'s last stop.
|
||||
/// Hex string for the track strip's rightmost solid-color end-cap. Auto-populated by evaluating `track` at position 1.
|
||||
#[serde(rename = "trackEndCSS")]
|
||||
#[widget_builder(skip)]
|
||||
pub track_end_css: String,
|
||||
@@ -579,22 +645,46 @@ pub struct SpectrumInput {
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SpectrumMarker {
|
||||
/// Position (0..1) of the marker along the spectrum track.
|
||||
/// Position of the marker along the spectrum track, normally from 0 to 1. A shifted or stretched non-cyclic ramp can
|
||||
/// place it outside that range, where the track draws only the markers falling within its visible span.
|
||||
position: f64,
|
||||
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true. The last marker's value is ignored.
|
||||
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true.
|
||||
/// The last marker's value controls the wrapped interval when `track_cyclic` is set, and is otherwise ignored.
|
||||
midpoint: f64,
|
||||
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
|
||||
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`],
|
||||
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
|
||||
#[serde(rename = "handleColorCSS")]
|
||||
handle_color_css: String,
|
||||
}
|
||||
|
||||
impl SpectrumMarker {
|
||||
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
||||
let handle_color_css = SRGBA8::from(handle_color).to_css_hex();
|
||||
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
|
||||
Self { position, midpoint, handle_color_css }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SpectrumSample {
|
||||
/// Position (0..1) of the sample along the spectrum track, drawn as the SVG stop's `offset`.
|
||||
position: f64,
|
||||
/// `#rrggbb` hex of the sample's color, drawn as the SVG stop's `stop-color`.
|
||||
color: String,
|
||||
/// Straight alpha (0..1) of the sample, drawn as the SVG stop's `stop-opacity`.
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl SpectrumSample {
|
||||
pub fn new(position: f64, color: Color) -> Self {
|
||||
Self {
|
||||
position,
|
||||
color: format!("#{}", SRGBA8::from(color).to_rgb_hex()),
|
||||
alpha: color.a(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SpectrumInputUpdate {
|
||||
|
||||
@@ -64,6 +64,8 @@ pub struct TextLabel {
|
||||
pub italic: bool,
|
||||
pub monospace: bool,
|
||||
pub multiline: bool,
|
||||
pub enquote: bool,
|
||||
pub selectable: bool,
|
||||
#[serde(rename = "centerAlign")]
|
||||
pub center_align: bool,
|
||||
#[serde(rename = "tableAlign")]
|
||||
|
||||
@@ -37,8 +37,7 @@ pub enum PathStep {
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorTableTab {
|
||||
#[default]
|
||||
Properties,
|
||||
Points,
|
||||
Segments,
|
||||
Regions,
|
||||
Handles,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,9 @@ use super::utility_types::network_interface::storage_metadata::{StorageMetadataV
|
||||
#[derivative(Clone, Debug, Default)]
|
||||
pub struct DocumentHistory {
|
||||
/// Stack of document network snapshots for previous history states.
|
||||
lagacy_undo_stack: VecDeque<NodeNetworkInterface>,
|
||||
legacy_undo_stack: VecDeque<NodeNetworkInterface>,
|
||||
/// Stack of document network snapshots for future history states.
|
||||
lagacy_redo_stack: VecDeque<NodeNetworkInterface>,
|
||||
legacy_redo_stack: VecDeque<NodeNetworkInterface>,
|
||||
/// The `Gdd` working copy: owns the CRDT `Session` and mirrors edits to disk. `None` until the mount
|
||||
/// future built by `load_document` resolves.
|
||||
#[derivative(Debug = "ignore")]
|
||||
@@ -31,38 +31,38 @@ impl DocumentHistory {
|
||||
|
||||
/// Push a snapshot onto the undo stack, evicting the oldest entry past the history cap.
|
||||
pub fn push_undo(&mut self, snapshot: NodeNetworkInterface) {
|
||||
Self::push_capped(&mut self.lagacy_undo_stack, snapshot);
|
||||
Self::push_capped(&mut self.legacy_undo_stack, snapshot);
|
||||
}
|
||||
|
||||
/// Push a snapshot onto the redo stack, evicting the oldest entry past the history cap.
|
||||
pub fn push_redo(&mut self, snapshot: NodeNetworkInterface) {
|
||||
Self::push_capped(&mut self.lagacy_redo_stack, snapshot);
|
||||
Self::push_capped(&mut self.legacy_redo_stack, snapshot);
|
||||
}
|
||||
|
||||
/// Pop the most recent undo snapshot, or `None` when the stack is empty.
|
||||
pub fn pop_undo(&mut self) -> Option<NodeNetworkInterface> {
|
||||
self.lagacy_undo_stack.pop_back()
|
||||
self.legacy_undo_stack.pop_back()
|
||||
}
|
||||
|
||||
/// Pop the most recent redo snapshot, or `None` when the stack is empty.
|
||||
pub fn pop_redo(&mut self) -> Option<NodeNetworkInterface> {
|
||||
self.lagacy_redo_stack.pop_back()
|
||||
self.legacy_redo_stack.pop_back()
|
||||
}
|
||||
|
||||
/// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified).
|
||||
pub fn discard_last_undo(&mut self) {
|
||||
self.lagacy_undo_stack.pop_back();
|
||||
self.legacy_undo_stack.pop_back();
|
||||
}
|
||||
|
||||
/// Clear the redo stack, called when a fresh edit invalidates the redo future.
|
||||
pub fn clear_redo(&mut self) {
|
||||
self.lagacy_redo_stack.clear();
|
||||
self.legacy_redo_stack.clear();
|
||||
}
|
||||
|
||||
/// Add the resources referenced by every snapshot in both history stacks into `resources`, so
|
||||
/// history-only resources stay alive for legacy undo/redo.
|
||||
pub fn collect_used_resources(&self, resources: &mut HashSet<ResourceId>) {
|
||||
for interface in self.lagacy_undo_stack.iter().chain(&self.lagacy_redo_stack) {
|
||||
for interface in self.legacy_undo_stack.iter().chain(&self.legacy_redo_stack) {
|
||||
interface.collect_used_resources(resources);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::utility_types::misc::{GroupFolderType, SnappingState};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -245,7 +245,7 @@ pub enum DocumentMessage {
|
||||
vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
},
|
||||
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
|
||||
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
||||
// `UpdateFillAttributes`/`UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so they never reach that serialization point.
|
||||
#[serde(skip)]
|
||||
UpdateFillAttributes {
|
||||
fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::consts::{
|
||||
ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING,
|
||||
VIEWPORT_ROTATE_SNAP_INTERVAL,
|
||||
};
|
||||
use crate::messages::frontend::utility_types::FileFilter;
|
||||
use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||
@@ -34,19 +35,19 @@ use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::application_io::wgpu_available;
|
||||
use graph_craft::descriptor;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||
use graphene_std::graphic::is_paint_present;
|
||||
use graph_craft::list;
|
||||
use graphene_std::Cover;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::graphic_types;
|
||||
use graphene_std::vector::misc::dvec2_to_point;
|
||||
use graphene_std::vector::style::RenderMode;
|
||||
use graphene_std::vector::{PointId, graphic_types};
|
||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
||||
use kurbo::{Affine, BezPath, Line, PathSeg, Shape};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -103,6 +104,7 @@ pub struct DocumentMessageHandler {
|
||||
pub properties_panel_collapsed_sections: Vec<NodeId>,
|
||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||
#[serde(skip_deserializing, default)]
|
||||
pub commit_hash: String,
|
||||
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
|
||||
pub document_ptz: PTZ,
|
||||
@@ -125,7 +127,7 @@ pub struct DocumentMessageHandler {
|
||||
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
||||
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
||||
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)>,
|
||||
|
||||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
@@ -298,7 +300,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
graph_operation_message_handler.process_message(message, responses, context);
|
||||
}
|
||||
DocumentMessage::Resource(message) => {
|
||||
let context = ResourceMessageContext { document_id, fonts };
|
||||
let context = ResourceMessageContext { document_id, fonts, resource_storage };
|
||||
self.resources.process_message(message, responses, context);
|
||||
}
|
||||
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
||||
@@ -915,7 +917,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
}
|
||||
responses.add(SelectToolMessage::Abort);
|
||||
responses.add(DocumentMessage::DocumentHistoryForward);
|
||||
responses.add(ToolMessage::Redo);
|
||||
|
||||
// The Pen tool advances its in-progress drawing to match the re-applied document once the graph has re-evaluated
|
||||
if *current_tool == ToolType::Pen {
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::Redo.into()],
|
||||
});
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
@@ -1065,7 +1074,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
(true, Some(storage)) => storage
|
||||
.export_to_bytes(
|
||||
document_format::ExportFormat::Xz,
|
||||
document_format::ExportOptions::default(),
|
||||
document_format::ExportOptions {
|
||||
include_history: false,
|
||||
..Default::default()
|
||||
},
|
||||
export_load_handle.as_ref(),
|
||||
Some(&legacy_document),
|
||||
)
|
||||
@@ -1085,6 +1097,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
name,
|
||||
path,
|
||||
folder,
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite Document".into(),
|
||||
extensions: vec![extension.into()],
|
||||
}],
|
||||
content: content.into(),
|
||||
})
|
||||
});
|
||||
@@ -1502,9 +1518,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_vector_data(layer_vector_data);
|
||||
}
|
||||
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
|
||||
DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_fill_attributes = fill_attributes
|
||||
let layer_appearance_attributes = appearance_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
@@ -1514,30 +1530,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_fill_attributes(layer_fill_attributes);
|
||||
}
|
||||
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_stroke_attributes = stroke_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||
(layer, attrs)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
|
||||
self.network_interface.update_appearance_attributes(layer_appearance_attributes);
|
||||
}
|
||||
DocumentMessage::Undo => {
|
||||
if self.network_interface.transaction_status() != TransactionStatus::Finished {
|
||||
return;
|
||||
}
|
||||
responses.add(ToolMessage::PreUndo);
|
||||
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||
|
||||
// The Pen tool keeps drawing across an undo, so instead of aborting beforehand it rewinds to match the reverted document once the graph has re-evaluated
|
||||
if *current_tool == ToolType::Pen {
|
||||
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::Undo.into()],
|
||||
});
|
||||
} else {
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(ToolMessage::Undo);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
DocumentMessage::UngroupSelectedLayers => {
|
||||
@@ -1712,7 +1723,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
}
|
||||
DocumentMessage::ZoomCanvasToFitAll => {
|
||||
let bounds = if self.graph_view_overlay_open {
|
||||
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path).cloned()
|
||||
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path)
|
||||
} else {
|
||||
self.network_interface.document_bounds_document_space(true)
|
||||
};
|
||||
@@ -1844,16 +1855,16 @@ impl DocumentMessageHandler {
|
||||
self.intersect_quad(viewport_quad, viewport).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space subpath
|
||||
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||
/// Runs an intersection test with all layers and a viewport space polygon path
|
||||
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
||||
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
|
||||
|
||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Polygon(viewport_polygon))
|
||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Path(viewport_polygon))
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space subpath; ignoring artboards
|
||||
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||
/// Runs an intersection test with all layers and a viewport space polygon path; ignoring artboards
|
||||
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
||||
self.intersect_polygon(viewport_polygon, viewport)
|
||||
.filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
}
|
||||
@@ -1881,29 +1892,24 @@ impl DocumentMessageHandler {
|
||||
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
|
||||
}
|
||||
|
||||
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: Subpath<PointId>) -> bool {
|
||||
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: BezPath) -> bool {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
||||
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
|
||||
|
||||
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
|
||||
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
|
||||
|
||||
layer_click_targets.is_some_and(|targets| {
|
||||
targets.iter().all(|target| match target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
let mut subpath = subpath.clone();
|
||||
subpath.apply_transform(layer_transform);
|
||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
||||
ClickTargetType::Path(path) => {
|
||||
let mut path = path.clone();
|
||||
path.apply_affine(Affine::new(layer_transform.to_cols_array()));
|
||||
bezpath_is_inside_bezpath(&path, &viewport_polygon, None, None)
|
||||
}
|
||||
ClickTargetType::CompoundPath(subpaths) => subpaths.iter().all(|subpath| {
|
||||
let mut subpath = subpath.clone();
|
||||
subpath.apply_transform(layer_transform);
|
||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
||||
}),
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
let mut point = *point;
|
||||
point.apply_transform(layer_transform);
|
||||
viewport_polygon.contains_point(point.position)
|
||||
viewport_polygon.contains(dvec2_to_point(point.position))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2414,22 +2420,26 @@ impl DocumentMessageHandler {
|
||||
self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, true, responses);
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
||||
// If there is no history return and don't broadcast SelectionChanged
|
||||
let mut network_interface = self.history.pop_undo()?;
|
||||
|
||||
/// Installs a history snapshot as the active network interface, carrying over the current view state and structure load, and returns the replaced interface.
|
||||
fn install_history_snapshot(&mut self, mut network_interface: NodeNetworkInterface, viewport: &ViewportMessageHandler) -> NodeNetworkInterface {
|
||||
// Set the previous network navigation metadata to the current navigation metadata
|
||||
network_interface.copy_all_navigation_metadata(&self.network_interface);
|
||||
std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types);
|
||||
|
||||
//Update the metadata transform based on document PTZ
|
||||
// Update the metadata transform based on document PTZ
|
||||
let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||
network_interface.set_document_to_viewport_transform(transform);
|
||||
|
||||
// Ensure document structure is loaded so that updating the selected nodes has the correct metadata
|
||||
network_interface.load_structure();
|
||||
|
||||
let previous_network = std::mem::replace(&mut self.network_interface, network_interface);
|
||||
std::mem::replace(&mut self.network_interface, network_interface)
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
||||
// If there is no history return and don't broadcast SelectionChanged
|
||||
let network_interface = self.history.pop_undo()?;
|
||||
let previous_network = self.install_history_snapshot(network_interface, viewport);
|
||||
|
||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
@@ -2454,17 +2464,9 @@ impl DocumentMessageHandler {
|
||||
|
||||
pub fn redo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) -> Option<NodeNetworkInterface> {
|
||||
// If there is no history return and don't broadcast SelectionChanged
|
||||
let mut network_interface = self.history.pop_redo()?;
|
||||
let network_interface = self.history.pop_redo()?;
|
||||
let previous_network = self.install_history_snapshot(network_interface, viewport);
|
||||
|
||||
// Set the previous network navigation metadata to the current navigation metadata
|
||||
network_interface.copy_all_navigation_metadata(&self.network_interface);
|
||||
std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types);
|
||||
|
||||
//Update the metadata transform based on document PTZ
|
||||
let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
|
||||
network_interface.set_document_to_viewport_transform(transform);
|
||||
|
||||
let previous_network = std::mem::replace(&mut self.network_interface, network_interface);
|
||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||
@@ -2617,7 +2619,11 @@ impl DocumentMessageHandler {
|
||||
|
||||
// If there's already a boolean operation on the selected layer, update it with the new operation
|
||||
if let (Some(upstream_boolean_op), Some(only_selected_layer)) = (upstream_boolean_op, only_selected_layer) {
|
||||
network_interface.set_input(&InputConnector::node(upstream_boolean_op, 1), NodeInput::value(TaggedValue::BooleanOperation(operation), false), &[]);
|
||||
network_interface.set_input(
|
||||
&InputConnector::node(upstream_boolean_op, graphene_std::path_bool_nodes::boolean_operation::OperationInput),
|
||||
NodeInput::value(TaggedValue::BooleanOperation(operation), false),
|
||||
&[],
|
||||
);
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
@@ -2748,7 +2754,7 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
/// For each selected layer, splits its fill and stroke into two stacked layers connected
|
||||
/// to a shared `Solidify Stroke` node via two `Index Elements` nodes (indices 0 and 1).
|
||||
/// to a shared `Solidify Stroke` node via two `Item at Index` nodes (indices 0 and 1).
|
||||
/// Layers with only a stroke get just a `Solidify Stroke` added.
|
||||
/// Layers with only a fill, or neither, are left untouched.
|
||||
fn handle_expand_fill_stroke_on_selected_layers(&mut self, responses: &mut VecDeque<Message>) {
|
||||
@@ -2758,25 +2764,24 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
||||
let index_elements_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::index_elements::IDENTIFIER).expect("Index Elements node should exist");
|
||||
let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::item_at_index::IDENTIFIER).expect("Item at Index node should exist");
|
||||
|
||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||
|
||||
for layer in selected_layers {
|
||||
let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else {
|
||||
if !self.network_interface.document_metadata().layer_vector_data.contains_key(&layer) {
|
||||
resulting_layers.push(layer.to_node());
|
||||
continue;
|
||||
};
|
||||
let stroke = vector_data.stroke.as_ref();
|
||||
}
|
||||
|
||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
||||
let appearance = self.network_interface.document_metadata().layer_appearance_attributes.get(&layer);
|
||||
|
||||
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
|
||||
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
|
||||
// So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth.
|
||||
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
|
||||
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
|
||||
let has_fill = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
|
||||
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
||||
let has_stroke = appearance.is_some_and(|appearance| {
|
||||
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
||||
&& appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_guaranteed_fully_transparent())
|
||||
});
|
||||
|
||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
||||
if !has_stroke {
|
||||
@@ -2791,7 +2796,7 @@ impl DocumentMessageHandler {
|
||||
if has_fill && has_stroke {
|
||||
let (existing_index, new_index) = (0_f64, 1_f64);
|
||||
|
||||
let existing_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
||||
let existing_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
||||
let existing_index_id = NodeId::new();
|
||||
self.network_interface.insert_node(existing_index_id, existing_index_template, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false);
|
||||
@@ -2813,12 +2818,13 @@ impl DocumentMessageHandler {
|
||||
self.network_interface.set_display_name(&new_layer_id, original_name, &[]);
|
||||
}
|
||||
|
||||
let new_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
||||
let new_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
||||
let new_index_id = NodeId::new();
|
||||
self.network_interface.insert_node(new_index_id, new_index_template, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false);
|
||||
|
||||
self.network_interface.create_wire(&OutputConnector::node(solidify_id, 0), &InputConnector::node(new_index_id, 0), &[]);
|
||||
self.network_interface
|
||||
.create_wire(&OutputConnector::primary_output(solidify_id), &InputConnector::primary_input(new_index_id), &[]);
|
||||
|
||||
resulting_layers.push(layer.to_node());
|
||||
resulting_layers.push(new_layer.to_node());
|
||||
@@ -3408,6 +3414,11 @@ impl DocumentMessageHandler {
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
|
||||
|
||||
// A layer whose chain cannot carry blending nodes has nowhere to put the value, so it disqualifies the whole selection
|
||||
let all_layers_support_blending = selected_nodes
|
||||
.selected_layers_except_artboards(&self.network_interface)
|
||||
.all(|layer| self.network_interface.layer_hosts_blending_nodes(&layer.to_node(), &[]));
|
||||
|
||||
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
|
||||
let mut blending_options = selected_layers_except_artboards.map(|layer| {
|
||||
(
|
||||
@@ -3419,8 +3430,8 @@ impl DocumentMessageHandler {
|
||||
let first_blending_options = blending_options.next();
|
||||
let result_blending_options = blending_options;
|
||||
|
||||
// If there are no selected layers, disable the opacity and blend mode widgets.
|
||||
let disabled = first_blending_options.is_none();
|
||||
// If there are no selected layers, or any of them cannot host the nodes, disable the opacity and blend mode widgets.
|
||||
let disabled = first_blending_options.is_none() || !all_layers_support_blending;
|
||||
|
||||
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
|
||||
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
|
||||
@@ -3583,7 +3594,7 @@ impl DocumentMessageHandler {
|
||||
// Showing only compatible types for the layer based on the output type of the node upstream from its horizontal input
|
||||
let compatible_type = selected_layer.and_then(|layer| {
|
||||
self.network_interface
|
||||
.upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[])
|
||||
.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[])
|
||||
.and_then(|upstream_output| self.network_interface.output_type(&upstream_output, &[]).add_node_string())
|
||||
});
|
||||
|
||||
@@ -3789,7 +3800,7 @@ impl DocumentMessageHandler {
|
||||
/// Create a network interface with a single export
|
||||
fn default_document_network_interface() -> NodeNetworkInterface {
|
||||
let mut network_interface = NodeNetworkInterface::default();
|
||||
network_interface.add_export(TaggedValue::TypeDefault(descriptor!(graphene_std::list::List<graphene_std::Artboard>)), -1, "", &[]);
|
||||
network_interface.add_export(TaggedValue::TypeDefault(list!(graphene_std::Artboard)), -1, "", &[]);
|
||||
network_interface
|
||||
}
|
||||
|
||||
@@ -3799,7 +3810,6 @@ enum XRayTarget {
|
||||
Point(DVec2),
|
||||
Quad(Quad),
|
||||
Path(BezPath),
|
||||
Polygon(Subpath<PointId>),
|
||||
}
|
||||
|
||||
/// The result for the [`ClickXRayIter`] on the layer
|
||||
@@ -3822,13 +3832,7 @@ fn quad_to_kurbo(quad: Quad) -> BezPath {
|
||||
|
||||
fn click_targets_to_kurbo<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> BezPath {
|
||||
let segments = click_targets
|
||||
.filter_map(|target| {
|
||||
if let ClickTargetType::Subpath(subpath) = target.target_type() {
|
||||
Some(subpath.iter())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|target| if let ClickTargetType::Path(path) = target.target_type() { Some(path.segments()) } else { None })
|
||||
.flatten()
|
||||
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
|
||||
BezPath::from_path_segments(segments)
|
||||
@@ -3899,10 +3903,6 @@ impl<'a> ClickXRayIter<'a> {
|
||||
}
|
||||
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_kurbo(*quad), transform),
|
||||
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
|
||||
XRayTarget::Polygon(polygon) => {
|
||||
let polygon = BezPath::from_path_segments(polygon.iter_closed());
|
||||
self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4007,7 +4007,7 @@ mod document_message_handler_tests {
|
||||
#[test]
|
||||
fn pending_gradient_bakes_round_trip_through_serialization() {
|
||||
let document = DocumentMessageHandler {
|
||||
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::Gradient::default())],
|
||||
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::LegacyGradient::default())],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -4318,4 +4318,40 @@ mod document_message_handler_tests {
|
||||
Dist: {distance} (should be < 1)"
|
||||
);
|
||||
}
|
||||
|
||||
// Grouping choreography transiently disconnects the stack wire, and the stored default for that connector
|
||||
// must stay an empty list rather than any value which materializes as a one-element phantom in the stack
|
||||
#[tokio::test]
|
||||
async fn grouping_adds_no_phantom_element_to_the_stack() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
editor
|
||||
.handle_message(DocumentMessage::GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType::Layer,
|
||||
})
|
||||
.await;
|
||||
|
||||
let instrumented = editor.eval_graph().await.unwrap();
|
||||
|
||||
// The emptiness guards keep these assertions honest: a wrong `Output` type on `grab_all_input` yields no records at all, which would otherwise pass without checking anything
|
||||
let base_lengths: Vec<usize> = instrumented
|
||||
.grab_all_input::<graphene_std::graphic::extend::BaseInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.map(|base| base.len())
|
||||
.collect();
|
||||
assert!(!base_lengths.is_empty(), "Instrumentation should have recorded at least one stack base");
|
||||
assert!(base_lengths.iter().all(|&len| len == 0), "Every stack base should be empty, found lengths {base_lengths:?}");
|
||||
|
||||
let news: Vec<graphene_std::list::List<graphene_std::Graphic>> = instrumented
|
||||
.grab_all_input::<graphene_std::graphic::extend::NewInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.collect();
|
||||
assert!(!news.is_empty(), "Instrumentation should have recorded at least one stacked element list");
|
||||
let phantom_count = news
|
||||
.iter()
|
||||
.flat_map(|new| new.iter_element_values())
|
||||
.filter(|graphic| matches!(graphic, graphene_std::Graphic::None(_)))
|
||||
.count();
|
||||
assert_eq!(phantom_count, 0, "No stacked element should be a phantom None graphic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
||||
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||
use graphene_std::vector::{Gradient, VectorModificationType};
|
||||
|
||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
@@ -20,11 +18,16 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
color: Option<Color>,
|
||||
},
|
||||
ColorValueSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
color: Color,
|
||||
},
|
||||
FillGradientSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient: GradientStops,
|
||||
gradient_type: GradientType,
|
||||
spread_method: GradientSpreadMethod,
|
||||
#[serde(skip)]
|
||||
gradient: Gradient,
|
||||
gradient_form: GradientForm,
|
||||
gradient_settings: GradientSettings,
|
||||
transform: DAffine2,
|
||||
},
|
||||
BlendingFillSet {
|
||||
@@ -33,19 +36,44 @@ pub enum GraphOperationMessage {
|
||||
},
|
||||
GradientStopsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
stops: GradientStops,
|
||||
#[serde(skip)]
|
||||
stops: Gradient,
|
||||
},
|
||||
GradientPositionsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
positions: Vec<f64>,
|
||||
},
|
||||
GradientMidpointsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
midpoints: Vec<f64>,
|
||||
},
|
||||
GradientTransformSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
transform: DAffine2,
|
||||
},
|
||||
GradientTypeSet {
|
||||
GradientFormSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_type: GradientType,
|
||||
gradient_form: GradientForm,
|
||||
},
|
||||
GradientSpreadMethodSet {
|
||||
GradientSpreadSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
spread_method: GradientSpreadMethod,
|
||||
gradient_spread: GradientSpread,
|
||||
},
|
||||
GradientSpaceSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_space: GradientSpace,
|
||||
},
|
||||
GradientCyclicSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_cyclic: bool,
|
||||
},
|
||||
GradientHueDirectionSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_hue_direction: GradientHueDirection,
|
||||
},
|
||||
GradientInterpolationSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_interpolation: GradientInterpolation,
|
||||
},
|
||||
OpacitySet {
|
||||
layer: LayerNodeIdentifier,
|
||||
@@ -63,6 +91,10 @@ pub enum GraphOperationMessage {
|
||||
color: Option<Color>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
StrokeOrderSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
paint_order: PaintOrder,
|
||||
},
|
||||
TransformChange {
|
||||
layer: LayerNodeIdentifier,
|
||||
transform: DAffine2,
|
||||
@@ -79,9 +111,23 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
modification_type: VectorModificationType,
|
||||
},
|
||||
Brush {
|
||||
NewBrushGroupLayer {
|
||||
id: NodeId,
|
||||
strokes_node_id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
color: Color,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
},
|
||||
NewBrushStrokesNode {
|
||||
layer: LayerNodeIdentifier,
|
||||
strokes: Vec<BrushStroke>,
|
||||
strokes_node_id: NodeId,
|
||||
color: Color,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
},
|
||||
SetUpstreamToChain {
|
||||
layer: LayerNodeIdentifier,
|
||||
@@ -128,12 +174,6 @@ pub enum GraphOperationMessage {
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
NewVectorLayer {
|
||||
id: NodeId,
|
||||
subpaths: Vec<Subpath<PointId>>,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
NewTextLayer {
|
||||
id: NodeId,
|
||||
text: String,
|
||||
|
||||
+495
-93
@@ -1,19 +1,20 @@
|
||||
use super::transform_utils;
|
||||
use super::utility_types::ModifyInputsContext;
|
||||
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
|
||||
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::descriptor;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::list::List;
|
||||
use graph_craft::list;
|
||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::{Artboard, Color};
|
||||
|
||||
#[derive(ExtractField)]
|
||||
@@ -39,15 +40,20 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.fill_color_set(color);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::ColorValueSet { layer, color } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.color_value_set(color);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::FillGradientSet {
|
||||
layer,
|
||||
gradient,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
gradient_form,
|
||||
gradient_settings,
|
||||
transform,
|
||||
} => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_settings, transform);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||
@@ -60,19 +66,49 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.gradient_stops_set(stops);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientPositionsSet { layer, positions } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_positions_set(positions);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientMidpointsSet { layer, midpoints } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_midpoints_set(midpoints);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientTransformSet { layer, transform } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_transform_set(transform);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientTypeSet { layer, gradient_type } => {
|
||||
GraphOperationMessage::GradientFormSet { layer, gradient_form } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_type_set(gradient_type);
|
||||
modify_inputs.gradient_form_set(gradient_form);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method } => {
|
||||
GraphOperationMessage::GradientSpreadSet { layer, gradient_spread } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_spread_method_set(spread_method);
|
||||
modify_inputs.gradient_spread_set(gradient_spread);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientSpaceSet { layer, gradient_space } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_space_set(gradient_space);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientCyclicSet { layer, gradient_cyclic } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_cyclic_set(gradient_cyclic);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_interpolation_set(gradient_interpolation);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||
@@ -96,6 +132,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.stroke_set(color, stroke);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::StrokeOrderSet { layer, paint_order } => {
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let Some(stroke_node_id) = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, network_interface) else {
|
||||
return;
|
||||
};
|
||||
if set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order) {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::TransformChange {
|
||||
layer,
|
||||
transform,
|
||||
@@ -126,13 +173,38 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.vector_modify(modification_type);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::Brush { layer, strokes } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.brush_modify(strokes);
|
||||
}
|
||||
GraphOperationMessage::NewBrushGroupLayer {
|
||||
id,
|
||||
strokes_node_id,
|
||||
parent,
|
||||
insert_index,
|
||||
color,
|
||||
diameter,
|
||||
hardness,
|
||||
flow,
|
||||
} => {
|
||||
let layer = ModifyInputsContext::new(network_interface, responses).create_layer(id);
|
||||
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
|
||||
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewBrushStrokesNode {
|
||||
layer,
|
||||
strokes_node_id,
|
||||
color,
|
||||
diameter,
|
||||
hardness,
|
||||
flow,
|
||||
} => {
|
||||
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||
|
||||
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::SetUpstreamToChain { layer } => {
|
||||
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(layer.to_node(), 1), &[]) else {
|
||||
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[]) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -180,16 +252,16 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
}
|
||||
|
||||
// Set the bottom input of the artboard back to artboard
|
||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
||||
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||
network_interface.set_input(&InputConnector::primary_input(artboard_layer.to_node()), bottom_input, &[]);
|
||||
} else {
|
||||
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
|
||||
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
|
||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
|
||||
network_interface.disconnect_input(&InputConnector::primary_input(artboard_layer.to_node()), &[]);
|
||||
network_interface.set_input(&InputConnector::layer_secondary_input(artboard_layer.to_node()), primary_input, &[]);
|
||||
|
||||
// Set the bottom input of the artboard back to artboard
|
||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
||||
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||
network_interface.set_input(&InputConnector::primary_input(artboard_layer.to_node()), bottom_input, &[]);
|
||||
}
|
||||
}
|
||||
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
|
||||
@@ -212,11 +284,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
|
||||
// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided
|
||||
let (chain_node_id, layer_alias, path_alias) = if let Some(count) = blend_count {
|
||||
(modify_inputs.insert_blend_data(layer, count as f64), "Blend", "Blend Path")
|
||||
// Insert the main chain node (Blend or Morph) depending on whether a blend count is provided, referencing
|
||||
// its control path input by the Blend template's named position or the Morph proto node's parameter symbol
|
||||
let (path_input_connector, layer_alias, path_alias) = if let Some(count) = blend_count {
|
||||
let blend_node_id = modify_inputs.insert_blend_data(layer, count as f64);
|
||||
(InputConnector::node_at_index(blend_node_id, BLEND_PATH_INPUT_INDEX), "Blend", "Blend Path")
|
||||
} else {
|
||||
(modify_inputs.insert_morph_data(layer), "Morph", "Morph Path")
|
||||
let morph_node_id = modify_inputs.insert_morph_data(layer);
|
||||
(InputConnector::node(morph_node_id, graphene_std::vector::morph::PathInput), "Morph", "Morph Path")
|
||||
};
|
||||
|
||||
// Create the control path layer (Path → Auto-Tangents → Origins to Polyline)
|
||||
@@ -226,9 +301,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
network_interface.move_layer_to_stack(control_path_layer, parent, insert_index, &[]);
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index + 1, &[]);
|
||||
|
||||
// Connect the Path node's output to the chain node's path parameter input (input 4 for both Morph and Blend).
|
||||
// Connect the Path node's output to the chain node's control path input.
|
||||
// Done after move_layer_to_stack so chain nodes have correct positions when converted to absolute.
|
||||
network_interface.set_input(&InputConnector::node(chain_node_id, 4), NodeInput::node(path_node_id, 0), &[]);
|
||||
network_interface.set_input(&path_input_connector, NodeInput::node(path_node_id, 0), &[]);
|
||||
|
||||
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
||||
node_id: id,
|
||||
@@ -246,29 +321,29 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
control_path_id,
|
||||
} => {
|
||||
// Find the chain node (Blend or Morph, first in chain of the layer)
|
||||
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::node(interpolation_layer_id, 1), &[]) else {
|
||||
let Some(OutputConnector::Node { node_id: chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(interpolation_layer_id), &[]) else {
|
||||
log::error!("Could not find chain node for layer {interpolation_layer_id}");
|
||||
return;
|
||||
};
|
||||
|
||||
// Get what feeds into the chain node's primary input (the children stack)
|
||||
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::node(chain_node, 0), &[]) else {
|
||||
let Some(OutputConnector::Node { node_id: children_id, output_index }) = network_interface.upstream_output_connector(&InputConnector::primary_input(chain_node), &[]) else {
|
||||
log::error!("Could not find children stack feeding chain node {chain_node}");
|
||||
return;
|
||||
};
|
||||
|
||||
// Find the deepest node in the control path layer's chain (Origins to Polyline)
|
||||
let mut deepest_chain_node = None;
|
||||
let mut current_connector = InputConnector::node(control_path_id, 1);
|
||||
let mut current_connector = InputConnector::layer_secondary_input(control_path_id);
|
||||
while let Some(OutputConnector::Node { node_id, .. }) = network_interface.upstream_output_connector(¤t_connector, &[]) {
|
||||
deepest_chain_node = Some(node_id);
|
||||
current_connector = InputConnector::node(node_id, 0);
|
||||
current_connector = InputConnector::primary_input(node_id);
|
||||
}
|
||||
|
||||
// Connect children to the deepest chain node's input 0 (or the layer's input 1 if no chain)
|
||||
let target_connector = match deepest_chain_node {
|
||||
Some(node_id) => InputConnector::node(node_id, 0),
|
||||
None => InputConnector::node(control_path_id, 1),
|
||||
Some(node_id) => InputConnector::primary_input(node_id),
|
||||
None => InputConnector::layer_secondary_input(control_path_id),
|
||||
};
|
||||
network_interface.set_input(&target_connector, NodeInput::node(children_id, output_index), &[]);
|
||||
|
||||
@@ -299,7 +374,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids });
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(layer.to_node(), 1),
|
||||
input_connector: InputConnector::layer_secondary_input(layer.to_node()),
|
||||
input: NodeInput::node(first_new_node_id, 0),
|
||||
});
|
||||
}
|
||||
@@ -310,14 +385,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
GraphOperationMessage::NewColorFillLayer { node_id, color, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
let layer = modify_inputs.create_layer(node_id);
|
||||
modify_inputs.insert_color_value(color, layer);
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
modify_inputs.insert_vector(subpaths, layer, true, true, true);
|
||||
modify_inputs.insert_color_value(color, layer, InputConnector::layer_secondary_input(layer.to_node()));
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
@@ -368,7 +436,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
input_node: NodeInput::node(document_node.inputs[1].as_node().unwrap_or_default(), 0),
|
||||
output_nodes: network_interface
|
||||
.outward_wires(&[])
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(artboard.to_node(), 0)))
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::primary_output(artboard.to_node())))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
merge_node: node_id,
|
||||
@@ -394,7 +462,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
for artboard in &artboard_data {
|
||||
// Modify downstream connections
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(artboard.1.merge_node, 1),
|
||||
input_connector: InputConnector::layer_secondary_input(artboard.1.merge_node),
|
||||
input: NodeInput::node(artboard.1.input_node.as_node().unwrap_or_default(), 0),
|
||||
});
|
||||
|
||||
@@ -402,7 +470,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
for outward_wire in &artboard.1.output_nodes {
|
||||
let input = NodeInput::node(artboard_data[artboard.0].merge_node, 0);
|
||||
let input_connector = match artboard_data.get(&outward_wire.node_id().unwrap_or_default()) {
|
||||
Some(artboard_info) => InputConnector::node(artboard_info.merge_node, outward_wire.input_index()),
|
||||
Some(artboard_info) => InputConnector::node_at_index(artboard_info.merge_node, outward_wire.input_index()),
|
||||
_ => *outward_wire,
|
||||
};
|
||||
responses.add(NodeGraphMessage::SetInput { input_connector, input });
|
||||
@@ -462,18 +530,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
};
|
||||
placement_transform.translation = placement_transform.translation.round();
|
||||
|
||||
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
|
||||
let gradient_info = SvgGradientInfo {
|
||||
graphite_stops: extract_graphite_gradient_stops(&svg),
|
||||
spaces: extract_gradient_spaces(&svg),
|
||||
};
|
||||
|
||||
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
|
||||
// The placement offset is then applied once to the root group layer below.
|
||||
import_usvg_node(
|
||||
&mut modify_inputs,
|
||||
&usvg::Node::Group(Box::new(tree.root().clone())),
|
||||
id,
|
||||
parent,
|
||||
insert_index,
|
||||
&graphite_gradient_stops,
|
||||
);
|
||||
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), id, parent, insert_index, &gradient_info);
|
||||
|
||||
// After import, `layer_node` is set to the root group. Apply the placement transform to it
|
||||
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
|
||||
@@ -505,11 +569,136 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
||||
|
||||
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
||||
|
||||
/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops.
|
||||
struct SvgGradientInfo {
|
||||
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
|
||||
graphite_stops: HashMap<String, Gradient>,
|
||||
/// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
|
||||
spaces: HashMap<String, GradientSpace>,
|
||||
}
|
||||
|
||||
/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's
|
||||
/// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the
|
||||
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
|
||||
fn extract_gradient_spaces(svg: &str) -> HashMap<String, GradientSpace> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
|
||||
if !svg.contains("color-interpolation") {
|
||||
return result;
|
||||
}
|
||||
|
||||
let doc = match usvg::roxmltree::Document::parse(svg) {
|
||||
Ok(doc) => doc,
|
||||
Err(_) => return result,
|
||||
};
|
||||
|
||||
// The document's `<style>` blocks apply to every element, so parse them once up front
|
||||
let mut stylesheet = simplecss::StyleSheet::new();
|
||||
for style_element in doc.descendants().filter(|node| node.tag_name().name() == "style") {
|
||||
if !matches!(style_element.attribute("type"), None | Some("") | Some("text/css")) {
|
||||
continue;
|
||||
}
|
||||
for text in style_element.children().filter(|child| child.is_text()).filter_map(|child| child.text()) {
|
||||
stylesheet.parse_more(text);
|
||||
}
|
||||
}
|
||||
|
||||
for node in doc.descendants() {
|
||||
match node.tag_name().name() {
|
||||
"linearGradient" | "radialGradient" => {}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
if let Some(gradient_id) = node.attribute("id")
|
||||
&& let Some(gradient_space) = resolve_color_interpolation(node, &stylesheet)
|
||||
{
|
||||
result.insert(gradient_id.to_string(), gradient_space);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each
|
||||
/// element's own winning declaration per [`declared_color_interpolation`]'s cascade order.
|
||||
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientSpace> {
|
||||
let mut next = Some(element);
|
||||
|
||||
while let Some(element) = next {
|
||||
match declared_color_interpolation(element, stylesheet) {
|
||||
Some("linearRGB") => return Some(GradientSpace::RgbLinear),
|
||||
// `inherit` defers to the ancestors like an undeclared element
|
||||
Some("inherit") | None => {}
|
||||
Some(_) => return Some(GradientSpace::RgbGamma),
|
||||
}
|
||||
|
||||
next = element.parent_element();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// The winning `color-interpolation` declaration on a single element per the CSS cascade: `!important` declarations
|
||||
/// beat normal ones, the inline `style` beats the `<style>` rules (already specificity-sorted, so their last match
|
||||
/// wins), and the presentation attribute yields to them all. Later declarations win priority ties.
|
||||
fn declared_color_interpolation<'a>(element: usvg::roxmltree::Node<'a, '_>, stylesheet: &simplecss::StyleSheet<'a>) -> Option<&'a str> {
|
||||
let mut winner: Option<(u8, &'a str)> = None;
|
||||
let mut consider = |priority: u8, value: &'a str| {
|
||||
if winner.is_none_or(|(existing, _)| priority >= existing) {
|
||||
winner = Some((priority, value));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(value) = element.attribute("color-interpolation") {
|
||||
consider(0, value.trim());
|
||||
}
|
||||
|
||||
for rule in stylesheet.rules.iter().filter(|rule| rule.selector.matches(&CssElement(element))) {
|
||||
for declaration in rule.declarations.iter().filter(|declaration| declaration.name == "color-interpolation") {
|
||||
consider(if declaration.important { 3 } else { 1 }, declaration.value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(style) = element.attribute("style") {
|
||||
for declaration in simplecss::DeclarationTokenizer::from(style).filter(|declaration| declaration.name == "color-interpolation") {
|
||||
consider(if declaration.important { 4 } else { 2 }, declaration.value);
|
||||
}
|
||||
}
|
||||
|
||||
winner.map(|(_, value)| value)
|
||||
}
|
||||
|
||||
/// Adapts a roxmltree element to simplecss's selector-matching interface.
|
||||
struct CssElement<'a, 'input>(usvg::roxmltree::Node<'a, 'input>);
|
||||
|
||||
impl simplecss::Element for CssElement<'_, '_> {
|
||||
fn parent_element(&self) -> Option<Self> {
|
||||
self.0.parent_element().map(CssElement)
|
||||
}
|
||||
|
||||
fn prev_sibling_element(&self) -> Option<Self> {
|
||||
self.0.prev_sibling_element().map(CssElement)
|
||||
}
|
||||
|
||||
fn has_local_name(&self, local_name: &str) -> bool {
|
||||
self.0.tag_name().name() == local_name
|
||||
}
|
||||
|
||||
fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
|
||||
self.0.attribute(local_name).is_some_and(|value| operator.matches(value))
|
||||
}
|
||||
|
||||
fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
|
||||
matches!(class, simplecss::PseudoClass::FirstChild) && self.0.prev_sibling_element().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
|
||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
|
||||
/// Returns a map from gradient element `id` to `Gradient` containing only the real stops.
|
||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, Gradient> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
||||
@@ -555,7 +744,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
|
||||
}
|
||||
|
||||
if has_any_midpoint && !real_stops.is_empty() {
|
||||
result.insert(gradient_id, GradientStops::new(real_stops));
|
||||
result.insert(gradient_id, Gradient::new(real_stops));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +759,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
||||
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
|
||||
Some(Color::from_gamma_srgb_channels(r, g, b, opacity))
|
||||
}
|
||||
|
||||
/// Import a usvg node as the root of an SVG import operation.
|
||||
@@ -579,14 +768,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
||||
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
||||
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
||||
/// the known tree structure.
|
||||
fn import_usvg_node(
|
||||
modify_inputs: &mut ModifyInputsContext,
|
||||
node: &usvg::Node,
|
||||
id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
||||
) {
|
||||
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, gradient_info: &SvgGradientInfo) {
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
|
||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
@@ -606,7 +788,7 @@ fn import_usvg_node(
|
||||
modify_inputs.import = true;
|
||||
|
||||
for child in group.children() {
|
||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map);
|
||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, &mut group_extents_map);
|
||||
child_extents_svg_order.push(extent);
|
||||
}
|
||||
|
||||
@@ -625,7 +807,7 @@ fn import_usvg_node(
|
||||
modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]);
|
||||
}
|
||||
usvg::Node::Path(path) => {
|
||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
||||
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||
}
|
||||
usvg::Node::Image(_image) => {
|
||||
warn!("Skip image");
|
||||
@@ -649,7 +831,7 @@ fn import_usvg_node_inner(
|
||||
id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
||||
gradient_info: &SvgGradientInfo,
|
||||
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
||||
) -> u32 {
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
@@ -660,7 +842,7 @@ fn import_usvg_node_inner(
|
||||
usvg::Node::Group(group) => {
|
||||
let mut child_extents: Vec<u32> = Vec::new();
|
||||
for child in group.children() {
|
||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map);
|
||||
let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, gradient_info, group_extents_map);
|
||||
child_extents.push(extent);
|
||||
}
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
@@ -675,7 +857,7 @@ fn import_usvg_node_inner(
|
||||
total_extent
|
||||
}
|
||||
usvg::Node::Path(path) => {
|
||||
import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops);
|
||||
import_usvg_path(modify_inputs, node, path, layer, gradient_info);
|
||||
0
|
||||
}
|
||||
usvg::Node::Image(_image) => {
|
||||
@@ -691,22 +873,38 @@ fn import_usvg_node_inner(
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, layer: LayerNodeIdentifier, strokes_node_id: NodeId, color: Color, diameter: f64, hardness: f64, flow: f64) {
|
||||
let Some(strokes_node_type) = resolve_proto_node_type(graphene_std::brush::brush_strokes::IDENTIFIER) else {
|
||||
log::error!("Brush strokes node does not exist");
|
||||
return;
|
||||
};
|
||||
let strokes_node = strokes_node_type.node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::Strokes(Vec::new()), false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(color), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(diameter), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(hardness), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(flow), false)),
|
||||
]);
|
||||
network_interface.insert_node(strokes_node_id, strokes_node, &[]);
|
||||
network_interface.set_input(&InputConnector::node_at_index(layer.to_node(), 1), NodeInput::node(strokes_node_id, 0), &[]);
|
||||
}
|
||||
|
||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
let subpaths = convert_usvg_path(path);
|
||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
|
||||
let bezpath = convert_usvg_path(path);
|
||||
|
||||
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||
let node_transform = usvg_transform(node.abs_transform());
|
||||
let has_transform = node_transform != DAffine2::IDENTITY;
|
||||
|
||||
modify_inputs.insert_vector(subpaths, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
|
||||
modify_inputs.insert_vector(bezpath, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
|
||||
|
||||
if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) {
|
||||
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);
|
||||
}
|
||||
|
||||
if let Some(fill) = path.fill() {
|
||||
apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops);
|
||||
apply_usvg_fill(fill, modify_inputs, gradient_info);
|
||||
}
|
||||
if let Some(stroke) = path.stroke() {
|
||||
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
||||
@@ -792,22 +990,21 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
||||
},
|
||||
join_miter_limit: stroke.miterlimit().get() as f64,
|
||||
align: StrokeAlign::Center,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
transform,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMethod {
|
||||
fn convert_gradient_spread(spread_method: usvg::SpreadMethod) -> GradientSpread {
|
||||
match spread_method {
|
||||
usvg::SpreadMethod::Pad => GradientSpreadMethod::Pad,
|
||||
usvg::SpreadMethod::Reflect => GradientSpreadMethod::Reflect,
|
||||
usvg::SpreadMethod::Repeat => GradientSpreadMethod::Repeat,
|
||||
usvg::SpreadMethod::Pad => GradientSpread::Pad,
|
||||
usvg::SpreadMethod::Reflect => GradientSpread::Reflect,
|
||||
usvg::SpreadMethod::Repeat => GradientSpread::Repeat,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, gradient_info: &SvgGradientInfo) {
|
||||
match &fill.paint() {
|
||||
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
||||
usvg::Paint::LinearGradient(linear) => {
|
||||
@@ -817,9 +1014,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
||||
let direction = end - start;
|
||||
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
||||
|
||||
let gradient_type = GradientType::Linear;
|
||||
let gradient_form = GradientForm::Linear;
|
||||
|
||||
let gradient = match graphite_gradient_stops.get(linear.id()) {
|
||||
let gradient = match gradient_info.graphite_stops.get(linear.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
let stops = linear.stops().iter().map(|stop| GradientStop {
|
||||
@@ -827,11 +1024,16 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
||||
midpoint: 0.5,
|
||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
Gradient::new(stops)
|
||||
}
|
||||
};
|
||||
let spread_method = convert_spread_method(linear.spread_method());
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
||||
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
|
||||
let settings = GradientSettings {
|
||||
spread: convert_gradient_spread(linear.spread_method()),
|
||||
space: gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma),
|
||||
..Default::default()
|
||||
};
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
|
||||
}
|
||||
usvg::Paint::RadialGradient(radial) => {
|
||||
let gradient_transform = usvg_transform(radial.transform());
|
||||
@@ -841,9 +1043,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
||||
let direction = end - start;
|
||||
let transform = DAffine2::from_cols(direction, direction.perp(), start);
|
||||
|
||||
let gradient_type = GradientType::Radial;
|
||||
let gradient_form = GradientForm::Radial;
|
||||
|
||||
let gradient = match graphite_gradient_stops.get(radial.id()) {
|
||||
let gradient = match gradient_info.graphite_stops.get(radial.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
let stops = radial.stops().iter().map(|stop| GradientStop {
|
||||
@@ -851,13 +1053,213 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
||||
midpoint: 0.5,
|
||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
Gradient::new(stops)
|
||||
}
|
||||
};
|
||||
let spread_method = convert_spread_method(radial.spread_method());
|
||||
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
|
||||
let settings = GradientSettings {
|
||||
spread: convert_gradient_spread(radial.spread_method()),
|
||||
space: gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma),
|
||||
..Default::default()
|
||||
};
|
||||
modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform);
|
||||
}
|
||||
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stroke_order_set_reorders_the_fill_and_stroke_nodes() {
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_paint_order;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use graphene_std::vector::style::PaintOrder;
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let layer = document.metadata().all_layers().next().unwrap();
|
||||
let paint_order = get_stroke_paint_order(layer, &document.network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeAbove, "a fresh shape should stroke above its fill");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||
layer,
|
||||
paint_order: PaintOrder::StrokeBelow,
|
||||
})
|
||||
.await;
|
||||
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeBelow, "the rewrite should move the stroke downstream of the fill");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||
layer,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
})
|
||||
.await;
|
||||
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeAbove, "the rewrite should move the stroke back upstream of the fill");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_node_inserted_between_the_fill_and_stroke_makes_the_order_swap_inapplicable() {
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::stroke_paint_order_applicable;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||
assert!(
|
||||
stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||
"a fresh shape's adjacent Fill and Stroke pair should be swappable"
|
||||
);
|
||||
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let stroke_node_id = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, &editor.active_document().network_interface).unwrap();
|
||||
|
||||
let node_template = Box::new(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).unwrap().default_node_template());
|
||||
let passthrough_node_id = NodeId::new();
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::InsertNode {
|
||||
node_id: passthrough_node_id,
|
||||
node_template,
|
||||
})
|
||||
.await;
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::InsertNodeBetween {
|
||||
node_id: passthrough_node_id,
|
||||
input_connector: InputConnector::node_at_index(stroke_node_id, 0),
|
||||
insert_node_input_index: 0,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||
"a node between the pair should gray the order radio out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
||||
<defs>
|
||||
<linearGradient id="inherited"/>
|
||||
<linearGradient id="attribute" color-interpolation="sRGB"/>
|
||||
<linearGradient id="styled" color-interpolation="sRGB" style="fill: red; color-interpolation: linearRGB"/>
|
||||
<radialGradient id="auto" color-interpolation="auto"/>
|
||||
</defs>
|
||||
</svg>"##;
|
||||
|
||||
let spaces = extract_gradient_spaces(svg);
|
||||
assert_eq!(spaces.get("inherited"), Some(&GradientSpace::RgbLinear), "an undeclared gradient should inherit from its ancestors");
|
||||
assert_eq!(spaces.get("attribute"), Some(&GradientSpace::RgbGamma), "an sRGB declaration should beat the inherited linearRGB");
|
||||
assert_eq!(spaces.get("styled"), Some(&GradientSpace::RgbLinear), "the inline style should beat the presentation attribute");
|
||||
assert_eq!(
|
||||
spaces.get("auto"),
|
||||
Some(&GradientSpace::RgbGamma),
|
||||
"auto should mean gamma like browsers treat it, not defer to ancestors"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphite_stop_extraction_keeps_real_stops_and_linearizes_their_colors() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art">
|
||||
<defs>
|
||||
<linearGradient id="ramp">
|
||||
<stop stop-color="#000000" graphite:midpoint="0.3" />
|
||||
<stop offset="0.25" stop-color="#404040" />
|
||||
<stop offset="0.5" stop-color="#808080" stop-opacity="0.5" graphite:midpoint="0.5" />
|
||||
<stop offset="1" stop-color="#ffffff" graphite:midpoint="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>"##;
|
||||
|
||||
let stops = extract_graphite_gradient_stops(svg);
|
||||
let gradient = stops.get("ramp").expect("the tagged gradient should be recovered");
|
||||
|
||||
// The untagged stop is baked approximation residue, not authored data
|
||||
assert_eq!(gradient.len(), 3, "only stops tagged with a midpoint should survive");
|
||||
assert_eq!(gradient.positions(false), vec![0., 0.5, 1.]);
|
||||
assert_eq!(gradient.midpoints(), vec![0.3, 0.5, 0.5]);
|
||||
|
||||
// Hex stop bytes are gamma-encoded, so the recovered color must lift them to linear light
|
||||
assert_eq!(gradient.color(1), Some(Color::from_gamma_srgb_channels(128. / 255., 128. / 255., 128. / 255., 0.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_interpolation_reads_style_blocks_with_selector_specificity() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
linearGradient { color-interpolation: linearRGB }
|
||||
.classy { color-interpolation: sRGB }
|
||||
#exact { color-interpolation: linearRGB }
|
||||
</style>
|
||||
<defs>
|
||||
<linearGradient id="from-type-rule"/>
|
||||
<linearGradient id="from-class-rule" class="classy"/>
|
||||
<linearGradient id="exact" class="classy"/>
|
||||
<linearGradient id="inline-beats-rules" class="classy" style="color-interpolation: linearRGB"/>
|
||||
<linearGradient id="rule-beats-attribute" class="classy" color-interpolation="linearRGB"/>
|
||||
</defs>
|
||||
</svg>"##;
|
||||
|
||||
let spaces = extract_gradient_spaces(svg);
|
||||
assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient");
|
||||
assert_eq!(
|
||||
spaces.get("from-class-rule"),
|
||||
Some(&GradientSpace::RgbGamma),
|
||||
"the class rule should outrank the type rule by specificity"
|
||||
);
|
||||
assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule");
|
||||
assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule");
|
||||
assert_eq!(
|
||||
spaces.get("rule-beats-attribute"),
|
||||
Some(&GradientSpace::RgbGamma),
|
||||
"a style block rule should beat the presentation attribute"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_and_important_declarations_resolve_by_cascade_order() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<style>.forced { color-interpolation: linearRGB !important }</style>
|
||||
<linearGradient id="last-declaration-wins" style="color-interpolation: sRGB; color-interpolation: linearRGB"/>
|
||||
<linearGradient id="important-beats-later" style="color-interpolation: linearRGB !important; color-interpolation: sRGB"/>
|
||||
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
|
||||
</svg>"##;
|
||||
|
||||
let spaces = extract_gradient_spaces(svg);
|
||||
assert_eq!(
|
||||
spaces.get("last-declaration-wins"),
|
||||
Some(&GradientSpace::RgbLinear),
|
||||
"the last of repeated inline declarations should win"
|
||||
);
|
||||
assert_eq!(
|
||||
spaces.get("important-beats-later"),
|
||||
Some(&GradientSpace::RgbLinear),
|
||||
"an `!important` declaration should beat a later normal one"
|
||||
);
|
||||
assert_eq!(
|
||||
spaces.get("important-rule-beats-inline"),
|
||||
Some(&GradientSpace::RgbLinear),
|
||||
"an `!important` style block rule should beat the inline style"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_interpolation_yields_nothing_when_never_declared() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
|
||||
|
||||
assert!(
|
||||
extract_gradient_spaces(svg).is_empty(),
|
||||
"gradients without any declaration should fall back to the caller's gamma default"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::Transform;
|
||||
use graphene_std::vector::PointId;
|
||||
|
||||
/// Update the inputs of the transform node to match a new transform
|
||||
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
|
||||
@@ -14,10 +12,26 @@ pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &
|
||||
let rotation = rotation.to_degrees();
|
||||
let skew = DVec2::new(skew.atan().to_degrees(), 0.);
|
||||
|
||||
network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(translation), false), &[]);
|
||||
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(rotation), false), &[]);
|
||||
network_interface.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::DVec2(scale), false), &[]);
|
||||
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(skew), false), &[]);
|
||||
network_interface.set_input(
|
||||
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::TranslationInput),
|
||||
NodeInput::value(TaggedValue::DVec2(translation), false),
|
||||
&[],
|
||||
);
|
||||
network_interface.set_input(
|
||||
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::RotationInput),
|
||||
NodeInput::value(TaggedValue::F64(rotation), false),
|
||||
&[],
|
||||
);
|
||||
network_interface.set_input(
|
||||
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::ScaleInput),
|
||||
NodeInput::value(TaggedValue::DVec2(scale), false),
|
||||
&[],
|
||||
);
|
||||
network_interface.set_input(
|
||||
&InputConnector::node(*node_id, graphene_std::transform_nodes::transform::SkewInput),
|
||||
NodeInput::value(TaggedValue::DVec2(skew), false),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: This should be extracted from the graph at the location of the transform node.
|
||||
@@ -71,32 +85,6 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
|
||||
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
|
||||
}
|
||||
|
||||
/// Expand a bounds to avoid div zero errors
|
||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
/// Returns corners of all subpaths
|
||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box())
|
||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
||||
clamp_bounds(bounds_min, bounds_max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
use super::transform_utils;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{
|
||||
ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||
ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
|
||||
};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete, descriptor};
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||
use graphene_std::vector::{Gradient, GradientRamp, Vector, VectorModification, VectorModificationType};
|
||||
use graphene_std::{Artboard, Color, Graphic};
|
||||
use kurbo::BezPath;
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformIn {
|
||||
@@ -58,72 +61,6 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
Some(document)
|
||||
}
|
||||
|
||||
/// Starts at any folder, or the output, and skips layer nodes based on insert_index. Non layer nodes are always skipped. Returns the post node InputConnector and pre node OutputConnector
|
||||
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
|
||||
/// -----> Post node
|
||||
/// | if insert_index == 0, return (Post node, Some(Layer1))
|
||||
/// -> Layer1
|
||||
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
|
||||
/// -> Layer2
|
||||
/// ↑
|
||||
/// -> NonLayerNode
|
||||
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
|
||||
/// -> Layer3
|
||||
/// if insert_index == 3, return (Layer3, None)
|
||||
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
|
||||
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
|
||||
InputConnector::Export(0)
|
||||
} else {
|
||||
InputConnector::node(parent.to_node(), 1)
|
||||
};
|
||||
// Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack.
|
||||
let mut current_index = 0;
|
||||
|
||||
// Set the post node to the layer node at insert_index
|
||||
loop {
|
||||
if current_index == insert_index {
|
||||
break;
|
||||
}
|
||||
let next_node_in_stack_id = network_interface
|
||||
.input_from_connector(&post_node_input_connector, &[])
|
||||
.and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None });
|
||||
|
||||
if let Some(next_node_in_stack_id) = next_node_in_stack_id {
|
||||
// Only increment index for layer nodes
|
||||
if network_interface.is_layer(next_node_in_stack_id, &[]) {
|
||||
current_index += 1;
|
||||
}
|
||||
// Input as a sibling to the Layer node above
|
||||
post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0);
|
||||
} else {
|
||||
log::error!("Error getting post node: insert_index out of bounds");
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
let layer_input_connector = post_node_input_connector;
|
||||
|
||||
// Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer
|
||||
loop {
|
||||
let pre_node_output_connector = network_interface.upstream_output_connector(&post_node_input_connector, &[]);
|
||||
|
||||
match pre_node_output_connector {
|
||||
Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !network_interface.is_layer(&pre_node_id, &[]) => {
|
||||
// Update post_node_input_connector for the next iteration
|
||||
post_node_input_connector = InputConnector::node(pre_node_id, 0);
|
||||
// Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input
|
||||
let primary_is_exposed = network_interface.input_from_connector(&post_node_input_connector, &[]).is_some_and(|input| input.is_exposed());
|
||||
if !primary_is_exposed {
|
||||
return layer_input_connector;
|
||||
}
|
||||
}
|
||||
_ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer
|
||||
}
|
||||
}
|
||||
|
||||
post_node_input_connector
|
||||
}
|
||||
|
||||
/// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after
|
||||
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
|
||||
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
|
||||
@@ -134,11 +71,11 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
/// Creates an artboard as the primary export for the document network.
|
||||
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
|
||||
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
|
||||
Some(NodeInput::type_default(descriptor!(List<Artboard>), true)),
|
||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
||||
Some(NodeInput::type_default(concrete!(List<Artboard>), true)),
|
||||
Some(NodeInput::type_default(concrete!(List<Graphic>), true)),
|
||||
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
|
||||
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(Some(background)), false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(background), false)),
|
||||
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
|
||||
]);
|
||||
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
||||
@@ -149,7 +86,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
|
||||
.expect("Boolean node does not exist")
|
||||
.node_template_input_override([
|
||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
||||
Some(NodeInput::type_default(concrete!(List<Graphic>), true)),
|
||||
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
||||
]);
|
||||
|
||||
@@ -161,7 +98,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
|
||||
let blend = resolve_network_node_type("Blend")
|
||||
.expect("Blend node does not exist")
|
||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
||||
.node_template_input_override([Some(NodeInput::type_default(concrete!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
||||
|
||||
let blend_id = NodeId::new();
|
||||
self.network_interface.insert_node(blend_id, blend, &[]);
|
||||
@@ -173,7 +110,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
|
||||
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
|
||||
.expect("Morph node does not exist")
|
||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
||||
.node_template_input_override([Some(NodeInput::type_default(concrete!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
||||
|
||||
let morph_id = NodeId::new();
|
||||
self.network_interface.insert_node(morph_id, morph, &[]);
|
||||
@@ -209,9 +146,9 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
path_id
|
||||
}
|
||||
|
||||
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
|
||||
pub fn insert_vector(&mut self, bezpath: BezPath, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
|
||||
// Build a VectorModification that reproduces the geometry (same format the Pen tool uses)
|
||||
let vector = Vector::from_subpaths(subpaths, true);
|
||||
let vector = Vector::from_bezpath(bezpath);
|
||||
let modification = Box::new(VectorModification::create_from_vector(&vector));
|
||||
|
||||
let shape = resolve_network_node_type("Path")
|
||||
@@ -297,14 +234,41 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import);
|
||||
}
|
||||
|
||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier, attachment_input: InputConnector) -> NodeId {
|
||||
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||
.expect("Color Value node does not exist")
|
||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(Some(color)), false))]);
|
||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
||||
|
||||
let color_value_id = NodeId::new();
|
||||
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
||||
self.network_interface.move_node_to_chain_start(&color_value_id, layer, &[], self.import);
|
||||
self.start_paint_chain(&color_value_id, layer, attachment_input);
|
||||
|
||||
color_value_id
|
||||
}
|
||||
|
||||
/// Clear the whole-expanse paint one tool left on a layer so the other can start its own chain there.
|
||||
/// Severing at the attachment detaches the layer from whatever the walk stopped at, which is the only part a node
|
||||
/// the rest of the graph also draws from is subjected to, since such a node is never among those deleted.
|
||||
fn clear_paint_chain(&mut self, paint_chain: &ReplaceablePaintChain) {
|
||||
self.network_interface.disconnect_input(&paint_chain.attachment_input, &[]);
|
||||
|
||||
if !paint_chain.nodes.is_empty() {
|
||||
self.network_interface.delete_nodes(paint_chain.nodes.clone(), false, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire a node that paints the layer's whole expanse into the start of its chain,
|
||||
/// or past the 'Transform' nodes a blank layer already carries so those go on applying to the paint.
|
||||
fn start_paint_chain(&mut self, node_id: &NodeId, layer: LayerNodeIdentifier, attachment_input: InputConnector) {
|
||||
let layer_content_input = InputConnector::layer_secondary_input(layer.to_node());
|
||||
|
||||
if attachment_input == layer_content_input {
|
||||
self.network_interface.move_node_to_chain_start(node_id, layer, &[], self.import);
|
||||
return;
|
||||
}
|
||||
|
||||
self.network_interface.set_input(&attachment_input, NodeInput::node(*node_id, 0), &[]);
|
||||
self.network_interface.set_chain_position(node_id, &[]);
|
||||
}
|
||||
|
||||
pub fn insert_image_data(&mut self, image: Image<Color>, layer: LayerNodeIdentifier) {
|
||||
@@ -383,6 +347,16 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.existing_node_id(&DefinitionIdentifier::ProtoNode(reference), create_if_nonexistent)
|
||||
}
|
||||
|
||||
/// The same as [`Self::existing_proto_node_id`], but yielding `None` on layers whose chain cannot host the node.
|
||||
fn existing_chain_hosted_node_id(&mut self, reference: ProtoNodeIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
|
||||
let output_layer = self.get_output_layer()?;
|
||||
if !self.network_interface.layer_chain_hosts_node(&output_layer.to_node(), &[], &reference) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.existing_proto_node_id(reference, create_if_nonexistent)
|
||||
}
|
||||
|
||||
/// Gets the node id of a document node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
|
||||
fn existing_node_id(&mut self, reference: &DefinitionIdentifier, create_if_nonexistent: bool) -> Option<NodeId> {
|
||||
// Start from the layer node or export
|
||||
@@ -433,17 +407,20 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
return None;
|
||||
};
|
||||
|
||||
// If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`.
|
||||
// Without a secondary input there is no chain to hold the node, so inserting it would strand it at the graph origin
|
||||
self.network_interface.input_from_connector(&InputConnector::layer_secondary_input(output_layer.to_node()), &[])?;
|
||||
|
||||
// If inserting a 'Path' node, insert a 'Combine Paths' node if the type is `Graphic`.
|
||||
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
|
||||
if node_definition.identifier == "Path" {
|
||||
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]);
|
||||
if layer_input_type.compiled_nested_type() == Some(&concrete!(List<Graphic>)) {
|
||||
let Some(flatten_path_definition) = resolve_proto_node_type(graphene_std::vector_nodes::flatten_path::IDENTIFIER) else {
|
||||
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
|
||||
let layer_input_type = self.network_interface.input_type(&InputConnector::layer_secondary_input(output_layer.to_node()), &[]);
|
||||
if layer_input_type.compiled_element_name().as_deref() == Some("Graphic") {
|
||||
let Some(combine_paths_definition) = resolve_proto_node_type(graphene_std::vector_nodes::combine_paths::IDENTIFIER) else {
|
||||
log::error!("Combine Paths does not exist in ModifyInputsContext::existing_node_id");
|
||||
return None;
|
||||
};
|
||||
let node_id = NodeId::new();
|
||||
self.network_interface.insert_node(node_id, flatten_path_definition.default_node_template(), &[]);
|
||||
self.network_interface.insert_node(node_id, combine_paths_definition.default_node_template(), &[]);
|
||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
||||
}
|
||||
}
|
||||
@@ -454,28 +431,35 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
|
||||
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput);
|
||||
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||
// The backup remembers the last solid color, so the red-slash "none" choice leaves it untouched
|
||||
if let Some(color) = color {
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
||||
}
|
||||
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||
}
|
||||
|
||||
pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
|
||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
|
||||
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||
return;
|
||||
};
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
||||
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
|
||||
let ramp = GradientRamp::from(gradient).with_settings(settings);
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
|
||||
|
||||
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX),
|
||||
NodeInput::value(TaggedValue::Gradient(gradient), false),
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput),
|
||||
NodeInput::value(TaggedValue::GradientRamp(ramp), false),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -485,49 +469,48 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&fill_node_id)
|
||||
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
||||
.and_then(|node| node.input(graphene_std::vector::fill::TransformInput))
|
||||
.is_some_and(|input| input.as_value().is_some());
|
||||
if transform_is_value {
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
||||
NodeInput::value(TaggedValue::OptionalDAffine2(Some(transform)), false),
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::HasTransformInput),
|
||||
NodeInput::value(TaggedValue::Bool(true), false),
|
||||
true,
|
||||
);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput),
|
||||
NodeInput::value(TaggedValue::DAffine2(transform), false),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
|
||||
NodeInput::value(TaggedValue::GradientType(gradient_type), false),
|
||||
true,
|
||||
);
|
||||
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
|
||||
NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false),
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientFormInput),
|
||||
NodeInput::value(TaggedValue::GradientForm(gradient_form), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
|
||||
let Some(blend_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput::INDEX);
|
||||
let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
|
||||
}
|
||||
|
||||
pub fn opacity_set(&mut self, opacity: f64) {
|
||||
let Some(opacity_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
|
||||
let Some(opacity_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
// Enable the `has_opacity` checkbox so the value is applied
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput),
|
||||
NodeInput::value(TaggedValue::Bool(true), false),
|
||||
false,
|
||||
);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput::INDEX),
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput),
|
||||
NodeInput::value(TaggedValue::F64(opacity * 100.), false),
|
||||
false,
|
||||
);
|
||||
@@ -536,47 +519,78 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
pub fn opacity_fill_set(&mut self, fill: f64) {
|
||||
// Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
|
||||
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
|
||||
let existing = self.existing_proto_node_id(identifier.clone(), false);
|
||||
let existing = self.existing_chain_hosted_node_id(identifier.clone(), false);
|
||||
let existed = existing.is_some();
|
||||
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
|
||||
let Some(opacity_node_id) = existing.or_else(|| self.existing_chain_hosted_node_id(identifier, true)) else {
|
||||
return;
|
||||
};
|
||||
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
|
||||
if !existed {
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput),
|
||||
NodeInput::value(TaggedValue::Bool(false), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
// Enable the `has_fill` checkbox so the value is applied
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput::INDEX),
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput),
|
||||
NodeInput::value(TaggedValue::Bool(true), false),
|
||||
false,
|
||||
);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput::INDEX),
|
||||
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput),
|
||||
NodeInput::value(TaggedValue::F64(fill * 100.), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the chain's 'Color Value' node, or start a chain with one on an empty layer, painting the layer's whole expanse.
|
||||
pub fn color_value_set(&mut self, color: Color) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
if let Some(node_id) = self.existing_proto_node_id_at(&target_input, graphene_std::math_nodes::color_value::IDENTIFIER, false) {
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||
return;
|
||||
}
|
||||
|
||||
// The 'Color Value' node discards its primary input, so only a blank 'Merge' layer may start a chain with one,
|
||||
// which any whole-expanse paint the other tool left behind is cleared off to become
|
||||
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
self.clear_paint_chain(&paint_chain);
|
||||
|
||||
let color_value_id = self.insert_color_value(color, output_layer, paint_chain.attachment_input);
|
||||
let input_connector = InputConnector::node(color_value_id, graphene_std::math_nodes::color_value::ColorInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
||||
}
|
||||
|
||||
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
||||
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
|
||||
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let starts_layer_chain = target == InputConnector::node(output_layer.to_node(), 1);
|
||||
let starts_layer_chain = target == InputConnector::layer_secondary_input(output_layer.to_node());
|
||||
|
||||
// The Gradient Value node discards its primary input, so starting a chain ahead of existing layer content would drop that content; refuse instead
|
||||
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
||||
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
||||
return;
|
||||
}
|
||||
// The 'Gradient Value' node discards its primary input, so only a blank 'Merge' layer may start a chain
|
||||
// with one, which any whole-expanse paint the other tool left behind is cleared off to become
|
||||
let paint_chain = if starts_layer_chain {
|
||||
let Some(paint_chain) = replaceable_paint_chain(output_layer, self.network_interface) else {
|
||||
log::error!("Refusing to start a gradient chain on anything but a blank 'Merge' layer");
|
||||
return;
|
||||
};
|
||||
self.clear_paint_chain(&paint_chain);
|
||||
|
||||
Some(paint_chain)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
||||
return;
|
||||
@@ -584,9 +598,9 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
let node_id = NodeId::new();
|
||||
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||
|
||||
if starts_layer_chain {
|
||||
if let Some(paint_chain) = paint_chain {
|
||||
// No Fill node: the new node starts the layer's chain
|
||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
||||
self.start_paint_chain(&node_id, output_layer, paint_chain.attachment_input);
|
||||
} else {
|
||||
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
||||
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
||||
@@ -603,8 +617,65 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
|
||||
// Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them
|
||||
let ramp = GradientRamp {
|
||||
stops: (&stops).into(),
|
||||
..self.gradient_value_ramp(gradient_value_id).unwrap_or_default()
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
/// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even
|
||||
/// though that node would otherwise override the stops value's own placement. Never inserts one: the stops value
|
||||
/// carries placement itself, and these setter nodes are user-authored procedural overrides. A wired input is
|
||||
/// procedural authorship too, so it is likewise left untouched.
|
||||
pub fn gradient_positions_set(&mut self, positions: Vec<f64>) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_positions::IDENTIFIER;
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_input = self
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&node_id)
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_positions::PositionsInput));
|
||||
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_positions::PositionsInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(positions), false), false);
|
||||
}
|
||||
|
||||
/// The 'Gradient Midpoints' counterpart of [`Self::gradient_positions_set`], likewise update-only.
|
||||
pub fn gradient_midpoints_set(&mut self, midpoints: Vec<f64>) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_midpoints::IDENTIFIER;
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_input = self
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&node_id)
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_midpoints::MidpointsInput));
|
||||
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_midpoints::MidpointsInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(midpoints), false), false);
|
||||
}
|
||||
|
||||
/// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space.
|
||||
@@ -669,69 +740,131 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
/// Write the gradient type to the last 'Gradient Type' node in the chain, inserting one only when the value differs
|
||||
/// Write the Gradient Form to the last 'Gradient Form' node in the chain, inserting one only when the value differs
|
||||
/// from the default (`Linear`).
|
||||
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
||||
pub fn gradient_form_set(&mut self, gradient_form: GradientForm) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
||||
let create_if_nonexistent = gradient_type != GradientType::default();
|
||||
let identifier = graphene_std::math_nodes::gradient_form::IDENTIFIER;
|
||||
let create_if_nonexistent = gradient_form != GradientForm::default();
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_type::GradientTypeInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientType(gradient_type), false), false);
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_form::GradientFormInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientForm(gradient_form), false), false);
|
||||
}
|
||||
|
||||
/// Write the spread method to the last 'Spread Method' node in the chain, inserting one only when the value differs
|
||||
/// from the default (`Pad`).
|
||||
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
||||
/// Set the spread on the chain's gradient value, which is where the ramp carries it.
|
||||
pub fn gradient_spread_set(&mut self, gradient_spread: GradientSpread) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
||||
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else {
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::spread_method::SpreadMethodInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), false);
|
||||
let ramp = GradientRamp { gradient_spread, ..ramp };
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
/// The ramp currently held by a 'Gradient Value' node.
|
||||
fn gradient_value_ramp(&self, gradient_value_id: NodeId) -> Option<GradientRamp> {
|
||||
let node = self.network_interface.document_network().nodes.get(&gradient_value_id)?;
|
||||
let TaggedValue::GradientRamp(ramp) = node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(ramp.clone())
|
||||
}
|
||||
|
||||
/// Set the space on the chain's gradient value, which is where the ramp carries it. Never touches a
|
||||
/// 'Gradient Space' node: that one is a user-authored procedural override, not something the tools manage.
|
||||
pub fn gradient_space_set(&mut self, gradient_space: GradientSpace) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||
|
||||
let ramp = GradientRamp { gradient_space, ..ramp };
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
/// Set the cyclic wrap flag on the chain's gradient value, which is where the ramp carries it. This holds the existing
|
||||
/// stops in place by reading their positions under the old flag, so batch it before any stops write rather than after one,
|
||||
/// or it would reinterpret incoming stops already authored under the new flag.
|
||||
pub fn gradient_cyclic_set(&mut self, gradient_cyclic: bool) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||
|
||||
let ramp = ramp.with_cyclic(gradient_cyclic);
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
/// Set the interpolation on the chain's gradient value, which is where the ramp carries it.
|
||||
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||
|
||||
let ramp = GradientRamp { gradient_interpolation, ..ramp };
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
|
||||
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
|
||||
|
||||
let ramp = GradientRamp { gradient_hue_direction, ..ramp };
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
|
||||
}
|
||||
|
||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||
let clip = !clip_mode.unwrap_or(false);
|
||||
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
||||
let Some(clip_node_id) = self.existing_chain_hosted_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput::INDEX);
|
||||
let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
||||
}
|
||||
|
||||
pub fn stroke_set(&mut self, color: Option<Color>, stroke: Stroke) {
|
||||
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
|
||||
let Some(stroke_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(color.map_or_else(TaggedValue::no_paint, TaggedValue::Color), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput);
|
||||
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);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
|
||||
// Our stroke node keeps a paint order input rather than encoding the order by Fill/Stroke node adjacency
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashPatternInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
||||
}
|
||||
|
||||
@@ -828,13 +961,6 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
|
||||
let Some(brush_node_id) = self.existing_proto_node_id(graphene_std::brush::brush::brush::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
|
||||
}
|
||||
|
||||
pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {
|
||||
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
|
||||
return;
|
||||
@@ -851,8 +977,16 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
dimensions.y = -dimensions.y;
|
||||
location.y -= dimensions.y;
|
||||
}
|
||||
self.set_input_with_refresh(InputConnector::node(artboard_node_id, 2), NodeInput::value(TaggedValue::DVec2(location), false), false);
|
||||
self.set_input_with_refresh(InputConnector::node(artboard_node_id, 3), NodeInput::value(TaggedValue::DVec2(dimensions), false), false);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node_at_index(artboard_node_id, ARTBOARD_LOCATION_INPUT_INDEX),
|
||||
NodeInput::value(TaggedValue::DVec2(location), false),
|
||||
false,
|
||||
);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node_at_index(artboard_node_id, ARTBOARD_DIMENSIONS_INPUT_INDEX),
|
||||
NodeInput::value(TaggedValue::DVec2(dimensions), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the input, refresh the Properties panel, and run the document graph if skip_rerender is false
|
||||
@@ -864,3 +998,16 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the Stroke node's paint order input, which decides whether the stroke paints above or below the fill.
|
||||
/// Returns whether the graph changed.
|
||||
pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool {
|
||||
let connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput);
|
||||
if let Some(NodeInput::Value { tagged_value, .. }) = network_interface.input_from_connector(&connector, network_path)
|
||||
&& **tagged_value == TaggedValue::PaintOrder(paint_order)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
network_interface.set_input(&connector, NodeInput::value(TaggedValue::PaintOrder(paint_order), false), network_path);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::DVec2;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::consts::{
|
||||
VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR,
|
||||
};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::pointer::ViewportPosition;
|
||||
use crate::messages::portfolio::document::navigation::utility_types::NavigationOperation;
|
||||
use crate::messages::portfolio::document::utility_types::misc::PTZ;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+39
-46
@@ -1,7 +1,6 @@
|
||||
use super::DocumentNodeDefinition;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, WidgetOverride};
|
||||
use graph_craft::document::*;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputMetadata, NodeTemplate, NodeTemplateImplementation, WidgetOverride};
|
||||
use graphene_std::registry::*;
|
||||
use graphene_std::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -14,7 +13,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
let network_nodes = custom
|
||||
.into_iter()
|
||||
.filter_map(|definition| {
|
||||
if let DocumentNodeImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.document_node.implementation {
|
||||
if let NodeTemplateImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.implementation {
|
||||
definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition);
|
||||
return None;
|
||||
};
|
||||
@@ -71,29 +70,22 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
DocumentNodeDefinition {
|
||||
identifier: display_name,
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
inputs,
|
||||
call_argument: input_type.clone(),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
context_features: ContextDependencies::default(),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_metadata: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
locked: false,
|
||||
..Default::default()
|
||||
},
|
||||
inputs,
|
||||
call_argument: input_type.clone(),
|
||||
implementation: NodeTemplateImplementation::ProtoNode(id.clone()),
|
||||
// Context features are resolved from the registry at compile time, not persisted on the node
|
||||
context_features: ContextDependencies::default(),
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_metadata: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
category,
|
||||
description: Cow::Borrowed(description),
|
||||
@@ -104,13 +96,14 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
|
||||
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes
|
||||
for mut network_node in network_nodes {
|
||||
traverse_node(&network_node.node_template.document_node, &mut network_node.node_template.persistent_node_metadata, &definitions_map);
|
||||
fill_proto_node_metadata(&mut network_node.node_template, &definitions_map);
|
||||
|
||||
// Set the reference to the node identifier
|
||||
if let Some(nested_metadata) = network_node.node_template.persistent_node_metadata.network_metadata.as_mut() {
|
||||
nested_metadata.persistent_metadata.reference = Some(network_node.identifier.to_string());
|
||||
if let NodeTemplateImplementation::Network(network_template) = &mut network_node.node_template.implementation {
|
||||
network_template.reference = Some(network_node.identifier.to_string());
|
||||
// If it is not a merge node, then set the display name to the identifier/reference
|
||||
if network_node.identifier != "Merge" {
|
||||
network_node.node_template.persistent_node_metadata.display_name = network_node.identifier.to_string();
|
||||
network_node.node_template.display_name = network_node.identifier.to_string();
|
||||
}
|
||||
}
|
||||
definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node);
|
||||
@@ -119,27 +112,27 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
definitions_map
|
||||
}
|
||||
|
||||
/// Traverses a document node template and metadata in parallel to add metadata to the protonodes
|
||||
fn traverse_node(node: &DocumentNode, node_metadata: &mut DocumentNodePersistentMetadata, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
|
||||
match &node.implementation {
|
||||
DocumentNodeImplementation::Network(node_network) => {
|
||||
for (nested_node_id, nested_node) in node_network.nodes.iter() {
|
||||
let nested_metadata = node_metadata.network_metadata.as_mut().unwrap().persistent_metadata.node_metadata.get_mut(nested_node_id).unwrap();
|
||||
traverse_node(nested_node, &mut nested_metadata.persistent_metadata, definitions_map);
|
||||
/// Recursively fills each nested proto node's editor metadata from its definition, preserving only the authored position.
|
||||
fn fill_proto_node_metadata(node_template: &mut NodeTemplate, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
|
||||
match &mut node_template.implementation {
|
||||
NodeTemplateImplementation::Network(network_template) => {
|
||||
for nested_template in network_template.nodes.values_mut() {
|
||||
fill_proto_node_metadata(nested_template, definitions_map);
|
||||
}
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(id) => {
|
||||
// Set all the metadata except the position to the proto node information from the macro
|
||||
// TODO: Use options in the template to specify what you want to default and what you want to override
|
||||
// If this fails then the proto node id in the definition doesn't match what is generated by the macro
|
||||
NodeTemplateImplementation::ProtoNode(id) => {
|
||||
// If this lookup fails then the proto node id in the definition doesn't match what is generated by the macro
|
||||
let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else {
|
||||
// log::error!("Could not get definition for id {} when filling in protonode metadata for a custom node", id.clone());
|
||||
return;
|
||||
};
|
||||
let mut new_metadata = definition.node_template.persistent_node_metadata.clone();
|
||||
new_metadata.node_type_metadata = node_metadata.node_type_metadata.clone();
|
||||
*node_metadata = new_metadata
|
||||
|
||||
let definition_template = &definition.node_template;
|
||||
node_template.display_name = definition_template.display_name.clone();
|
||||
node_template.input_metadata = definition_template.input_metadata.clone();
|
||||
node_template.output_names = definition_template.output_names.clone();
|
||||
node_template.locked = definition_template.locked;
|
||||
node_template.pinned = definition_template.pinned;
|
||||
}
|
||||
DocumentNodeImplementation::Extract => {}
|
||||
NodeTemplateImplementation::Extract => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::utility_types::Direction;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector};
|
||||
@@ -157,7 +157,7 @@ pub enum NodeGraphMessage {
|
||||
SetInputValue {
|
||||
node_id: NodeId,
|
||||
input_index: usize,
|
||||
value: TaggedValue,
|
||||
value: Box<TaggedValue>,
|
||||
},
|
||||
SetInput {
|
||||
input_connector: InputConnector,
|
||||
|
||||
@@ -234,8 +234,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
x: (mid_point.x / 24.) as i32,
|
||||
y: (mid_point.y / 24.) as i32,
|
||||
});
|
||||
let node_input_connector = InputConnector::node(node_id, 0);
|
||||
let node_output_connector = OutputConnector::node(node_id, 0);
|
||||
let node_input_connector = InputConnector::primary_input(node_id);
|
||||
let node_output_connector = OutputConnector::primary_output(node_id);
|
||||
responses.add(NodeGraphMessage::CreateWire {
|
||||
output_connector,
|
||||
input_connector: node_input_connector,
|
||||
@@ -301,7 +301,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
// A freshly added Text node carries no font, so give it the default font (registered like the Text tool does)
|
||||
if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) {
|
||||
let font_resource_id = graph_craft::application_io::resource::ResourceId::new();
|
||||
if let Some(font_input) = node_template.document_node.inputs.get_mut(graphene_std::text::text::FontInput::INDEX) {
|
||||
if let Some(font_input) = node_template.input_mut(graphene_std::text::text::FontInput) {
|
||||
*font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false);
|
||||
}
|
||||
responses.add(DocumentMessage::Resource(ResourceMessage::AddFont {
|
||||
@@ -336,10 +336,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
};
|
||||
|
||||
// Ensure connection is to correct input of new node. If it does not have an input then do not connect
|
||||
if let Some((input_index, _)) = node_template.document_node.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) {
|
||||
if let Some((input_index, _)) = node_template.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) {
|
||||
responses.add(NodeGraphMessage::CreateWire {
|
||||
output_connector: *output_connector,
|
||||
input_connector: InputConnector::node(node_id, input_index),
|
||||
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
@@ -493,7 +493,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
*exposed = set_to_exposed;
|
||||
}
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, input_index),
|
||||
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||
input: node_input,
|
||||
});
|
||||
|
||||
@@ -515,7 +515,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
|
||||
let encapsulating_connector = InputConnector::node(*node_id, 0);
|
||||
let encapsulating_connector = InputConnector::primary_input(*node_id);
|
||||
if !exposed {
|
||||
network_interface.disconnect_input(&encapsulating_connector, network_path);
|
||||
}
|
||||
@@ -530,19 +530,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
|
||||
network_interface.set_input(&encapsulating_connector, input, network_path);
|
||||
|
||||
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
|
||||
log::error!("Could not get outward wires in remove_import");
|
||||
return;
|
||||
};
|
||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
|
||||
log::error!("Could not get outward wires for import in remove_import");
|
||||
return;
|
||||
};
|
||||
|
||||
// Disconnect all connections in the encapsulating network
|
||||
for downstream_connection in &downstream_connections {
|
||||
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
|
||||
}
|
||||
// Disconnect all connections fed by the hidden import in the nested network
|
||||
network_interface.disconnect_import_wires(0, breadcrumb_network_path);
|
||||
|
||||
responses.add(NodeGraphMessage::UpdateImportsExports);
|
||||
responses.add(NodeGraphMessage::SendWires);
|
||||
@@ -565,18 +554,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
|
||||
// Disconnect all connections in the encapsulating network
|
||||
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
|
||||
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
|
||||
log::error!("Could not get outward wires in remove_import");
|
||||
return;
|
||||
};
|
||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
|
||||
log::error!("Could not get outward wires for import in remove_import");
|
||||
return;
|
||||
};
|
||||
|
||||
for downstream_connection in &downstream_connections {
|
||||
network_interface.disconnect_input(downstream_connection, encapsulating_path);
|
||||
}
|
||||
network_interface.disconnect_output_wires(&OutputConnector::primary_output(*encapsulating_node), encapsulating_path);
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::UpdateImportsExports);
|
||||
@@ -618,7 +596,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
// Ensure that nodes can be grouped by checking if there is an unselected node between selected nodes
|
||||
for selected_node_id in &selected_node_ids {
|
||||
for input_index in 0..network_interface.number_of_inputs(selected_node_id, breadcrumb_network_path) {
|
||||
let input_connector = InputConnector::node(*selected_node_id, input_index);
|
||||
let input_connector = InputConnector::node_at_index(*selected_node_id, input_index);
|
||||
if let Some(upstream_deselected_node_id) = network_interface
|
||||
.upstream_output_connector(&input_connector, breadcrumb_network_path)
|
||||
.and_then(|output_connector| output_connector.node_id())
|
||||
@@ -640,7 +618,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
for node_id in nodes_sorted_top_to_bottom {
|
||||
for input_index in 0..network_interface.number_of_inputs(&node_id, breadcrumb_network_path) {
|
||||
let current_input_connector = InputConnector::node(node_id, input_index);
|
||||
let current_input_connector = InputConnector::node_at_index(node_id, input_index);
|
||||
let Some(upstream_connector) = network_interface.upstream_output_connector(¤t_input_connector, breadcrumb_network_path) else {
|
||||
continue;
|
||||
};
|
||||
@@ -693,7 +671,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32);
|
||||
default_node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1));
|
||||
default_node_template.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1));
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(NodeGraphMessage::InsertNode {
|
||||
node_id: encapsulating_node_id,
|
||||
@@ -737,7 +715,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
for (input_index, output_connector) in input_connections.into_iter().enumerate() {
|
||||
responses.add(NodeGraphMessage::CreateWire {
|
||||
output_connector,
|
||||
input_connector: InputConnector::node(encapsulating_node_id, input_index),
|
||||
input_connector: InputConnector::node_at_index(encapsulating_node_id, input_index),
|
||||
});
|
||||
}
|
||||
for (output_index, input_connectors) in output_connections.into_iter().enumerate() {
|
||||
@@ -1179,6 +1157,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
data_type: self.wire_in_progress_type,
|
||||
thick: false,
|
||||
dashed: false,
|
||||
is_list: false,
|
||||
center_path_string: String::new(),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
|
||||
}
|
||||
@@ -1350,7 +1330,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
self.shift_without_push = false;
|
||||
|
||||
// Reset all offsets to end the rubber banding while dragging
|
||||
network_interface.unload_stack_dependents_y_offset(selection_network_path);
|
||||
network_interface.clear_drag_offsets(selection_network_path);
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
|
||||
log::error!("Could not get selected nodes in PointerUp");
|
||||
return;
|
||||
@@ -1379,7 +1359,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
network_interface.try_set_upstream_to_chain(&InputConnector::node(layer, 1), selection_network_path);
|
||||
network_interface.try_set_upstream_to_chain(&InputConnector::layer_secondary_input(layer), selection_network_path);
|
||||
}
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
|
||||
@@ -1390,9 +1370,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
// Check if a single node was dragged onto a wire and that the node was dragged onto the wire
|
||||
if selected_nodes.selected_nodes_ref().len() == 1 && !self.begin_dragging {
|
||||
let selected_node_id = selected_nodes.selected_nodes_ref()[0];
|
||||
let has_primary_output_connection = network_interface
|
||||
.outward_wires(selection_network_path)
|
||||
.is_some_and(|outward_wires| outward_wires.get(&OutputConnector::node(selected_node_id, 0)).is_some_and(|outward_wires| !outward_wires.is_empty()));
|
||||
let has_primary_output_connection = network_interface.outward_wires(selection_network_path).is_some_and(|outward_wires| {
|
||||
outward_wires
|
||||
.get(&OutputConnector::primary_output(selected_node_id))
|
||||
.is_some_and(|outward_wires| !outward_wires.is_empty())
|
||||
});
|
||||
if !has_primary_output_connection {
|
||||
let Some(network) = network_interface.nested_network(selection_network_path) else {
|
||||
return;
|
||||
@@ -1417,7 +1399,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
// Prevent inserting on a link that is connected upstream to the selected node
|
||||
for upstream_node in network_interface.upstream_flow_back_from_nodes(vec![selected_node_id], selection_network_path, network_interface::FlowType::UpstreamFlow) {
|
||||
for input_index in 0..network_interface.number_of_inputs(&upstream_node, selection_network_path) {
|
||||
wires_to_check.remove(&InputConnector::node(upstream_node, input_index));
|
||||
wires_to_check.remove(&InputConnector::node_at_index(upstream_node, input_index));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1431,7 +1413,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return None;
|
||||
}
|
||||
|
||||
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
||||
let (wire, _center_line, 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);
|
||||
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
|
||||
@@ -1527,7 +1509,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::ShakeNode => {
|
||||
let Some(drag_start) = &self.drag_start else {
|
||||
let Some((drag_start, _)) = &mut self.drag_start else {
|
||||
log::error!("Drag start should be initialized when shaking a node");
|
||||
return;
|
||||
};
|
||||
@@ -1545,7 +1527,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
.transform_point2(viewport_location);
|
||||
|
||||
// Collect the distance to move the shaken nodes after the undo
|
||||
let graph_delta = IVec2::new(((point.x - drag_start.0.start_x) / 24.).round() as i32, ((point.y - drag_start.0.start_y) / 24.).round() as i32);
|
||||
let graph_delta = IVec2::new(((point.x - drag_start.start_x) / 24.).round() as i32, ((point.y - drag_start.start_y) / 24.).round() as i32);
|
||||
|
||||
// Keep the incremental rounding baseline in sync with the total shift reapplied below
|
||||
drag_start.round_x = graph_delta.x;
|
||||
drag_start.round_y = graph_delta.y;
|
||||
|
||||
// Undo to the state of the graph before shaking
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
@@ -1574,7 +1560,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
for selected_node in &all_selected_nodes {
|
||||
// Handle inputs of selected node
|
||||
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
|
||||
let input_connector = InputConnector::node(*selected_node, input_index);
|
||||
let input_connector = InputConnector::node_at_index(*selected_node, input_index);
|
||||
// Only disconnect inputs to non selected nodes
|
||||
if network_interface
|
||||
.upstream_output_connector(&input_connector, selection_network_path)
|
||||
@@ -1585,13 +1571,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
|
||||
let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
|
||||
let mut first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::node(*selected_node, 0), selection_network_path);
|
||||
let mut first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::primary_input(*selected_node), selection_network_path);
|
||||
while let Some(OutputConnector::Node { node_id, .. }) = &first_deselected_upstream_output {
|
||||
if !all_selected_nodes.contains(node_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::node(*node_id, 0), selection_network_path);
|
||||
first_deselected_upstream_output = network_interface.upstream_output_connector(&InputConnector::primary_input(*node_id), selection_network_path);
|
||||
}
|
||||
|
||||
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||
@@ -1614,7 +1600,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
// Handle reconnection
|
||||
// Find first non selected upstream node by primary flow
|
||||
if let Some(first_deselected_upstream_output) = first_deselected_upstream_output {
|
||||
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::node(*selected_node, 0)).cloned() else {
|
||||
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::primary_output(*selected_node)).cloned() else {
|
||||
log::error!("Could not get downstream_connections_to_first_output in shake node");
|
||||
return;
|
||||
};
|
||||
@@ -1726,7 +1712,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
continue;
|
||||
};
|
||||
|
||||
if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y {
|
||||
// Expand the cull box by a grid cell so a node stays rendered until its connectors, which reach beyond its bounding box, also leave the viewport
|
||||
let cull_margin = 24.;
|
||||
if node_bbox[1].x + cull_margin >= document_bbox[0].x
|
||||
&& node_bbox[0].x - cull_margin <= document_bbox[1].x
|
||||
&& node_bbox[1].y + cull_margin >= document_bbox[0].y
|
||||
&& node_bbox[0].y - cull_margin <= document_bbox[1].y
|
||||
{
|
||||
nodes.push(*node_id);
|
||||
}
|
||||
for error in &network_interface.resolved_types.node_graph_errors {
|
||||
@@ -1742,6 +1734,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(EventMessage::GraphChanged);
|
||||
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
||||
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
||||
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
||||
@@ -1769,16 +1762,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
||||
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
||||
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
||||
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::INDEX;
|
||||
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::PaintInput::INDEX;
|
||||
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
||||
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
||||
.into_iter()
|
||||
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
|
||||
});
|
||||
|
||||
let input = NodeInput::value(value, false);
|
||||
let input = NodeInput::value(*value, false);
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, input_index),
|
||||
input_connector: InputConnector::node_at_index(node_id, input_index),
|
||||
input,
|
||||
});
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
@@ -1804,7 +1797,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path);
|
||||
|
||||
if !rubber_band {
|
||||
network_interface.unload_stack_dependents_y_offset(selection_network_path);
|
||||
network_interface.clear_drag_offsets(selection_network_path);
|
||||
}
|
||||
|
||||
if graph_view_overlay_open {
|
||||
@@ -2167,7 +2160,37 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
|
||||
// Hidden passthrough nodes let a wire borrow its color and rank from an upstream node, so any type change can restyle wires whose own node is unchanged.
|
||||
// Compare each displayed wire's style (color, rank) across the update and unload only those that changed, so value-only recompiles keep their built wire paths.
|
||||
let types_changed = !resolved_types.add.is_empty() || !resolved_types.remove.is_empty();
|
||||
let wire_style = |network_interface: &mut NodeNetworkInterface, input: &InputConnector| {
|
||||
network_interface.upstream_output_connector(input, breadcrumb_network_path).map(|output| {
|
||||
let output_type = network_interface.output_type(&output, breadcrumb_network_path);
|
||||
(output_type.displayed_type(), output_type.is_list())
|
||||
})
|
||||
};
|
||||
let styles_before = types_changed.then(|| {
|
||||
network_interface
|
||||
.node_graph_input_connectors(breadcrumb_network_path)
|
||||
.into_iter()
|
||||
.map(|input| {
|
||||
let style = wire_style(network_interface, &input);
|
||||
(input, style)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
network_interface.resolved_types.update(resolved_types, node_graph_errors);
|
||||
|
||||
if let Some(styles_before) = styles_before {
|
||||
for (input, style_before) in styles_before {
|
||||
if wire_style(network_interface, &input) != style_before {
|
||||
network_interface.unload_wire(&input, breadcrumb_network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
NodeGraphMessage::UpdateActionButtons => {
|
||||
if selection_network_path == breadcrumb_network_path {
|
||||
@@ -2406,7 +2429,7 @@ impl NodeGraphMessageHandler {
|
||||
if subgraph_path_names_length >= 2 {
|
||||
widgets.extend([
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
BreadcrumbTrailButtons::new(subgraph_path_names)
|
||||
BreadcrumbTrailButtons::new(subgraph_path_names.iter().map(|name| truncate_breadcrumb_label(name)).collect())
|
||||
.on_update(move |index| {
|
||||
DocumentMessage::ExitNestedNetwork {
|
||||
steps_back: subgraph_path_names_length - (*index as usize) - 1,
|
||||
@@ -2591,7 +2614,7 @@ impl NodeGraphMessageHandler {
|
||||
.popover_layout({
|
||||
let compatible_type = context
|
||||
.network_interface
|
||||
.upstream_output_connector(&InputConnector::node(layer, 1), &[])
|
||||
.upstream_output_connector(&InputConnector::layer_secondary_input(layer), &[])
|
||||
.and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string());
|
||||
|
||||
let mut node_chooser = NodeCatalog::new();
|
||||
@@ -2688,7 +2711,7 @@ impl NodeGraphMessageHandler {
|
||||
};
|
||||
let mut nodes = Vec::new();
|
||||
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
|
||||
let primary_input_connector = InputConnector::node(node_id, 0);
|
||||
let primary_input_connector = InputConnector::primary_input(node_id);
|
||||
|
||||
let primary_input = if network_interface
|
||||
.input_from_connector(&primary_input_connector, breadcrumb_network_path)
|
||||
@@ -2699,10 +2722,10 @@ impl NodeGraphMessageHandler {
|
||||
None
|
||||
};
|
||||
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
|
||||
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path))
|
||||
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node_at_index(node_id, input_index), breadcrumb_network_path))
|
||||
.collect();
|
||||
|
||||
let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
|
||||
let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::primary_output(node_id), breadcrumb_network_path);
|
||||
|
||||
let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
|
||||
.filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
|
||||
@@ -2830,7 +2853,7 @@ impl NodeGraphMessageHandler {
|
||||
}))
|
||||
);
|
||||
|
||||
let clippable = layer.can_be_clipped(network_interface.document_metadata());
|
||||
let clippable = layer.can_be_clipped(network_interface.document_metadata()) && network_interface.layer_hosts_blending_nodes(&node_id, &[]);
|
||||
|
||||
let data = LayerPanelEntry {
|
||||
id: node_id,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,13 @@ pub struct FrontendNode {
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InputTypeConstraint {
|
||||
Limited(std::collections::BTreeSet<String>),
|
||||
All,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FrontendNodeType {
|
||||
@@ -112,7 +119,7 @@ pub struct FrontendNodeType {
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
#[serde(rename = "inputTypes")]
|
||||
pub input_types: Vec<String>,
|
||||
pub input_types: Vec<InputTypeConstraint>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
||||
use glam::DVec2;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::style::FillChoiceUI;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
|
||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||
let origin = document.snapping_state.grid.origin;
|
||||
@@ -274,7 +274,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
]);
|
||||
color_widgets.push(
|
||||
ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
|
||||
ColorInput::new(FillChoice::<SRGBA8>::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
|
||||
.tooltip_label("Grid Display Color")
|
||||
.allow_none(false)
|
||||
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use super::utility_types::{DrawHandles, OverlayContext};
|
||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
use crate::consts::{HIDE_HANDLE_DISTANCE, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||
pub use crate::messages::portfolio::document::utility_types::text_metrics::text_width;
|
||||
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||
use crate::messages::tool::common_functionality::utility_functions::closest_open_path_endpoint;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles};
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||
use kurbo::{Affine, ParamCurve, PathSeg};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
#[cfg(target_family = "wasm")]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -61,7 +60,7 @@ pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Adding segments which are are connected to selected anchors
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
for (segment_id, _, start, end) in vector.segment_iter() {
|
||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||
selected_segments.push(segment_id);
|
||||
}
|
||||
@@ -69,23 +68,25 @@ pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState)
|
||||
selected_segments
|
||||
}
|
||||
|
||||
fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAffine2, is_selected: impl Fn(ManipulatorPointId) -> bool, overlay_context: &mut OverlayContext) {
|
||||
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
|
||||
fn overlay_bezier_handles(segment: PathSeg, segment_id: SegmentId, transform: DAffine2, is_selected: impl Fn(ManipulatorPointId) -> bool, overlay_context: &mut OverlayContext) {
|
||||
let segment = Affine::new(transform.to_cols_array()) * segment;
|
||||
let segment_start = point_to_dvec2(segment.start());
|
||||
let segment_end = point_to_dvec2(segment.end());
|
||||
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||
|
||||
match bezier.handles {
|
||||
BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
|
||||
overlay_context.line(handle, bezier.start, None, None);
|
||||
overlay_context.line(handle, bezier.end, None, None);
|
||||
match segment_to_handles(&segment) {
|
||||
BezierHandles::Quadratic { handle } if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) => {
|
||||
overlay_context.line(handle, segment_start, None, None);
|
||||
overlay_context.line(handle, segment_end, None, None);
|
||||
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
if not_under_anchor(handle_start, bezier.start) {
|
||||
overlay_context.line(handle_start, bezier.start, None, None);
|
||||
if not_under_anchor(handle_start, segment_start) {
|
||||
overlay_context.line(handle_start, segment_start, None, None);
|
||||
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||
}
|
||||
if not_under_anchor(handle_end, bezier.end) {
|
||||
overlay_context.line(handle_end, bezier.end, None, None);
|
||||
if not_under_anchor(handle_end, segment_end) {
|
||||
overlay_context.line(handle_end, segment_end, None, None);
|
||||
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
||||
}
|
||||
}
|
||||
@@ -94,7 +95,7 @@ fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAff
|
||||
}
|
||||
|
||||
fn overlay_bezier_handle_specific_point(
|
||||
bezier: Bezier,
|
||||
segment: PathSeg,
|
||||
segment_id: SegmentId,
|
||||
(start, end): (PointId, PointId),
|
||||
point_to_render: PointId,
|
||||
@@ -102,24 +103,24 @@ fn overlay_bezier_handle_specific_point(
|
||||
is_selected: impl Fn(ManipulatorPointId) -> bool,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
|
||||
let segment = Affine::new(transform.to_cols_array()) * segment;
|
||||
let segment_start = point_to_dvec2(segment.start());
|
||||
let segment_end = point_to_dvec2(segment.end());
|
||||
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||
|
||||
match bezier.handles {
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) {
|
||||
let end = if start == point_to_render { bezier.start } else { bezier.end };
|
||||
overlay_context.line(handle, end, None, None);
|
||||
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||
}
|
||||
match segment_to_handles(&segment) {
|
||||
BezierHandles::Quadratic { handle } if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) => {
|
||||
let anchor = if start == point_to_render { segment_start } else { segment_end };
|
||||
overlay_context.line(handle, anchor, None, None);
|
||||
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
if not_under_anchor(handle_start, bezier.start) && (point_to_render == start) {
|
||||
overlay_context.line(handle_start, bezier.start, None, None);
|
||||
if not_under_anchor(handle_start, segment_start) && (point_to_render == start) {
|
||||
overlay_context.line(handle_start, segment_start, None, None);
|
||||
overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
|
||||
}
|
||||
if not_under_anchor(handle_end, bezier.end) && (point_to_render == end) {
|
||||
overlay_context.line(handle_end, bezier.end, None, None);
|
||||
if not_under_anchor(handle_end, segment_end) && (point_to_render == end) {
|
||||
overlay_context.line(handle_end, segment_end, None, None);
|
||||
overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +133,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
let display_handles = overlay_context.visibility_settings.handles();
|
||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||
if display_path {
|
||||
@@ -154,23 +155,23 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
|
||||
match draw_handles {
|
||||
DrawHandles::All => {
|
||||
vector.segment_bezier_iter().for_each(|(segment_id, bezier, _start, _end)| {
|
||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
||||
vector.segment_iter().for_each(|(segment_id, segment, _start, _end)| {
|
||||
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||
});
|
||||
}
|
||||
DrawHandles::SelectedAnchors(ref selected_segments) => {
|
||||
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.segment_iter()
|
||||
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
|
||||
.for_each(|(segment_id, bezier, _start, _end)| {
|
||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
||||
.for_each(|(segment_id, segment, _start, _end)| {
|
||||
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||
});
|
||||
|
||||
for (segment_id, bezier, start, end) in vector.segment_bezier_iter() {
|
||||
for (segment_id, segment, start, end) in vector.segment_iter() {
|
||||
if let Some((corresponding_anchor, _)) = opposite_handles_data.iter().find(|(_, adj_segment_id)| adj_segment_id == &segment_id) {
|
||||
overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context);
|
||||
overlay_bezier_handle_specific_point(segment, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,14 +179,14 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.segment_iter()
|
||||
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
|
||||
.for_each(|(segment_id, bezier, start, end)| {
|
||||
.for_each(|(segment_id, segment, start, end)| {
|
||||
if segment_endpoints.get(&segment_id).unwrap().len() == 1 {
|
||||
let point_to_render = segment_endpoints.get(&segment_id).unwrap()[0];
|
||||
overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), point_to_render, transform, is_selected, overlay_context);
|
||||
overlay_bezier_handle_specific_point(segment, segment_id, (start, end), point_to_render, transform, is_selected, overlay_context);
|
||||
} else {
|
||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
||||
overlay_bezier_handles(segment, segment_id, transform, is_selected, overlay_context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -201,43 +202,41 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
|
||||
/// Draws an anchor overlay at each endpoint of every open path on the selected visible layers, in the selected style for endpoints that are part of the path editing selection.
|
||||
/// Given a pointer position, the endpoint a press there would continue from is drawn in the hover style instead.
|
||||
pub fn open_path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &ShapeState, pointer: Option<DVec2>, overlay_context: &mut OverlayContext) {
|
||||
if !overlay_context.visibility_settings.anchors() {
|
||||
return;
|
||||
}
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else {
|
||||
continue;
|
||||
};
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
||||
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
let is_selected = |layer: LayerNodeIdentifier, id: PointId| {
|
||||
shape_editor
|
||||
.selected_shape_state
|
||||
.get(&layer)
|
||||
.is_some_and(|state| state.is_point_selected(ManipulatorPointId::Anchor(id)))
|
||||
};
|
||||
let hovered = pointer.and_then(|pointer| closest_open_path_endpoint(document, pointer, SNAP_POINT_TOLERANCE, selected_nodes.selected_visible_layers(&document.network_interface)));
|
||||
|
||||
for point in vector.anchor_endpoints() {
|
||||
let Some(position) = vector.point_domain.position_from_id(point) else { continue };
|
||||
let position = transform.transform_point2(position);
|
||||
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
|
||||
for layer in selected_nodes.selected_visible_layers(&document.network_interface) {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||
|
||||
for id in vector.anchor_endpoints() {
|
||||
if hovered.is_some_and(|(hovered_layer, hovered_id, _)| hovered_layer == layer && hovered_id == id) {
|
||||
continue;
|
||||
}
|
||||
let Some(position) = vector.point_domain.position_from_id(id) else { continue };
|
||||
|
||||
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(layer, id), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
|
||||
|
||||
pub fn text_width(text: &str, font_size: f64) -> f64 {
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
letter_spacing: 0.,
|
||||
letter_tilt: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
align: TextAlign::AlignLeft,
|
||||
};
|
||||
|
||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
||||
let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||
bounds.x
|
||||
// Drawn last so its halo sits above any other endpoint at the same spot
|
||||
if let Some((layer, id, position)) = hovered {
|
||||
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
|
||||
overlay_context.hover_manipulator_anchor(transform.transform_point2(position), is_selected(layer, id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
|
||||
|
||||
@@ -3,19 +3,19 @@ use crate::consts::{
|
||||
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
||||
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::hex_to_rgba_u8;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||
use crate::messages::prelude::Message;
|
||||
use crate::messages::prelude::ViewportMessageHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::ATTR_TRANSFORM;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::{self, Subpath};
|
||||
use graphene_std::text::{TextAlign, TypesettingConfig};
|
||||
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::misc::point_to_dvec2;
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||
@@ -403,14 +403,14 @@ impl OverlayContext {
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
self.internal().fill_path(subpaths, transform, color);
|
||||
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
self.internal().fill_path(bezpath, transform, color);
|
||||
}
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
self.internal().fill_path_pattern(subpaths, transform, color);
|
||||
pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
self.internal().fill_path_pattern(bezpath, transform, color);
|
||||
}
|
||||
|
||||
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
|
||||
@@ -977,49 +977,25 @@ impl OverlayContextInternal {
|
||||
path.push(bezier.as_path_el());
|
||||
}
|
||||
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
|
||||
fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
|
||||
for subpath in subpaths {
|
||||
let subpath = subpath.borrow();
|
||||
let mut curves = subpath.iter().peekable();
|
||||
let snap_start = |context: &Self, point: kurbo::Point| {
|
||||
let snapped = context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
|
||||
kurbo::Point::new(snapped.x, snapped.y)
|
||||
};
|
||||
let snap_center = |context: &Self, point: kurbo::Point| {
|
||||
let snapped = context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
|
||||
kurbo::Point::new(snapped.x, snapped.y)
|
||||
};
|
||||
|
||||
let Some(first) = curves.peek() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
|
||||
let start_point = self.snap_to_physical_pixel(start_point);
|
||||
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
|
||||
|
||||
for curve in curves {
|
||||
match curve {
|
||||
PathSeg::Line(line) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(line.p1));
|
||||
let a = self.snap_to_physical_pixel_center(a);
|
||||
path.line_to(kurbo::Point::new(a.x, a.y));
|
||||
}
|
||||
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 = self.snap_to_physical_pixel_center(a);
|
||||
let b = self.snap_to_physical_pixel_center(b);
|
||||
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
|
||||
}
|
||||
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 = self.snap_to_physical_pixel_center(a);
|
||||
let b = self.snap_to_physical_pixel_center(b);
|
||||
let c = self.snap_to_physical_pixel_center(c);
|
||||
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subpath.closed() {
|
||||
path.close_path();
|
||||
for element in bezpath.elements() {
|
||||
match *element {
|
||||
kurbo::PathEl::MoveTo(point) => path.move_to(snap_start(self, point)),
|
||||
kurbo::PathEl::LineTo(point) => path.line_to(snap_center(self, point)),
|
||||
kurbo::PathEl::QuadTo(a, b) => path.quad_to(snap_center(self, a), snap_center(self, b)),
|
||||
kurbo::PathEl::CurveTo(a, b, c) => path.curve_to(snap_center(self, a), snap_center(self, b), snap_center(self, c)),
|
||||
kurbo::PathEl::ClosePath => path.close_path(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,20 +1004,19 @@ 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<subpath::Subpath<PointId>> = vec![];
|
||||
let mut combined = BezPath::new();
|
||||
|
||||
for target_type in target_types {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||
}
|
||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
||||
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
|
||||
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
|
||||
}
|
||||
}
|
||||
|
||||
if !subpaths.is_empty() {
|
||||
let path = self.push_path(subpaths.iter(), transform);
|
||||
if !combined.is_empty() {
|
||||
let path = self.push_path(&combined, transform);
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
|
||||
@@ -1050,15 +1025,15 @@ impl OverlayContextInternal {
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
let path = self.push_path(subpaths, transform);
|
||||
fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
let path = self.push_path(bezpath, transform);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
|
||||
}
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
const PATTERN_WIDTH: u32 = 4;
|
||||
const PATTERN_HEIGHT: u32 = 4;
|
||||
|
||||
@@ -1095,7 +1070,7 @@ impl OverlayContextInternal {
|
||||
},
|
||||
};
|
||||
|
||||
let path = self.push_path(subpaths, transform);
|
||||
let path = self.push_path(bezpath, transform);
|
||||
let brush = peniko::Brush::Image(image);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
|
||||
@@ -1116,17 +1091,17 @@ impl OverlayContextInternal {
|
||||
align: TextAlign::AlignLeft,
|
||||
};
|
||||
|
||||
// Get text dimensions directly from layout
|
||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
||||
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||
// Lay out the text once, taking its dimensions and vector paths from the same thread-local context pass
|
||||
let (text_size, text_list) = TextContext::with_thread_local(|text_context| {
|
||||
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||
(text_size, text_list)
|
||||
});
|
||||
let text_width = text_size.x;
|
||||
let text_height = text_size.y;
|
||||
// Create a rect from the size (assuming text starts at origin)
|
||||
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
|
||||
|
||||
// Convert text to vector paths for rendering
|
||||
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||
|
||||
// Calculate position based on pivot
|
||||
let mut position = DVec2::ZERO;
|
||||
match pivot[0] {
|
||||
@@ -1189,7 +1164,7 @@ impl OverlayContextInternal {
|
||||
|
||||
let width = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
None => &format_rounded(translation.x, 2),
|
||||
};
|
||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
@@ -1200,7 +1175,7 @@ impl OverlayContextInternal {
|
||||
|
||||
let height = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
None => &format_rounded(translation.y, 2),
|
||||
};
|
||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
|
||||
@@ -7,16 +7,16 @@ use crate::consts::{
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||
use crate::messages::viewport::ViewportMessageHandler;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
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 kurbo::{self, Affine, BezPath, CubicBez, ParamCurve, PathSeg};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
|
||||
@@ -930,50 +930,33 @@ impl OverlayContext {
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
|
||||
fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
self.render_context.begin_path();
|
||||
for subpath in subpaths {
|
||||
let subpath = subpath.borrow();
|
||||
let mut curves = subpath.iter().peekable();
|
||||
|
||||
let Some(&first) = curves.peek() else {
|
||||
continue;
|
||||
};
|
||||
let snap_start = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
|
||||
let snap_center = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
|
||||
|
||||
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
|
||||
let start_point = self.snap_to_physical_pixel(start_point);
|
||||
self.render_context.move_to(start_point.x, start_point.y);
|
||||
|
||||
for curve in curves {
|
||||
match curve {
|
||||
PathSeg::Line(line) => {
|
||||
let a = transform.transform_point2(point_to_dvec2(line.p1));
|
||||
let a = self.snap_to_physical_pixel_center(a);
|
||||
self.render_context.line_to(a.x, a.y);
|
||||
}
|
||||
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 = self.snap_to_physical_pixel_center(a);
|
||||
let b = self.snap_to_physical_pixel_center(b);
|
||||
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
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 = self.snap_to_physical_pixel_center(a);
|
||||
let b = self.snap_to_physical_pixel_center(b);
|
||||
let c = self.snap_to_physical_pixel_center(c);
|
||||
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
|
||||
}
|
||||
for element in bezpath.elements() {
|
||||
match *element {
|
||||
kurbo::PathEl::MoveTo(point) => {
|
||||
let point = snap_start(self, point);
|
||||
self.render_context.move_to(point.x, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
if subpath.closed() {
|
||||
self.render_context.close_path();
|
||||
kurbo::PathEl::LineTo(point) => {
|
||||
let point = snap_center(self, point);
|
||||
self.render_context.line_to(point.x, point.y);
|
||||
}
|
||||
kurbo::PathEl::QuadTo(a, b) => {
|
||||
let (a, b) = (snap_center(self, a), snap_center(self, b));
|
||||
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
kurbo::PathEl::CurveTo(a, b, c) => {
|
||||
let (a, b, c) = (snap_center(self, a), snap_center(self, b), snap_center(self, c));
|
||||
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
|
||||
}
|
||||
kurbo::PathEl::ClosePath => self.render_context.close_path(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,18 +965,17 @@ 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<Subpath<PointId>> = vec![];
|
||||
let mut combined = BezPath::new();
|
||||
|
||||
target_types.for_each(|target_type| match target_type.borrow() {
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||
}
|
||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
||||
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
|
||||
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
|
||||
});
|
||||
|
||||
if !subpaths.is_empty() {
|
||||
self.push_path(subpaths.iter(), transform);
|
||||
if !combined.is_empty() {
|
||||
self.push_path(&combined, transform);
|
||||
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.set_stroke_style_str(color);
|
||||
@@ -1004,8 +986,8 @@ impl OverlayContext {
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
self.push_path(subpaths, transform);
|
||||
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
self.push_path(bezpath, transform);
|
||||
|
||||
self.render_context.set_fill_style_str(color);
|
||||
self.render_context.fill();
|
||||
@@ -1013,7 +995,7 @@ impl OverlayContext {
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
|
||||
const PATTERN_WIDTH: usize = 4;
|
||||
const PATTERN_HEIGHT: usize = 4;
|
||||
|
||||
@@ -1046,7 +1028,7 @@ impl OverlayContext {
|
||||
pattern_context.put_image_data(&image_data, 0, 0).unwrap();
|
||||
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
|
||||
|
||||
self.push_path(subpaths, transform);
|
||||
self.push_path(bezpath, transform);
|
||||
|
||||
self.render_context.set_fill_style_canvas_pattern(&pattern);
|
||||
self.render_context.fill();
|
||||
@@ -1090,7 +1072,7 @@ impl OverlayContext {
|
||||
|
||||
let width = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
None => &format_rounded(translation.x, 2),
|
||||
};
|
||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
@@ -1101,7 +1083,7 @@ impl OverlayContext {
|
||||
|
||||
let height = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
None => &format_rounded(translation.y, 2),
|
||||
};
|
||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::portfolio::{document::resource::utility_types::EmbeddedReso
|
||||
use crate::messages::prelude::*;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry};
|
||||
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry, ResourceStorage};
|
||||
use graphene_std::text::Font;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
@@ -12,6 +12,7 @@ use url::Url;
|
||||
pub struct ResourceMessageContext<'a> {
|
||||
pub document_id: DocumentId,
|
||||
pub fonts: &'a FontsMessageHandler,
|
||||
pub resource_storage: &'a ResourceStorageMessageHandler,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
||||
@@ -25,7 +26,7 @@ pub struct ResourceMessageHandler {
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
|
||||
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
|
||||
let ResourceMessageContext { document_id, fonts } = context;
|
||||
let ResourceMessageContext { document_id, fonts, resource_storage } = context;
|
||||
|
||||
match message {
|
||||
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
||||
@@ -48,8 +49,9 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
responses.add(ResourceMessage::Resolve { resource_id });
|
||||
}
|
||||
ResourceMessage::ResolveAll => {
|
||||
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
|
||||
for id in unresolved_ids {
|
||||
let storage = resource_storage.resources_mut();
|
||||
let ids: Vec<ResourceId> = self.registry.ids().filter(|id| !self.registry.hash(id).is_some_and(|hash| storage.contains(&hash))).collect();
|
||||
for id in ids {
|
||||
if self.pending_resolves.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
@@ -65,7 +67,7 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
log::error!("Resolve for {resource_id}: no registry entry");
|
||||
return;
|
||||
};
|
||||
if info.hash.is_some() {
|
||||
if info.hash.is_some_and(|hash| resource_storage.resources_mut().contains(hash)) {
|
||||
log::warn!("Resource {resource_id} already resolved");
|
||||
return;
|
||||
}
|
||||
@@ -297,3 +299,47 @@ impl<'de> serde::Deserialize<'de> for ResourceMessageHandler {
|
||||
deserializer.deserialize_map(EmbeddedResourcesVisitor { human_readable })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use graph_craft::application_io::resource::ResourceStorage;
|
||||
|
||||
#[test]
|
||||
fn resolve_all_refetches_resources_whose_bytes_are_missing() {
|
||||
let mut handler = ResourceMessageHandler::default();
|
||||
let storage = ResourceStorageMessageHandler::default();
|
||||
let fonts = FontsMessageHandler::default();
|
||||
|
||||
let font = |style: &str| DataSource::Font {
|
||||
family: "Lato".into(),
|
||||
style: Some(style.into()),
|
||||
};
|
||||
|
||||
let cached = ResourceId::from(1);
|
||||
let cached_hash = storage.resources_mut().store(b"stored font bytes");
|
||||
handler.registry.resolve(&cached, cached_hash);
|
||||
handler.registry.push_source_back(&cached, font("Regular (400)"));
|
||||
|
||||
let evicted = ResourceId::from(2);
|
||||
let evicted_hash = ResourceHash::from(b"evicted font bytes".as_slice());
|
||||
handler.registry.resolve(&evicted, evicted_hash);
|
||||
handler.registry.push_source_back(&evicted, font("Black (900)"));
|
||||
|
||||
let mut responses = VecDeque::new();
|
||||
handler.process_message(
|
||||
ResourceMessage::ResolveAll,
|
||||
&mut responses,
|
||||
ResourceMessageContext {
|
||||
document_id: DocumentId(0),
|
||||
fonts: &fonts,
|
||||
resource_storage: &storage,
|
||||
},
|
||||
);
|
||||
|
||||
let resolve_requested = |id: ResourceId| responses.contains(&Message::from(ResourceMessage::Resolve { resource_id: id }));
|
||||
assert!(resolve_requested(evicted), "a resource whose bytes are gone should be resolved again");
|
||||
assert!(!resolve_requested(cached), "a resource whose bytes are in storage should be left alone");
|
||||
assert_eq!(handler.registry.hash(&evicted), Some(evicted_hash), "the hash keeps naming the content");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::storage_metadata::{StorageMetadataView, build_interface_from_storage};
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use graphene_std::NodeParameter;
|
||||
use graphene_std::vector::style::RenderMode;
|
||||
|
||||
/// Every node addressable in `original` resolves identically through the round-tripped interface:
|
||||
@@ -708,3 +709,171 @@ async fn demo_artwork_edit_autosaves_and_round_trips() {
|
||||
// Autosaving the undone state still round-trips cleanly (no drift panic).
|
||||
editor.active_document_mut().commit_storage_snapshot(&byte_store, true);
|
||||
}
|
||||
|
||||
/// The document's single Fill node, as `(network_path, node_id)`.
|
||||
fn find_fill_node(document: &DocumentMessageHandler) -> (Vec<graph_craft::document::NodeId>, graph_craft::document::NodeId) {
|
||||
node_paths(&document.network_interface)
|
||||
.into_iter()
|
||||
.find(|(network_path, node_id)| {
|
||||
let Some(network) = document.network_interface.nested_network(network_path) else { return false };
|
||||
network.nodes[node_id].implementation == graph_craft::document::DocumentNodeImplementation::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER)
|
||||
})
|
||||
.expect("the document should contain a Fill node")
|
||||
}
|
||||
|
||||
/// The stored paint value of the document's single Fill node.
|
||||
fn fill_paint_value(document: &DocumentMessageHandler) -> graph_craft::document::value::TaggedValue {
|
||||
let (network_path, node_id) = find_fill_node(document);
|
||||
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||
let input = network.nodes[&node_id].input(graphene_std::vector::fill::PaintInput).expect("Fill should have a paint input");
|
||||
input.as_value().expect("the paint input should hold a value").clone()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn none_fill_survives_document_reopen() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
// Pick the red-slash "none" paint, stored the same way as the Fill widget's None choice
|
||||
let (_, fill_node_id) = find_fill_node(editor.active_document());
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::SetInputValue {
|
||||
node_id: fill_node_id,
|
||||
input_index: graphene_std::vector::fill::PaintInput::INDEX,
|
||||
value: graph_craft::document::value::TaggedValue::no_paint().into(),
|
||||
})
|
||||
.await;
|
||||
assert!(fill_paint_value(editor.active_document()).is_no_paint(), "the None pick should store as no_paint");
|
||||
|
||||
// Reopen through the editor's real open path, which runs the document migrations
|
||||
let serialized = editor.active_document().serialize_document();
|
||||
editor
|
||||
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: None,
|
||||
document_path: None,
|
||||
document_serialized_content: serialized,
|
||||
})
|
||||
.await;
|
||||
|
||||
let reopened_paint = fill_paint_value(editor.active_document());
|
||||
assert!(reopened_paint.is_no_paint(), "a none fill should survive reopening, but the stored paint became {reopened_paint:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
|
||||
// A minimal master-era document: a 4-input Fill (content, fill: wired, backup color, backup gradient) fed by another node
|
||||
const LEGACY_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Node":{"node_id":2,"output_index":0,"lambda":false}},{"Value":{"tagged_value":{"OptionalColor":null},"exposed":false}},{"Value":{"tagged_value":{"Gradient":{"stops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]],"gradient_type":"Linear","start":[0.0,0.5],"end":[1.0,0.5],"transform":[1.0,0.0,0.0,1.0,0.0,0.0]}},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}],[2,{"inputs":[{"Value":{"tagged_value":"None","exposed":false}},{"Value":{"tagged_value":{"GradientStops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]]},"exposed":false}},{"Value":{"tagged_value":{"F64":0.5},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::ops::SampleGradientNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Vector Data"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null}],"output_names":["Future<Instances<VectorData>>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}],[2,{"persistent_metadata":{"reference":"Sample Gradient","display_name":"","input_properties":[{"input_data":{"input_name":"Primary"},"widget_override":null},{"input_data":{"input_name":"Gradient"},"widget_override":null},{"input_data":{"input_name":"Position"},"widget_override":null}],"output_names":["Future<Color>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-20,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"legacy_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
|
||||
|
||||
// Deserializing alone must succeed, so a failure below is attributable to the migrations
|
||||
DocumentMessageHandler::deserialize_document(LEGACY_DOCUMENT).expect("the legacy document should deserialize");
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor
|
||||
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: None,
|
||||
document_path: None,
|
||||
document_serialized_content: LEGACY_DOCUMENT.to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let (network_path, node_id) = find_fill_node(document);
|
||||
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||
let fill_node = &network.nodes[&node_id];
|
||||
|
||||
assert_eq!(fill_node.inputs.len(), 7, "the legacy Fill should upgrade to the 7-input shape");
|
||||
let paint = fill_node.input(graphene_std::vector::fill::PaintInput);
|
||||
assert!(
|
||||
matches!(paint, Some(graph_craft::document::NodeInput::Node { .. })),
|
||||
"the wired legacy fill should keep its connection, but became {paint:?}"
|
||||
);
|
||||
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
||||
assert!(
|
||||
matches!(has_transform, Some(TaggedValue::Bool(_))),
|
||||
"the has-transform input should hold a bool, but became {has_transform:?}"
|
||||
);
|
||||
let transform = fill_node.input_value(graphene_std::vector::fill::TransformInput);
|
||||
assert!(
|
||||
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||
"the transform input should hold a matrix, but became {transform:?}"
|
||||
);
|
||||
|
||||
// The Evaluate Gradient parameter held the tuple-form stops, which parse as the ramp value with even positions elided
|
||||
let evaluate_gradient_node = &network.nodes[&graph_craft::document::NodeId(2)];
|
||||
let stops = evaluate_gradient_node.input_value(graphene_std::math_nodes::evaluate_gradient::GradientInput);
|
||||
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
|
||||
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
ramp.gradient_space,
|
||||
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
|
||||
);
|
||||
let stops = graphene_std::vector::Gradient::from(ramp);
|
||||
assert_eq!(stops.len(), 2);
|
||||
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::vector::style::GradientSpread;
|
||||
|
||||
// A minimal document from the era when the gradient spread was the Fill node's own sixth input (tagged `GradientSpreadMethod`), here set to Repeat
|
||||
const EIGHT_INPUT_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"Color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}},"exposed":false}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"GradientType":"Linear"},"exposed":false}},{"Value":{"tagged_value":{"GradientSpreadMethod":"Repeat"},"exposed":false}},{"Value":{"tagged_value":{"Bool":false},"exposed":false}},{"Value":{"tagged_value":{"DAffine2":[1.0,0.0,0.0,1.0,0.0,0.0]},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Content"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null},{"input_data":{"input_name":"Gradient Type"},"widget_override":null},{"input_data":{"input_name":"Spread Method"},"widget_override":null},{"input_data":{"input_name":"Has Transform"},"widget_override":null},{"input_data":{"input_name":"Transform"},"widget_override":null}],"output_names":[""],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"eight_input_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
|
||||
|
||||
// Deserializing alone must succeed, so a failure below is attributable to the migrations
|
||||
DocumentMessageHandler::deserialize_document(EIGHT_INPUT_DOCUMENT).expect("the eight-input document should deserialize");
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor
|
||||
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: None,
|
||||
document_path: None,
|
||||
document_serialized_content: EIGHT_INPUT_DOCUMENT.to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let (network_path, node_id) = find_fill_node(document);
|
||||
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||
let fill_node = &network.nodes[&node_id];
|
||||
|
||||
assert_eq!(fill_node.inputs.len(), 7, "the eight-input Fill should fold down to the 7-input shape");
|
||||
|
||||
let paint = fill_node.input_value(graphene_std::vector::fill::PaintInput);
|
||||
let Some(TaggedValue::GradientRamp(ramp)) = paint else {
|
||||
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
|
||||
};
|
||||
assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp");
|
||||
assert_eq!(
|
||||
ramp.gradient_space,
|
||||
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
|
||||
);
|
||||
|
||||
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
|
||||
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
|
||||
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
|
||||
};
|
||||
assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp");
|
||||
assert_eq!(
|
||||
backup_ramp.gradient_space,
|
||||
graphene_std::vector::style::GradientSpace::RgbGamma,
|
||||
"the backup ramp should carry the explicit gamma interpolation too"
|
||||
);
|
||||
|
||||
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
|
||||
assert!(
|
||||
matches!(has_transform, Some(TaggedValue::Bool(false))),
|
||||
"the has-transform input should shift down intact, but became {has_transform:?}"
|
||||
);
|
||||
let transform = fill_node.input_value(graphene_std::vector::fill::TransformInput);
|
||||
assert!(
|
||||
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||
"the transform input should shift down intact, but became {transform:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ use graph_craft::document::NodeId;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::{PointId, Vector};
|
||||
use kurbo::{Affine, BezPath};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::num::NonZeroU64;
|
||||
use std::sync::Arc;
|
||||
@@ -193,11 +193,13 @@ impl DocumentMetadata {
|
||||
self.visual_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| match click_target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
|
||||
ClickTargetType::CompoundPath(subpaths) => subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.loose_bounding_box_with_transform(transform))
|
||||
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]),
|
||||
ClickTargetType::Path(path) => {
|
||||
let mut transformed = path.clone();
|
||||
transformed.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
let control_box = transformed.control_box();
|
||||
(!transformed.is_empty()).then(|| [DVec2::new(control_box.min_x(), control_box.min_y()), DVec2::new(control_box.max_x(), control_box.max_y())])
|
||||
}
|
||||
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
@@ -233,10 +235,15 @@ impl DocumentMetadata {
|
||||
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
|
||||
/// bounds for non-vector layers (groups, raster, text, color, gradient).
|
||||
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer)
|
||||
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer))
|
||||
{
|
||||
return Some(bounds);
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer) {
|
||||
let stroke = self
|
||||
.layer_appearance_attributes
|
||||
.get(&layer)
|
||||
.and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke))
|
||||
.map(graphene_std::Coverage::stroke_params);
|
||||
if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) {
|
||||
return Some(bounds);
|
||||
}
|
||||
}
|
||||
self.bounding_box_document(layer)
|
||||
}
|
||||
@@ -251,11 +258,10 @@ 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 = &subpath::Subpath<PointId>> {
|
||||
self.visual_targets(layer).unwrap_or(&[]).iter().flat_map(|target| match target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => std::slice::from_ref(subpath),
|
||||
ClickTargetType::CompoundPath(subpaths) => subpaths.as_slice(),
|
||||
ClickTargetType::FreePoint(_) => &[],
|
||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &BezPath> {
|
||||
self.visual_targets(layer).unwrap_or(&[]).iter().filter_map(|target| match target.target_type() {
|
||||
ClickTargetType::Path(path) => Some(path),
|
||||
ClickTargetType::FreePoint(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,6 @@ pub mod error;
|
||||
pub mod misc;
|
||||
pub mod network_interface;
|
||||
pub mod nodes;
|
||||
pub mod text_metrics;
|
||||
pub mod transformation;
|
||||
pub mod wires;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+403
@@ -0,0 +1,403 @@
|
||||
use super::{InputConnector, OutputConnector, Previewing, RootNode, TransactionStatus};
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::uuid::NodeId;
|
||||
|
||||
fn assert_invariants(editor: &EditorTestUtils, context: &str) {
|
||||
let violations = editor.active_document().network_interface.validate_invariants();
|
||||
assert!(violations.is_empty(), "Invariant violations {context}:\n{}", violations.join("\n"));
|
||||
}
|
||||
|
||||
fn rectangle_definition() -> DefinitionIdentifier {
|
||||
DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER)
|
||||
}
|
||||
|
||||
fn new_artboard_message(id: NodeId) -> GraphOperationMessage {
|
||||
GraphOperationMessage::NewArtboard {
|
||||
id,
|
||||
location: DVec2::ZERO,
|
||||
dimensions: DVec2::new(400., 300.),
|
||||
background: Color::WHITE,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invariants_hold_through_basic_editing_flow() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
// This test covers the interface's structural invariants. The storage dual-write is out of its scope and asserted by the storage round-trip tests.
|
||||
editor.editor.handle_message(PreferencesMessage::ValidateStorageRoundTrip { enabled: false });
|
||||
|
||||
editor.new_document().await;
|
||||
assert_invariants(&editor, "after opening a new document");
|
||||
|
||||
editor.handle_message(new_artboard_message(NodeId::new())).await;
|
||||
assert_invariants(&editor, "after creating an artboard");
|
||||
|
||||
let rectangle = editor.create_node_by_name(rectangle_definition()).await;
|
||||
assert_invariants(&editor, "after creating a node");
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rectangle] }).await;
|
||||
editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }).await;
|
||||
assert_invariants(&editor, "after deleting the node");
|
||||
|
||||
editor.handle_message(DocumentMessage::Undo).await;
|
||||
assert_invariants(&editor, "after undo");
|
||||
|
||||
editor.handle_message(DocumentMessage::Redo).await;
|
||||
assert_invariants(&editor, "after redo");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_node_with_children_prunes_them_from_the_selection() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let parent = editor.create_node_by_name(rectangle_definition()).await;
|
||||
let child = editor.create_node_by_name(rectangle_definition()).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
assert!(network_interface.number_of_inputs(&parent, &[]) >= 2, "Test needs a secondary input to wire the child into");
|
||||
|
||||
// Wire the child into the parent's secondary input so it is a sole dependent, then select both and delete only the parent
|
||||
network_interface.set_input(&InputConnector::node_at_index(parent, 1), NodeInput::node(child, 0), &[]);
|
||||
network_interface.selected_nodes_mut(&[]).unwrap().set_selected_nodes(vec![parent, child]);
|
||||
network_interface.delete_nodes(vec![parent], true, &[]);
|
||||
|
||||
assert!(network_interface.document_network().nodes.is_empty(), "Both the parent and its sole-dependent child should be deleted");
|
||||
let remaining_selection = network_interface.selected_nodes_mut(&[]).unwrap().selected_nodes().copied().collect::<Vec<_>>();
|
||||
assert!(remaining_selection.is_empty(), "Deleted children should be pruned from the selection, found {remaining_selection:?}");
|
||||
|
||||
assert_invariants(&editor, "after deleting a node with children");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_node_keeps_children_shared_with_other_nodes() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let parent = editor.create_node_by_name(rectangle_definition()).await;
|
||||
let sibling = editor.create_node_by_name(rectangle_definition()).await;
|
||||
let shared_child = editor.create_node_by_name(rectangle_definition()).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
|
||||
// Wire the same child into the secondary inputs of both nodes, then delete only the parent along with its children
|
||||
network_interface.set_input(&InputConnector::node_at_index(parent, 1), NodeInput::node(shared_child, 0), &[]);
|
||||
network_interface.set_input(&InputConnector::node_at_index(sibling, 1), NodeInput::node(shared_child, 0), &[]);
|
||||
network_interface.delete_nodes(vec![parent], true, &[]);
|
||||
|
||||
let nodes = &network_interface.document_network().nodes;
|
||||
assert!(!nodes.contains_key(&parent), "The deleted node itself should be gone");
|
||||
assert!(nodes.contains_key(&shared_child), "A child shared with another node is not a sole dependent and should survive");
|
||||
assert!(nodes.contains_key(&sibling), "The unrelated sibling should survive");
|
||||
|
||||
assert_invariants(&editor, "after deleting a node with a shared child");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cyclic_connection_is_rejected_without_side_effects() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let a = editor.create_node_by_name(rectangle_definition()).await;
|
||||
let b = editor.create_node_by_name(rectangle_definition()).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.set_input(&InputConnector::node_at_index(a, 1), NodeInput::node(b, 0), &[]);
|
||||
|
||||
// Attempt to complete a cycle inside a transaction: the edit must be rejected without marking the transaction as modified
|
||||
network_interface.start_transaction();
|
||||
let input_before = network_interface.input_from_connector(&InputConnector::node_at_index(b, 1), &[]).cloned();
|
||||
network_interface.set_input(&InputConnector::node_at_index(b, 1), NodeInput::node(a, 0), &[]);
|
||||
|
||||
let input_after = network_interface.input_from_connector(&InputConnector::node_at_index(b, 1), &[]).cloned();
|
||||
assert_eq!(input_before, input_after, "A rejected cyclic connection should leave the input unchanged");
|
||||
assert_eq!(
|
||||
network_interface.transaction_status(),
|
||||
TransactionStatus::Started,
|
||||
"A rejected cyclic connection should not mark the transaction as modified"
|
||||
);
|
||||
network_interface.finish_transaction();
|
||||
|
||||
assert_invariants(&editor, "after rejecting a cyclic connection");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toggling_preview_on_a_disconnected_export() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let node = editor.create_node_by_name(rectangle_definition()).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.disconnect_input(&InputConnector::Export(0), &[]);
|
||||
|
||||
// Previewing a node while the export is disconnected is a preview with nothing to restore
|
||||
network_interface.toggle_preview(node, &[]);
|
||||
assert_eq!(network_interface.previewing(&[]), Previewing::Yes { root_node_to_restore: None });
|
||||
let export = network_interface.input_from_connector(&InputConnector::Export(0), &[]);
|
||||
assert_eq!(export.and_then(|input| input.as_node()), Some(node), "The previewed node should be wired to the export");
|
||||
|
||||
// Ending the preview restores the disconnected export
|
||||
network_interface.toggle_preview(node, &[]);
|
||||
assert_eq!(network_interface.previewing(&[]), Previewing::No);
|
||||
let export = network_interface.input_from_connector(&InputConnector::Export(0), &[]);
|
||||
assert!(export.is_some_and(|input| input.as_node().is_none()), "Ending the preview should disconnect the export again");
|
||||
|
||||
assert_invariants(&editor, "after toggling preview twice");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_identity_is_independent_of_scene_connectivity() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let artboard = NodeId::new();
|
||||
editor.handle_message(new_artboard_message(artboard)).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
assert!(network_interface.is_artboard(&artboard, &[]));
|
||||
assert!(!network_interface.all_artboards().is_empty());
|
||||
|
||||
// Disconnecting the artboard from the export keeps its identity but removes it from the scene's artboards
|
||||
network_interface.disconnect_input(&InputConnector::Export(0), &[]);
|
||||
assert!(network_interface.is_artboard(&artboard, &[]), "Artboard identity should survive disconnection");
|
||||
assert!(network_interface.all_artboards().is_empty(), "Disconnected artboards should not count as scene artboards");
|
||||
|
||||
assert_invariants(&editor, "after disconnecting an artboard");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn selection_history_is_not_serialized() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let node = editor.create_node_by_name(rectangle_definition()).await;
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![node] }).await;
|
||||
|
||||
let serialized = editor.active_document().serialize_document();
|
||||
assert!(!serialized.contains("selection_undo_history"), "Selection history should not be persisted into saved documents");
|
||||
assert!(!serialized.contains("selection_redo_history"), "Selection history should not be persisted into saved documents");
|
||||
}
|
||||
|
||||
fn merge_definition() -> DefinitionIdentifier {
|
||||
DefinitionIdentifier::Network("Merge".to_string())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn layer_stacking_follows_wiring() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let upper = editor.create_node_by_name_at(merge_definition(), 20, 10).await;
|
||||
let lower = editor.create_node_by_name_at(merge_definition(), 20, 16).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.set_to_node_or_layer(&upper, &[], true);
|
||||
network_interface.set_to_node_or_layer(&lower, &[], true);
|
||||
assert!(network_interface.is_layer(&upper, &[]) && network_interface.is_layer(&lower, &[]));
|
||||
assert!(network_interface.is_absolute(&lower, &[]));
|
||||
|
||||
// Wiring a layer into the bottom input of another layer converts it to stack positioning at its current visual spot
|
||||
let lower_position_before = network_interface.position(&lower, &[]).expect("Lower layer should have a position");
|
||||
network_interface.create_wire(&OutputConnector::primary_output(lower), &InputConnector::primary_input(upper), &[]);
|
||||
assert!(network_interface.is_stack(&lower, &[]), "A layer feeding the bottom of a layer should be stack positioned");
|
||||
let stacked_position = network_interface.position(&lower, &[]).expect("Stacked layer should have a position");
|
||||
assert_eq!(stacked_position.y, lower_position_before.y, "Stacking should preserve the layer's vertical position");
|
||||
|
||||
// Disconnecting converts the layer back to absolute positioning without moving it
|
||||
network_interface.disconnect_input(&InputConnector::primary_input(upper), &[]);
|
||||
assert!(network_interface.is_absolute(&lower, &[]), "A disconnected stack layer should return to absolute positioning");
|
||||
assert_eq!(network_interface.position(&lower, &[]), Some(stacked_position), "Unstacking should not move the layer");
|
||||
|
||||
assert_invariants(&editor, "after stacking and unstacking a layer");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chain_membership_follows_wiring() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let layer = editor.create_node_by_name_at(merge_definition(), 20, 10).await;
|
||||
let node = editor.create_node_by_name_at(rectangle_definition(), 15, 10).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.set_to_node_or_layer(&layer, &[], true);
|
||||
|
||||
// A node wired into a layer's secondary input from the same row, within chain distance, joins the chain
|
||||
network_interface.create_wire(&OutputConnector::primary_output(node), &InputConnector::layer_secondary_input(layer), &[]);
|
||||
assert!(
|
||||
network_interface.is_chain(&node, &[]),
|
||||
"A node feeding a layer's secondary input from chain range should become a chain node"
|
||||
);
|
||||
|
||||
// Disconnecting breaks the chain and the node becomes absolute at its chain spot
|
||||
let chained_y = network_interface.position(&node, &[]).expect("Chained node should have a position").y;
|
||||
network_interface.disconnect_input(&InputConnector::layer_secondary_input(layer), &[]);
|
||||
assert!(!network_interface.is_chain(&node, &[]), "Disconnecting should break the chain");
|
||||
assert!(network_interface.is_absolute(&node, &[]));
|
||||
assert_eq!(network_interface.position(&node, &[]).map(|position| position.y), Some(chained_y));
|
||||
|
||||
assert_invariants(&editor, "after forming and breaking a chain");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_layer_to_stack_builds_the_layer_stack() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let artboard = NodeId::new();
|
||||
editor.handle_message(new_artboard_message(artboard)).await;
|
||||
|
||||
let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await;
|
||||
let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.set_to_node_or_layer(&first, &[], true);
|
||||
network_interface.set_to_node_or_layer(&second, &[], true);
|
||||
|
||||
let artboard_layer = LayerNodeIdentifier::new(artboard, network_interface);
|
||||
let first_layer = LayerNodeIdentifier::new(first, network_interface);
|
||||
let second_layer = LayerNodeIdentifier::new(second, network_interface);
|
||||
|
||||
network_interface.move_layer_to_stack(first_layer, artboard_layer, 0, &[]);
|
||||
let children = artboard_layer.children(network_interface.document_metadata()).collect::<Vec<_>>();
|
||||
assert_eq!(children, vec![first_layer], "The first moved layer should become the artboard's only child");
|
||||
|
||||
network_interface.move_layer_to_stack(second_layer, artboard_layer, 1, &[]);
|
||||
let children = artboard_layer.children(network_interface.document_metadata()).collect::<Vec<_>>();
|
||||
assert_eq!(children, vec![first_layer, second_layer], "The second layer should be inserted below the first");
|
||||
assert!(network_interface.is_stack(&second, &[]), "A layer below a sibling should be stack positioned");
|
||||
|
||||
assert_invariants(&editor, "after moving two layers into an artboard");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drag_offsets_do_not_outlive_programmatic_shifts() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let artboard = NodeId::new();
|
||||
editor.handle_message(new_artboard_message(artboard)).await;
|
||||
|
||||
let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await;
|
||||
let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await;
|
||||
let third = editor.create_node_by_name_at(merge_definition(), 0, 50).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.set_to_node_or_layer(&first, &[], true);
|
||||
network_interface.set_to_node_or_layer(&second, &[], true);
|
||||
network_interface.set_to_node_or_layer(&third, &[], true);
|
||||
|
||||
let artboard_layer = LayerNodeIdentifier::new(artboard, network_interface);
|
||||
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(first, network_interface), artboard_layer, 0, &[]);
|
||||
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(second, network_interface), artboard_layer, 1, &[]);
|
||||
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(third, network_interface), artboard_layer, 2, &[]);
|
||||
|
||||
// Making room while inserting into the stack pushes neighbors with drag-offset bookkeeping that must not outlive the operation
|
||||
for node_id in [first, second, third] {
|
||||
assert_eq!(network_interface.drag_offset(&node_id, &[]), 0, "Inserting into a stack should not leave a drag offset on {node_id}");
|
||||
}
|
||||
|
||||
// Deleting the middle layer collapses the stack upward, which must also leave no offsets behind
|
||||
network_interface.delete_nodes(vec![second], false, &[]);
|
||||
assert_eq!(network_interface.drag_offset(&third, &[]), 0, "Collapsing the deleted layer's space should not leave a drag offset");
|
||||
|
||||
// A later nudge must move the survivor by exactly one unit, without replaying any restore toward its pre-collapse position
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![third] }).await;
|
||||
let network_interface = &editor.active_document().network_interface;
|
||||
let before = network_interface.position(&third, &[]).expect("Surviving layer should have a position");
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::ShiftSelectedNodes {
|
||||
direction: Direction::Down,
|
||||
rubber_band: false,
|
||||
})
|
||||
.await;
|
||||
let network_interface = &editor.active_document().network_interface;
|
||||
let after = network_interface.position(&third, &[]).expect("Surviving layer should have a position");
|
||||
assert_eq!(after.y - before.y, 1, "A single nudge should move the layer exactly one unit");
|
||||
|
||||
assert_invariants(&editor, "after stack pushes, a delete collapse, and a nudge");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signature_edits_keep_parallel_metadata_in_sync() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let merge = editor.create_node_by_name_at(merge_definition(), 0, 0).await;
|
||||
let path = vec![merge];
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
let initial_imports = network_interface.number_of_imports(&path);
|
||||
let initial_exports = network_interface.number_of_exports(&path);
|
||||
|
||||
network_interface.add_import(TaggedValue::None, true, -1, "Extra import", "", &path);
|
||||
assert_eq!(network_interface.number_of_imports(&path), initial_imports + 1);
|
||||
assert_invariants(&editor, "after adding an import");
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.add_export(TaggedValue::None, -1, "Extra export", &path);
|
||||
assert_eq!(network_interface.number_of_exports(&path), initial_exports + 1);
|
||||
assert_invariants(&editor, "after adding an export");
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.reorder_import(initial_imports, 0, &path);
|
||||
assert_invariants(&editor, "after reordering an import to the front");
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.remove_import(0, &path);
|
||||
assert_eq!(network_interface.number_of_imports(&path), initial_imports);
|
||||
assert_invariants(&editor, "after removing the reordered import");
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
network_interface.remove_export(initial_exports, &path);
|
||||
assert_eq!(network_interface.number_of_exports(&path), initial_exports);
|
||||
assert_invariants(&editor, "after removing the added export");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toggle_preview_transitions_with_a_connected_export() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let artboard = NodeId::new();
|
||||
editor.handle_message(new_artboard_message(artboard)).await;
|
||||
let node = editor.create_node_by_name_at(rectangle_definition(), 0, 20).await;
|
||||
|
||||
let network_interface = &mut editor.active_document_mut().network_interface;
|
||||
let export_node = |network_interface: &super::NodeNetworkInterface| network_interface.input_from_connector(&InputConnector::Export(0), &[]).and_then(|input| input.as_node());
|
||||
assert_eq!(export_node(network_interface), Some(artboard));
|
||||
|
||||
// Previewing a node remembers the artboard as the connection to restore
|
||||
network_interface.toggle_preview(node, &[]);
|
||||
assert_eq!(export_node(network_interface), Some(node));
|
||||
assert_eq!(
|
||||
network_interface.previewing(&[]),
|
||||
Previewing::Yes {
|
||||
root_node_to_restore: Some(RootNode { node_id: artboard, output_index: 0 })
|
||||
}
|
||||
);
|
||||
|
||||
// Toggling the previewed node again restores the artboard connection
|
||||
network_interface.toggle_preview(node, &[]);
|
||||
assert_eq!(export_node(network_interface), Some(artboard));
|
||||
assert_eq!(network_interface.previewing(&[]), Previewing::No);
|
||||
|
||||
// Toggling the restore node while previewing promotes it to the export with nothing left to restore
|
||||
network_interface.toggle_preview(node, &[]);
|
||||
network_interface.toggle_preview(artboard, &[]);
|
||||
assert_eq!(export_node(network_interface), Some(artboard));
|
||||
assert_eq!(network_interface.previewing(&[]), Previewing::Yes { root_node_to_restore: None });
|
||||
|
||||
// Toggling it once more ends the preview by disconnecting the export entirely
|
||||
network_interface.toggle_preview(artboard, &[]);
|
||||
assert_eq!(export_node(network_interface), None);
|
||||
assert_eq!(network_interface.previewing(&[]), Previewing::No);
|
||||
|
||||
assert_invariants(&editor, "after cycling through the preview states");
|
||||
}
|
||||
-1
@@ -72,7 +72,6 @@ impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersisten
|
||||
input_name: properties_row.input_name,
|
||||
input_description: properties_row.input_description,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
DocumentNodePersistentMetadataHasPrimaryOutput {
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
use super::*;
|
||||
|
||||
// Helper functions for mutable getters
|
||||
impl NodeNetworkInterface {
|
||||
pub fn upstream_chain_nodes(&self, network_path: &[NodeId]) -> Vec<NodeId> {
|
||||
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
||||
log::error!("Could not get selected nodes in upstream_chain_nodes");
|
||||
return Vec::new();
|
||||
};
|
||||
let mut all_selected_nodes = selected_nodes.selected_nodes().cloned().collect::<Vec<_>>();
|
||||
for selected_node_id in selected_nodes.selected_nodes() {
|
||||
if self.is_layer(selected_node_id, network_path) {
|
||||
let unique_upstream_chain = self
|
||||
.upstream_flow_back_from_nodes(vec![*selected_node_id], network_path, FlowType::HorizontalFlow)
|
||||
.skip(1)
|
||||
.take_while(|node_id| self.is_chain(node_id, network_path))
|
||||
.filter(|upstream_node| all_selected_nodes.iter().all(|new_selected_node| new_selected_node != upstream_node))
|
||||
.collect::<Vec<_>>();
|
||||
all_selected_nodes.extend(unique_upstream_chain);
|
||||
}
|
||||
}
|
||||
all_selected_nodes
|
||||
}
|
||||
|
||||
pub fn collect_frontend_click_targets(&self, network_path: &[NodeId]) -> FrontendClickTargets {
|
||||
let mut all_node_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");
|
||||
return FrontendClickTargets::default();
|
||||
};
|
||||
let nodes = network_metadata.persistent_metadata.node_metadata.keys().copied().collect::<Vec<_>>();
|
||||
self.with_import_export_ports(network_path, |import_export_click_targets| {
|
||||
for port in import_export_click_targets.click_targets() {
|
||||
if let ClickTargetType::Path(path) = port.target_type() {
|
||||
connector_click_targets.push(path.to_svg());
|
||||
}
|
||||
}
|
||||
});
|
||||
nodes.into_iter().for_each(|node_id| {
|
||||
self.with_node_click_targets(&node_id, network_path, |node_click_targets| {
|
||||
let mut node_path = String::new();
|
||||
|
||||
if let ClickTargetType::Path(path) = node_click_targets.node_click_target.target_type() {
|
||||
node_path.push_str(path.to_svg().as_str())
|
||||
}
|
||||
all_node_click_targets.push((node_id, node_path));
|
||||
for port in node_click_targets.port_click_targets.click_targets() {
|
||||
if let ClickTargetType::Path(path) = port.target_type() {
|
||||
connector_click_targets.push(path.to_svg());
|
||||
}
|
||||
}
|
||||
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
|
||||
// Visibility button (eye icon)
|
||||
if let ClickTargetType::Path(path) = layer_metadata.visibility_click_target.target_type() {
|
||||
icon_click_targets.push(path.to_svg());
|
||||
}
|
||||
// Lock button (padlock icon), only when the layer is locked
|
||||
if let Some(lock_click_target) = &layer_metadata.lock_click_target
|
||||
&& let ClickTargetType::Path(path) = lock_click_target.target_type()
|
||||
{
|
||||
icon_click_targets.push(path.to_svg());
|
||||
}
|
||||
// Drag grip (dotted symbol)
|
||||
if let ClickTargetType::Path(path) = layer_metadata.grip_click_target.target_type() {
|
||||
icon_click_targets.push(path.to_svg());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
let mut layer_click_targets = Vec::new();
|
||||
let mut node_click_targets = Vec::new();
|
||||
all_node_click_targets.into_iter().for_each(|(node_id, path)| {
|
||||
if self.is_layer(&node_id, network_path) {
|
||||
layer_click_targets.push(path);
|
||||
} else {
|
||||
node_click_targets.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
let bounds = self.all_nodes_bounding_box(network_path).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let all_nodes_bounding_box = rectangle_path(bounds[0], bounds[1]).to_svg();
|
||||
|
||||
let mut modify_import_export = Vec::new();
|
||||
self.with_modify_import_export(network_path, |modify_import_export_click_targets| {
|
||||
for click_target in modify_import_export_click_targets
|
||||
.remove_imports_exports
|
||||
.click_targets()
|
||||
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
|
||||
{
|
||||
if let ClickTargetType::Path(path) = click_target.target_type() {
|
||||
modify_import_export.push(path.to_svg());
|
||||
}
|
||||
}
|
||||
});
|
||||
FrontendClickTargets {
|
||||
node_click_targets,
|
||||
layer_click_targets,
|
||||
connector_click_targets,
|
||||
icon_click_targets,
|
||||
all_nodes_bounding_box,
|
||||
modify_import_export,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_document_to_viewport_transform(&mut self, transform: DAffine2) {
|
||||
self.document_metadata.document_to_viewport = transform;
|
||||
}
|
||||
|
||||
pub fn is_eligible_to_be_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||
self.query(network_path, "is_eligible_to_be_layer", |view| view.is_eligible_to_be_layer(node_id)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn node_graph_ptz(&self, network_path: &[NodeId]) -> Option<&PTZ> {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in node_graph_ptz_mut");
|
||||
return None;
|
||||
};
|
||||
Some(&network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz)
|
||||
}
|
||||
|
||||
pub fn node_graph_ptz_mut(&mut self, network_path: &[NodeId]) -> Option<&mut PTZ> {
|
||||
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in node_graph_ptz_mut");
|
||||
return None;
|
||||
};
|
||||
Some(&mut network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz)
|
||||
}
|
||||
|
||||
// TODO: Optimize getting click target intersections from click by using a spacial data structure like a quadtree instead of linear search
|
||||
/// Click target getter methods
|
||||
pub fn node_from_click(&self, click: DVec2, network_path: &[NodeId]) -> Option<NodeId> {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in node_from_click");
|
||||
return None;
|
||||
};
|
||||
let Some(network) = self.nested_network(network_path) else {
|
||||
log::error!("Could not get nested network in node_from_click");
|
||||
return None;
|
||||
};
|
||||
|
||||
let point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
|
||||
let nodes = network.nodes.keys().copied().collect::<Vec<_>>();
|
||||
let clicked_nodes = nodes
|
||||
.iter()
|
||||
.filter(|node_id| {
|
||||
self.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
transient_node_metadata.node_click_target.intersect_point_no_stroke(point)
|
||||
}) == Some(true)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Since nodes are placed on top of layer chains, find the first non layer node that was clicked, and if there way no non layer nodes clicked, then find the first layer node that was clicked
|
||||
clicked_nodes
|
||||
.iter()
|
||||
.find_map(|node_id| {
|
||||
let Some(node_metadata) = self.network_metadata(network_path)?.persistent_metadata.node_metadata.get(node_id) else {
|
||||
log::error!("Could not get node_metadata for node {node_id}");
|
||||
return None;
|
||||
};
|
||||
if !node_metadata.persistent_metadata.is_layer() { Some(*node_id) } else { None }
|
||||
})
|
||||
.or_else(|| clicked_nodes.into_iter().next())
|
||||
}
|
||||
|
||||
pub fn layer_click_target_from_click(&self, click: DVec2, click_target_type: LayerClickTargetTypes, network_path: &[NodeId]) -> Option<NodeId> {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in visibility_from_click");
|
||||
return None;
|
||||
};
|
||||
let Some(network) = self.nested_network(network_path) else {
|
||||
log::error!("Could not get nested network in visibility_from_click");
|
||||
return None;
|
||||
};
|
||||
|
||||
let point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
|
||||
let node_ids: Vec<_> = network.nodes.keys().copied().collect();
|
||||
|
||||
node_ids
|
||||
.iter()
|
||||
.filter_map(|node_id| {
|
||||
self.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
if let NodeTypeClickTargets::Layer(layer) = &transient_node_metadata.node_type_metadata {
|
||||
match click_target_type {
|
||||
LayerClickTargetTypes::Visibility => layer.visibility_click_target.intersect_point_no_stroke(point).then_some(*node_id),
|
||||
LayerClickTargetTypes::Lock => layer.lock_click_target.as_ref().and_then(|target| target.intersect_point_no_stroke(point).then_some(*node_id)),
|
||||
LayerClickTargetTypes::Grip => layer.grip_click_target.intersect_point_no_stroke(point).then_some(*node_id),
|
||||
LayerClickTargetTypes::Name => layer.name_click_target.as_ref().and_then(|target| target.intersect_point_no_stroke(point).then_some(*node_id)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
pub fn input_connector_from_click(&self, click: DVec2, network_path: &[NodeId]) -> Option<InputConnector> {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in input_connector_from_click");
|
||||
return None;
|
||||
};
|
||||
let Some(network) = self.nested_network(network_path) else {
|
||||
log::error!("Could not get nested network in input_connector_from_click");
|
||||
return None;
|
||||
};
|
||||
|
||||
let point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
|
||||
network
|
||||
.nodes
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.filter_map(|node_id| {
|
||||
self.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
transient_node_metadata
|
||||
.port_click_targets
|
||||
.clicked_input_port_from_point(point)
|
||||
.map(|port| InputConnector::node_at_index(*node_id, port))
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.next()
|
||||
.or_else(|| {
|
||||
self.with_import_export_ports(network_path, |import_export_ports| import_export_ports.clicked_input_port_from_point(point).map(InputConnector::Export))
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn output_connector_from_click(&self, click: DVec2, network_path: &[NodeId]) -> Option<OutputConnector> {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in output_connector_from_click");
|
||||
return None;
|
||||
};
|
||||
let Some(network) = self.nested_network(network_path) else {
|
||||
log::error!("Could not get nested network in output_connector_from_click");
|
||||
return None;
|
||||
};
|
||||
|
||||
let point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
|
||||
let nodes = network.nodes.keys().copied().collect::<Vec<_>>();
|
||||
nodes
|
||||
.iter()
|
||||
.filter_map(|node_id| {
|
||||
self.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
transient_node_metadata
|
||||
.port_click_targets
|
||||
.clicked_output_port_from_point(point)
|
||||
.map(|output_index| OutputConnector::node(*node_id, output_index))
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.next()
|
||||
.or_else(|| {
|
||||
self.with_import_export_ports(network_path, |import_export_ports| {
|
||||
import_export_ports.clicked_output_port_from_point(point).map(OutputConnector::Import)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn input_position(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option<DVec2> {
|
||||
match input_connector {
|
||||
InputConnector::Node { node_id, input_index } => self
|
||||
.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
transient_node_metadata.port_click_targets.input_port_position(*input_index)
|
||||
})
|
||||
.flatten(),
|
||||
InputConnector::Export(export_index) => self
|
||||
.with_import_export_ports(network_path, |import_export_ports| import_export_ports.input_port_position(*export_index))
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_position(&self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option<DVec2> {
|
||||
match output_connector {
|
||||
OutputConnector::Node { node_id, output_index } => self
|
||||
.with_node_click_targets(node_id, network_path, |transient_node_metadata| {
|
||||
transient_node_metadata.port_click_targets.output_port_position(*output_index)
|
||||
})
|
||||
.flatten(),
|
||||
OutputConnector::Import(import_index) => self
|
||||
.with_import_export_ports(network_path, |import_export_ports| import_export_ports.output_port_position(*import_index))
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the combined bounding box of the click targets of the selected nodes in the node graph in viewport space
|
||||
pub fn selected_nodes_bounding_box_viewport(&self, network_path: &[NodeId]) -> Option<[DVec2; 2]> {
|
||||
// Always get the bounding box for nodes in the currently viewed network
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in selected_nodes_bounding_box_viewport");
|
||||
return None;
|
||||
};
|
||||
let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport;
|
||||
self.selected_nodes_bounding_box(network_path)
|
||||
.map(|[a, b]| [node_graph_to_viewport.transform_point2(a), node_graph_to_viewport.transform_point2(b)])
|
||||
}
|
||||
|
||||
pub fn selected_layers_artwork_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
pub fn selected_unlocked_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(node, &[]) && !self.is_locked(node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Get the combined bounding box of the click targets of the selected nodes in the node graph in layer space
|
||||
pub fn selected_nodes_bounding_box(&self, network_path: &[NodeId]) -> Option<[DVec2; 2]> {
|
||||
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
||||
log::error!("Could not get selected nodes in selected_nodes_bounding_box_viewport");
|
||||
return None;
|
||||
};
|
||||
selected_nodes
|
||||
.selected_nodes()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.filter_map(|node_id| self.node_bounding_box(node_id, network_path))
|
||||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Gets the bounding box in viewport coordinates for each node in the node graph
|
||||
pub fn graph_bounds_viewport_space(&self, network_path: &[NodeId]) -> Option<[DVec2; 2]> {
|
||||
let bounds = self.all_nodes_bounding_box(network_path)?;
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in graph_bounds_viewport_space");
|
||||
return None;
|
||||
};
|
||||
|
||||
let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport;
|
||||
Some((node_graph_to_viewport * Quad::from_box(bounds)).bounding_box())
|
||||
}
|
||||
|
||||
pub fn collect_layer_widths(&self, network_path: &[NodeId]) -> (HashMap<NodeId, u32>, HashMap<NodeId, u32>, HashMap<NodeId, bool>) {
|
||||
let Some(network_metadata) = self.network_metadata(network_path) else {
|
||||
log::error!("Could not get nested network_metadata in collect_layer_widths");
|
||||
return (HashMap::new(), HashMap::new(), HashMap::new());
|
||||
};
|
||||
let nodes = network_metadata
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.keys()
|
||||
.filter_map(|node_id| if self.is_layer(node_id, network_path) { Some(*node_id) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
let layer_widths = nodes
|
||||
.iter()
|
||||
.filter_map(|node_id| self.layer_width(node_id, network_path).map(|layer_width| (*node_id, layer_width)))
|
||||
.collect::<HashMap<NodeId, u32>>();
|
||||
let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::<HashMap<NodeId, u32>>();
|
||||
let has_left_input_wire = nodes
|
||||
.iter()
|
||||
.map(|node_id| {
|
||||
(
|
||||
*node_id,
|
||||
!self
|
||||
.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow)
|
||||
.skip(1)
|
||||
.all(|node_id| self.is_chain(&node_id, network_path)),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<NodeId, bool>>();
|
||||
|
||||
(layer_widths, chain_widths, has_left_input_wire)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+96
-52
@@ -3,12 +3,8 @@ use std::collections::{HashMap, HashSet};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
|
||||
use graph_craft::proto::{GraphErrorType, GraphErrors};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
use graph_craft::{ProtoNodeIdentifier, Type, concrete};
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::{Artboard, Graphic};
|
||||
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
|
||||
use interpreted_executor::node_registry::NODE_REGISTRY;
|
||||
|
||||
@@ -56,28 +52,38 @@ impl TypeSource {
|
||||
return FrontendGraphDataType::Invalid;
|
||||
};
|
||||
match self.compiled_nested_type() {
|
||||
Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) {
|
||||
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => {
|
||||
FrontendGraphDataType::Number
|
||||
}
|
||||
TaggedValue::Color(_) => FrontendGraphDataType::Color,
|
||||
TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient,
|
||||
TaggedValue::String(_) => FrontendGraphDataType::Typography,
|
||||
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
|
||||
TaggedValue::TypeDefault(td) => match td.name.as_ref() {
|
||||
n if n == graphene_std::core_types::normalize_type_name(std::any::type_name::<List<Graphic>>()) => FrontendGraphDataType::Graphic,
|
||||
n if n == graphene_std::core_types::normalize_type_name(std::any::type_name::<List<Artboard>>()) => FrontendGraphDataType::Artboard,
|
||||
n if n == graphene_std::core_types::normalize_type_name(std::any::type_name::<List<Raster<CPU>>>()) => FrontendGraphDataType::Raster,
|
||||
n if n == graphene_std::core_types::normalize_type_name(std::any::type_name::<List<Vector>>()) => FrontendGraphDataType::Vector,
|
||||
n if n == graphene_std::core_types::normalize_type_name(std::any::type_name::<List<String>>()) => FrontendGraphDataType::Typography,
|
||||
_ => FrontendGraphDataType::General,
|
||||
},
|
||||
_ => FrontendGraphDataType::General,
|
||||
},
|
||||
Some(nested_type) => FrontendGraphDataType::from_type(nested_type),
|
||||
None => FrontendGraphDataType::General,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the compiled type is a packed `Record` lane, as opposed to a bare rank-0 value.
|
||||
pub fn is_list(&self) -> bool {
|
||||
// `nested_type` peels `Record`, so the rank has to be read off the unpeeled type
|
||||
fn is_record(ty: &Type) -> bool {
|
||||
match ty {
|
||||
Type::Fn(_, output) | Type::Future(output) => is_record(output),
|
||||
Type::Record(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
match self {
|
||||
TypeSource::Compiled(compiled_type) => is_record(compiled_type),
|
||||
TypeSource::TaggedValue(value_type) => is_record(value_type),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The element type, so semantic type checks can be rank-agnostic.
|
||||
pub fn compiled_element_type(&self) -> Option<&Type> {
|
||||
self.compiled_nested_type()
|
||||
}
|
||||
|
||||
/// The identifier name of [`Self::compiled_element_type`].
|
||||
pub fn compiled_element_name(&self) -> Option<String> {
|
||||
Some(self.compiled_element_type()?.identifier_name())
|
||||
}
|
||||
|
||||
pub fn compiled_nested_type(&self) -> Option<&Type> {
|
||||
match self {
|
||||
TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()),
|
||||
@@ -115,7 +121,7 @@ impl TypeSource {
|
||||
}
|
||||
|
||||
impl NodeNetworkInterface {
|
||||
fn input_has_error(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> bool {
|
||||
fn input_has_error(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> bool {
|
||||
match input_connector {
|
||||
InputConnector::Node { node_id, input_index } => {
|
||||
let Some(implementation) = self.implementation(node_id, network_path) else {
|
||||
@@ -125,10 +131,10 @@ impl NodeNetworkInterface {
|
||||
let node_path = [network_path, &[*node_id]].concat();
|
||||
match implementation {
|
||||
DocumentNodeImplementation::Network(_) => {
|
||||
let Some(map) = self.outward_wires(&node_path) else { return false };
|
||||
let Some(outward_wires) = map.get(&OutputConnector::Import(*input_index)) else { return false };
|
||||
outward_wires.clone().iter().any(|connector| match connector {
|
||||
InputConnector::Node { node_id, input_index } => self.input_has_error(&InputConnector::node(*node_id, *input_index), &node_path),
|
||||
let outward_wires = self.with_outward_wires(&node_path, |map| map.get(&OutputConnector::Import(*input_index)).cloned()).flatten();
|
||||
let Some(outward_wires) = outward_wires else { return false };
|
||||
outward_wires.iter().any(|connector| match connector {
|
||||
InputConnector::Node { node_id, input_index } => self.input_has_error(&InputConnector::node_at_index(*node_id, *input_index), &node_path),
|
||||
InputConnector::Export(_) => false,
|
||||
})
|
||||
}
|
||||
@@ -147,7 +153,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_type_not_invalid(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
pub fn input_type_not_invalid(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
let Some(input) = self.input_from_connector(input_connector, network_path) else {
|
||||
return TypeSource::Error("Could not get input from connector");
|
||||
};
|
||||
@@ -165,7 +171,7 @@ impl NodeNetworkInterface {
|
||||
let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else {
|
||||
return TypeSource::Error("Could not get type of import in document network since it has no imports");
|
||||
};
|
||||
self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path)
|
||||
self.input_type(&InputConnector::node_at_index(*encapsulating_node, *import_index), encapsulating_path)
|
||||
}
|
||||
NodeInput::Scope(_) => TypeSource::Compiled(concrete!(())),
|
||||
NodeInput::Reflection(document_node_metadata) => TypeSource::Compiled(document_node_metadata.ty()),
|
||||
@@ -173,9 +179,42 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given node has a registered implementation accepting the layer chain's element type as its content input.
|
||||
/// A chain awaiting compilation has no resolved type yet, so only a known-wrong type or a type error locks the layer out.
|
||||
pub fn layer_chain_hosts_node(&self, node_id: &NodeId, network_path: &[NodeId], node: &ProtoNodeIdentifier) -> bool {
|
||||
let secondary_input = InputConnector::layer_secondary_input(*node_id);
|
||||
if !self.input_from_connector(&secondary_input, network_path).is_some_and(|input| input.is_exposed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let chain_type = self.input_type(&secondary_input, network_path);
|
||||
match chain_type.compiled_element_type() {
|
||||
Some(element) => {
|
||||
let Some(implementations) = NODE_REGISTRY.get(node) else {
|
||||
log::error!("Proto node {node:?} not found in the node registry, in layer_chain_hosts_node");
|
||||
return false;
|
||||
};
|
||||
implementations.keys().any(|node_io| node_io.inputs.first().is_some_and(|content| content.nested_type() == element))
|
||||
}
|
||||
None => !matches!(chain_type, TypeSource::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the blending nodes (blend mode, opacity, clipping mask) can be spliced into this layer's chain.
|
||||
pub fn layer_hosts_blending_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||
// Blend Mode stands in for the trio since they share one implementations list
|
||||
self.layer_chain_hosts_node(node_id, network_path, &graphene_std::blending_nodes::blend_mode::IDENTIFIER)
|
||||
}
|
||||
|
||||
/// Whether the Fill and Stroke nodes can be spliced into this layer's chain.
|
||||
pub fn layer_hosts_paint_nodes(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||
// Fill stands in for both since they share one implementations list
|
||||
self.layer_chain_hosts_node(node_id, network_path, &graphene_std::vector_nodes::fill::IDENTIFIER)
|
||||
}
|
||||
|
||||
/// Get the [`TypeSource`] for any InputConnector.
|
||||
/// If the input is not compiled, then an Unknown or default from the definition is returned.
|
||||
pub fn input_type(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
pub fn input_type(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
// First check if there is an error with this node or any protonodes it is connected to
|
||||
if self.input_has_error(input_connector, network_path) {
|
||||
return TypeSource::Invalid;
|
||||
@@ -184,7 +223,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
/// Gets the default tagged value for an input. If its not compiled, then it tries to get a valid type. If there are no valid types, then it picks a random implementation.
|
||||
pub fn tagged_value_from_input(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue {
|
||||
pub fn tagged_value_from_input(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> TaggedValue {
|
||||
let guaranteed_type = match self.input_type(input_connector, network_path) {
|
||||
TypeSource::Compiled(compiled) => compiled,
|
||||
TypeSource::TaggedValue(value) => value,
|
||||
@@ -206,11 +245,13 @@ impl NodeNetworkInterface {
|
||||
concrete!(())
|
||||
}
|
||||
};
|
||||
|
||||
// `TaggedValue::from_type` recurses through `Record` to the element, so a record default already drops to rank 0
|
||||
TaggedValue::from_type_or_none(&guaranteed_type)
|
||||
}
|
||||
|
||||
/// A list of all valid input types for this specific node.
|
||||
pub fn potential_valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
pub fn potential_valid_input_types(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
let InputConnector::Node { node_id, input_index } = input_connector else {
|
||||
// An export can have any type connected to it
|
||||
return vec![graph_craft::generic!(T)];
|
||||
@@ -222,17 +263,15 @@ impl NodeNetworkInterface {
|
||||
match implementation {
|
||||
DocumentNodeImplementation::Network(_) => {
|
||||
let nested_path = [network_path, &[*node_id]].concat();
|
||||
let Some(outward_wires) = self.outward_wires(&nested_path) else {
|
||||
log::error!("Could not get outward wires in potential_valid_input_types");
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else {
|
||||
let inputs_from_import = self
|
||||
.with_outward_wires(&nested_path, |outward_wires| outward_wires.get(&OutputConnector::Import(*input_index)).cloned())
|
||||
.flatten();
|
||||
let Some(inputs_from_import) = inputs_from_import else {
|
||||
log::error!("Could not get inputs from import in potential_valid_input_types");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let intersection: HashSet<Type> = inputs_from_import
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|input_connector| self.potential_valid_input_types(input_connector, &nested_path).into_iter().collect::<HashSet<_>>())
|
||||
.fold(None, |acc: Option<HashSet<Type>>, set| match acc {
|
||||
@@ -258,7 +297,7 @@ impl NodeNetworkInterface {
|
||||
let node_io = &entry.io;
|
||||
// Check if this NodeIOTypes implementation is valid for the other inputs
|
||||
let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| {
|
||||
let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path);
|
||||
let input_type = self.input_type_not_invalid(&InputConnector::node_at_index(*node_id, iterator_index), network_path);
|
||||
// TODO: Fix type checking for different call arguments
|
||||
// For example a node input of (Footprint) -> Vector would not be compatible with a node that is called with () and returns Vector
|
||||
node_io.inputs.get(iterator_index).map(|ty| ty.nested_type()) == input_type.compiled_nested_type()
|
||||
@@ -277,7 +316,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
/// Performs a downstream traversal to ensure input type will work in the full context of the graph.
|
||||
pub fn complete_valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
pub fn complete_valid_input_types(&self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
match input_connector {
|
||||
InputConnector::Node { node_id, input_index } => {
|
||||
let Some(implementation) = self.implementation(node_id, network_path) else {
|
||||
@@ -291,7 +330,7 @@ impl NodeNetworkInterface {
|
||||
log::error!("Protonode {proto_node_identifier:?} not found in registry in complete_valid_input_types");
|
||||
return Vec::new();
|
||||
};
|
||||
let valid_output_types = self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path);
|
||||
let valid_output_types = self.valid_output_types(&OutputConnector::primary_output(*node_id), network_path);
|
||||
|
||||
implementations
|
||||
.iter()
|
||||
@@ -302,7 +341,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
let valid_inputs = (0..node_io.inputs.len()).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| {
|
||||
let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path);
|
||||
let input_type = self.input_type_not_invalid(&InputConnector::node_at_index(*node_id, iterator_index), network_path);
|
||||
match input_type.compiled_nested_type() {
|
||||
Some(input_type) => node_io.inputs.get(iterator_index).is_some_and(|node_io_input_type| node_io_input_type.nested_type() == input_type),
|
||||
None => true,
|
||||
@@ -332,15 +371,24 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
pub fn output_type(&self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||
match output_connector {
|
||||
OutputConnector::Node { node_id, output_index } => {
|
||||
// A hidden node is replaced by a passthrough during flattening, so its output carries its primary input's type
|
||||
if *output_index == 0 && !self.is_visible(node_id, network_path) {
|
||||
return self.input_type(&InputConnector::primary_input(*node_id), network_path);
|
||||
}
|
||||
|
||||
// First try iterating upstream to the first protonode and try get its compiled type
|
||||
let Some(implementation) = self.implementation(node_id, network_path) else {
|
||||
return TypeSource::Error("Could not get implementation");
|
||||
};
|
||||
match implementation {
|
||||
DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()),
|
||||
// The compiler removes passthrough nodes so they resolve no type of their own, but their output carries their primary input's type
|
||||
DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::ops::passthrough::IDENTIFIER => {
|
||||
self.input_type(&InputConnector::primary_input(*node_id), network_path)
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) {
|
||||
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
|
||||
None => TypeSource::Unknown,
|
||||
@@ -352,7 +400,7 @@ impl NodeNetworkInterface {
|
||||
let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else {
|
||||
return TypeSource::Error("Cannot get import type in document network since it has no imports");
|
||||
};
|
||||
let mut input_type = self.input_type(&InputConnector::node(*encapsulating_node, *import_index), encapsulating_path);
|
||||
let mut input_type = self.input_type(&InputConnector::node_at_index(*encapsulating_node, *import_index), encapsulating_path);
|
||||
if matches!(input_type, TypeSource::Invalid) {
|
||||
input_type = TypeSource::Unknown
|
||||
}
|
||||
@@ -362,18 +410,14 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
/// The valid output types are all types that are valid for each downstream connection.
|
||||
fn valid_output_types(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
let Some(outward_wires) = self.outward_wires(network_path) else {
|
||||
log::error!("Could not get outward wires in valid_output_types");
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(inputs_from_import) = outward_wires.get(output_connector) else {
|
||||
fn valid_output_types(&self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Vec<Type> {
|
||||
let inputs_from_import = self.with_outward_wires(network_path, |outward_wires| outward_wires.get(output_connector).cloned()).flatten();
|
||||
let Some(inputs_from_import) = inputs_from_import else {
|
||||
log::error!("Could not get inputs from import in valid_output_types");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let intersection = inputs_from_import
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|input_connector| self.potential_valid_input_types(input_connector, network_path).into_iter().collect::<HashSet<_>>())
|
||||
.fold(None, |acc: Option<HashSet<Type>>, set| match acc {
|
||||
|
||||
-1
@@ -387,6 +387,5 @@ fn input_metadata_entry_to_runtime(entry: InputMetadataEntry) -> InputMetadata {
|
||||
widget_override: entry.widget_override,
|
||||
input_data: entry.input_data,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::*;
|
||||
|
||||
impl NodeNetworkInterface {
|
||||
pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option<Vector> {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self);
|
||||
|
||||
if let Some(path_node) = graph_layer.upstream_visible_node_id_from_name_in_layer(&DefinitionIdentifier::Network("Path".into()))
|
||||
&& let Some(vector) = self.document_metadata.vector_modify.get(&path_node)
|
||||
{
|
||||
let mut modified = vector.clone();
|
||||
|
||||
let path_node = self.document_network().nodes.get(&path_node);
|
||||
let modification_input = path_node.and_then(|node: &DocumentNode| node.inputs.get(1)).and_then(|input| input.as_value());
|
||||
if let Some(TaggedValue::VectorModification(modification)) = modification_input {
|
||||
modification.apply(&mut modified);
|
||||
}
|
||||
return Some(modified);
|
||||
}
|
||||
|
||||
self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone())
|
||||
}
|
||||
|
||||
/// The vector geometry an upstream Path node would surface for editing.
|
||||
/// This is the result of `compute_modified_vector`, but only if a visible 'Path' node is actually upstream.
|
||||
/// Useful for tool overlays and snap target collection usages that want to match the Path tool's view
|
||||
/// (e.g. the pre-solidified centerline for a Solidify Stroke layer) and otherwise do nothing.
|
||||
pub fn upstream_path_node_vector(&self, layer: LayerNodeIdentifier) -> Option<Vector> {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self);
|
||||
graph_layer.upstream_visible_node_id_from_name_in_layer(&DefinitionIdentifier::Network("Path".into()))?;
|
||||
self.compute_modified_vector(layer)
|
||||
}
|
||||
|
||||
/// Outline targets for the Select tool's hover/selection overlay, mirroring the Path tool's view.
|
||||
/// Returns `Some` when an upstream Path node exists so the outline matches what the Path tool edits
|
||||
/// (e.g. the pre-solidified centerline for a Solidify Stroke layer); returns `None` otherwise so the
|
||||
/// caller can fall back to the layer's recorded `outlines`/`click_targets`.
|
||||
pub fn path_aware_outline_targets(&self, layer: LayerNodeIdentifier) -> Option<Vec<ClickTargetType>> {
|
||||
let vector = self.upstream_path_node_vector(layer)?;
|
||||
|
||||
let mut targets = Vec::new();
|
||||
let mut combined = BezPath::new();
|
||||
for bezpath in vector.stroke_bezpath_iter() {
|
||||
combined.extend(bezpath.elements().iter().copied());
|
||||
}
|
||||
if !combined.is_empty() {
|
||||
targets.push(ClickTargetType::Path(combined));
|
||||
}
|
||||
|
||||
for &point_id in vector.point_domain.ids() {
|
||||
if !vector.any_connected(point_id) {
|
||||
let position = vector.point_domain.position_from_id(point_id).unwrap_or_default();
|
||||
targets.push(ClickTargetType::FreePoint(FreePoint::new(point_id, position)));
|
||||
}
|
||||
}
|
||||
|
||||
Some(targets)
|
||||
}
|
||||
|
||||
/// Loads the structure of layer nodes from a node graph.
|
||||
pub fn load_structure(&mut self) {
|
||||
self.document_metadata.structure = HashMap::from_iter([(LayerNodeIdentifier::ROOT_PARENT, NodeRelations::default())]);
|
||||
|
||||
// Only load structure if there is a root node
|
||||
let Some(root_node) = self.root_node(&[]) else { return };
|
||||
|
||||
let Some(first_root_layer) = self
|
||||
.upstream_flow_back_from_nodes(vec![root_node.node_id], &[], FlowType::PrimaryFlow)
|
||||
.find_map(|node_id| if self.is_layer(&node_id, &[]) { Some(LayerNodeIdentifier::new(node_id, self)) } else { None })
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Should refer to output node
|
||||
let mut awaiting_horizontal_flow = vec![(first_root_layer.to_node(), first_root_layer)];
|
||||
let mut awaiting_primary_flow = vec![];
|
||||
|
||||
while let Some((horizontal_root_node_id, mut parent_layer_node)) = awaiting_horizontal_flow.pop() {
|
||||
let horizontal_flow_iter = self.upstream_flow_back_from_nodes(vec![horizontal_root_node_id], &[], FlowType::HorizontalFlow);
|
||||
let mut children = Vec::new();
|
||||
|
||||
// Special handling for the root layer, since it should not be skipped
|
||||
if horizontal_root_node_id == first_root_layer.to_node() {
|
||||
for current_node_id in horizontal_flow_iter {
|
||||
if self.is_layer(¤t_node_id, &[]) {
|
||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||
if current_node_id == first_root_layer.to_node() {
|
||||
awaiting_primary_flow.push((current_node_id, LayerNodeIdentifier::ROOT_PARENT));
|
||||
children.push((LayerNodeIdentifier::ROOT_PARENT, current_layer_node));
|
||||
} else {
|
||||
awaiting_primary_flow.push((current_node_id, parent_layer_node));
|
||||
children.push((parent_layer_node, current_layer_node));
|
||||
}
|
||||
parent_layer_node = current_layer_node;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Skip the horizontal_root_node_id node
|
||||
for current_node_id in horizontal_flow_iter.skip(1) {
|
||||
if self.is_layer(¤t_node_id, &[]) {
|
||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||
awaiting_primary_flow.push((current_node_id, parent_layer_node));
|
||||
children.push((parent_layer_node, current_layer_node));
|
||||
parent_layer_node = current_layer_node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (parent, child) in children {
|
||||
parent.push_child(&mut self.document_metadata, child);
|
||||
}
|
||||
|
||||
while let Some((primary_root_node_id, parent_layer_node)) = awaiting_primary_flow.pop() {
|
||||
let primary_flow_iter = self.upstream_flow_back_from_nodes(vec![primary_root_node_id], &[], FlowType::PrimaryFlow);
|
||||
// Skip the primary_root_node_id node
|
||||
let mut children = Vec::new();
|
||||
for current_node_id in primary_flow_iter.skip(1) {
|
||||
if self.is_layer(¤t_node_id, &[]) {
|
||||
// Create a new layer for the top of each stack, and add it as a child to the previous parent
|
||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||
children.push(current_layer_node);
|
||||
|
||||
// The layer nodes for the horizontal flow is itself
|
||||
awaiting_horizontal_flow.push((current_node_id, current_layer_node));
|
||||
}
|
||||
}
|
||||
}
|
||||
for child in children {
|
||||
parent_layer_node.push_child(&mut self.document_metadata, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodes: HashSet<NodeId> = self.document_network().nodes.keys().cloned().collect::<HashSet<_>>();
|
||||
|
||||
self.document_metadata.upstream_footprints.retain(|node, _| nodes.contains(node));
|
||||
self.document_metadata.local_transforms.retain(|node, _| nodes.contains(node));
|
||||
self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node));
|
||||
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
self.document_metadata.outlines.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
self.document_metadata.text_frames.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
}
|
||||
|
||||
/// Update the cached transforms of the layers
|
||||
pub fn update_transforms(&mut self, upstream_footprints: HashMap<NodeId, Footprint>, local_transforms: HashMap<NodeId, DAffine2>) {
|
||||
self.document_metadata.upstream_footprints = upstream_footprints;
|
||||
self.document_metadata.local_transforms = local_transforms;
|
||||
}
|
||||
|
||||
/// Update the cached first item's source id of the layers
|
||||
pub fn update_first_element_source_id(&mut self, new: HashMap<NodeId, Option<NodeId>>) {
|
||||
self.document_metadata.first_element_source_ids = new;
|
||||
}
|
||||
|
||||
/// Update the cached click targets of the layers
|
||||
pub fn update_click_targets(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>) {
|
||||
self.document_metadata.click_targets = new_click_targets;
|
||||
}
|
||||
|
||||
/// Update the cached source-geometry outline targets of the layers
|
||||
pub fn update_outlines(&mut self, new_outlines: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>) {
|
||||
self.document_metadata.outlines = new_outlines;
|
||||
}
|
||||
|
||||
/// Update the cached per-layer 'Text' node text frames in row-local space (as `DAffine2`
|
||||
/// mapping the unit square onto the frame).
|
||||
pub fn update_text_frames(&mut self, new_text_frames: HashMap<LayerNodeIdentifier, DAffine2>) {
|
||||
self.document_metadata.text_frames = new_text_frames;
|
||||
}
|
||||
|
||||
/// Update the cached clip targets of the layers
|
||||
pub fn update_clip_targets(&mut self, new_clip_targets: HashSet<NodeId>) {
|
||||
self.document_metadata.clip_targets = new_clip_targets;
|
||||
}
|
||||
|
||||
/// Update the vector modify of the layers
|
||||
pub fn update_vector_modify(&mut self, new_vector_modify: HashMap<NodeId, Vector>) {
|
||||
self.document_metadata.vector_modify = new_vector_modify;
|
||||
}
|
||||
|
||||
/// Update the layer vector data (for layers without Path nodes)
|
||||
pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>) {
|
||||
self.document_metadata.layer_vector_data = new_layer_vector_data;
|
||||
}
|
||||
|
||||
/// Update the per-layer `ATTR_APPEARANCE` snapshot.
|
||||
pub fn update_appearance_attributes(&mut self, new_layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>) {
|
||||
self.document_metadata.layer_appearance_attributes = new_layer_appearance_attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use super::*;
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::concrete;
|
||||
use graphene_std::Context;
|
||||
|
||||
// PartialEq required by message handlers
|
||||
/// All persistent editor and Graphene data for a node, unified into a single flat shape.
|
||||
/// Used to author node definitions, pass nodes through the editor, and serialize them for the clipboard.
|
||||
///
|
||||
/// [`Self::into_parts`] and [`Self::from_parts`] are the only places this is split into (or joined from) the
|
||||
/// [`DocumentNode`] and [`DocumentNodePersistentMetadata`] halves stored by the network interface, so the parallel-tree
|
||||
/// storage invariants hold by construction for every node built from a template.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(from = "NodeTemplateParts", into = "NodeTemplateParts")]
|
||||
pub struct NodeTemplate {
|
||||
/// The graph data inputs to the node. Kept the same length as `input_metadata` by [`Self::normalize_input_metadata`] and [`Self::into_parts`].
|
||||
pub inputs: Vec<NodeInput>,
|
||||
/// Type of the argument which this node can be evaluated with.
|
||||
pub call_argument: Type,
|
||||
/// A nested network of templates, a proto node identifier, or the Extract tag.
|
||||
pub implementation: NodeTemplateImplementation,
|
||||
/// Represents the eye icon for hiding/showing the node in the graph UI.
|
||||
pub visible: bool,
|
||||
/// Prevents identical proto nodes from being deduplicated during compilation, e.g. for monitor nodes.
|
||||
pub skip_deduplication: bool,
|
||||
/// List of Extract and Inject annotations for the Context.
|
||||
pub context_features: ContextDependencies,
|
||||
/// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the implementation name is displayed in italics.
|
||||
pub display_name: String,
|
||||
/// Metadata to override the properties panel widgets for each input. Kept the same length as `inputs`.
|
||||
pub input_metadata: Vec<InputMetadata>,
|
||||
pub output_names: Vec<String>,
|
||||
/// Represents the lock icon for locking/unlocking the node in the graph UI.
|
||||
pub locked: bool,
|
||||
/// Indicates that the node will be shown in the Properties panel when it would otherwise be empty.
|
||||
pub pinned: bool,
|
||||
/// Whether the node is displayed as a left-to-right node or bottom-to-top layer, along with its position.
|
||||
pub node_type_metadata: NodeTypePersistentMetadata,
|
||||
}
|
||||
|
||||
impl NodeTemplate {
|
||||
/// The input slot named by the given parameter symbol, mirroring [`DocumentNode::input`].
|
||||
pub fn input<P: graphene_std::NodeParameter>(&self, _parameter: P) -> Option<&NodeInput> {
|
||||
self.inputs.get(P::INDEX)
|
||||
}
|
||||
|
||||
/// Mutable access to the input slot named by the given parameter symbol, mirroring [`DocumentNode::input_mut`].
|
||||
pub fn input_mut<P: graphene_std::NodeParameter>(&mut self, _parameter: P) -> Option<&mut NodeInput> {
|
||||
self.inputs.get_mut(P::INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NodeTemplate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inputs: Vec::new(),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: NodeTemplateImplementation::default(),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
context_features: ContextDependencies::default(),
|
||||
display_name: String::new(),
|
||||
input_metadata: Vec::new(),
|
||||
output_names: Vec::new(),
|
||||
locked: false,
|
||||
pinned: false,
|
||||
node_type_metadata: NodeTypePersistentMetadata::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of a [`NodeTemplate`], mirroring [`DocumentNodeImplementation`] but carrying unified templates for nested network nodes.
|
||||
// Templates are transient authoring objects, so the Network variant's size is not worth the authoring noise of boxing
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NodeTemplateImplementation {
|
||||
Network(NodeNetworkTemplate),
|
||||
ProtoNode(ProtoNodeIdentifier),
|
||||
Extract,
|
||||
}
|
||||
|
||||
impl Default for NodeTemplateImplementation {
|
||||
fn default() -> Self {
|
||||
NodeTemplateImplementation::ProtoNode(graphene_std::ops::passthrough::IDENTIFIER)
|
||||
}
|
||||
}
|
||||
|
||||
/// A nested network within a [`NodeTemplate`], holding each nested node as a unified template alongside the network-level persistent metadata.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct NodeNetworkTemplate {
|
||||
pub exports: Vec<NodeInput>,
|
||||
pub nodes: HashMap<NodeId, NodeTemplate>,
|
||||
pub scope_injections: HashMap<String, (NodeId, Type)>,
|
||||
/// The identifier of the [`DocumentNodeDefinition`] this network was instantiated from, if unmodified.
|
||||
///
|
||||
/// [`DocumentNodeDefinition`]: crate::messages::portfolio::document::node_graph::document_node_definitions::DocumentNodeDefinition
|
||||
pub reference: Option<String>,
|
||||
/// The display order of pinned nodes in the Properties panel.
|
||||
pub pinned_node_order: Vec<NodeId>,
|
||||
pub previewing: Previewing,
|
||||
pub navigation_metadata: NavigationMetadata,
|
||||
}
|
||||
|
||||
impl NodeTemplate {
|
||||
/// Joins a [`DocumentNode`] and its persistent metadata into the unified template shape. Missing nested metadata is filled with defaults.
|
||||
pub fn from_parts(document_node: DocumentNode, persistent_node_metadata: DocumentNodePersistentMetadata) -> Self {
|
||||
let DocumentNode {
|
||||
inputs,
|
||||
call_argument,
|
||||
implementation,
|
||||
visible,
|
||||
skip_deduplication,
|
||||
context_features,
|
||||
original_location: _,
|
||||
} = document_node;
|
||||
let DocumentNodePersistentMetadata {
|
||||
display_name,
|
||||
input_metadata,
|
||||
output_names,
|
||||
locked,
|
||||
pinned,
|
||||
node_type_metadata,
|
||||
network_metadata,
|
||||
} = persistent_node_metadata;
|
||||
|
||||
let implementation = match implementation {
|
||||
DocumentNodeImplementation::Network(network) => {
|
||||
let mut nested_persistent = network_metadata.map(|metadata| metadata.persistent_metadata).unwrap_or_default();
|
||||
|
||||
// Pair each nested node with its metadata by ID, recursively
|
||||
let mut nodes = HashMap::with_capacity(network.nodes.len());
|
||||
for (node_id, node) in network.nodes {
|
||||
let node_persistent_metadata = nested_persistent.node_metadata.remove(&node_id).map(|metadata| metadata.persistent_metadata).unwrap_or_default();
|
||||
nodes.insert(node_id, NodeTemplate::from_parts(node, node_persistent_metadata));
|
||||
}
|
||||
|
||||
NodeTemplateImplementation::Network(NodeNetworkTemplate {
|
||||
exports: network.exports,
|
||||
nodes,
|
||||
scope_injections: network.scope_injections.into_iter().collect(),
|
||||
reference: nested_persistent.reference,
|
||||
pinned_node_order: nested_persistent.pinned_node_order,
|
||||
previewing: nested_persistent.previewing,
|
||||
navigation_metadata: nested_persistent.navigation_metadata,
|
||||
})
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(identifier) => NodeTemplateImplementation::ProtoNode(identifier),
|
||||
DocumentNodeImplementation::Extract => NodeTemplateImplementation::Extract,
|
||||
};
|
||||
|
||||
NodeTemplate {
|
||||
inputs,
|
||||
call_argument,
|
||||
implementation,
|
||||
visible,
|
||||
skip_deduplication,
|
||||
context_features,
|
||||
display_name,
|
||||
input_metadata,
|
||||
output_names,
|
||||
locked,
|
||||
pinned,
|
||||
node_type_metadata,
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the template into the [`DocumentNode`] and [`DocumentNodePersistentMetadata`] halves stored by the network interface.
|
||||
pub fn into_parts(self) -> (DocumentNode, DocumentNodePersistentMetadata) {
|
||||
let NodeTemplate {
|
||||
inputs,
|
||||
call_argument,
|
||||
implementation,
|
||||
visible,
|
||||
skip_deduplication,
|
||||
context_features,
|
||||
display_name,
|
||||
mut input_metadata,
|
||||
output_names,
|
||||
locked,
|
||||
pinned,
|
||||
node_type_metadata,
|
||||
} = self;
|
||||
|
||||
let (implementation, network_metadata) = implementation.into_parts();
|
||||
|
||||
// The stored metadata invariant requires exactly one input metadata entry per input
|
||||
input_metadata.resize_with(inputs.len(), InputMetadata::default);
|
||||
|
||||
let document_node = DocumentNode {
|
||||
inputs,
|
||||
call_argument,
|
||||
implementation,
|
||||
visible,
|
||||
skip_deduplication,
|
||||
context_features,
|
||||
original_location: Default::default(),
|
||||
};
|
||||
let persistent_node_metadata = DocumentNodePersistentMetadata {
|
||||
display_name,
|
||||
input_metadata,
|
||||
output_names,
|
||||
locked,
|
||||
pinned,
|
||||
node_type_metadata,
|
||||
network_metadata,
|
||||
};
|
||||
(document_node, persistent_node_metadata)
|
||||
}
|
||||
|
||||
/// Resizes `input_metadata` to match `inputs` at every nesting level, filling gaps with defaults.
|
||||
pub fn normalize_input_metadata(&mut self) {
|
||||
self.input_metadata.resize_with(self.inputs.len(), InputMetadata::default);
|
||||
|
||||
if let NodeTemplateImplementation::Network(network_template) = &mut self.implementation {
|
||||
for nested_template in network_template.nodes.values_mut() {
|
||||
nested_template.normalize_input_metadata();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes the stored types at every nesting level via [`DocumentNode::normalize_stored_types`], round-tripping through the split halves to reuse its logic.
|
||||
pub fn normalize_stored_types(&mut self) {
|
||||
let (mut document_node, persistent_node_metadata) = std::mem::take(self).into_parts();
|
||||
document_node.normalize_stored_types();
|
||||
*self = NodeTemplate::from_parts(document_node, persistent_node_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTemplateImplementation {
|
||||
/// Splits into the [`DocumentNodeImplementation`] and the nested network metadata stored alongside it.
|
||||
pub fn into_parts(self) -> (DocumentNodeImplementation, Option<NodeNetworkMetadata>) {
|
||||
match self {
|
||||
NodeTemplateImplementation::Network(network_template) => {
|
||||
let NodeNetworkTemplate {
|
||||
exports,
|
||||
nodes,
|
||||
scope_injections,
|
||||
reference,
|
||||
pinned_node_order,
|
||||
previewing,
|
||||
navigation_metadata,
|
||||
} = network_template;
|
||||
|
||||
// Split each nested template into its two halves, recursively
|
||||
let mut network = NodeNetwork {
|
||||
exports,
|
||||
scope_injections: scope_injections.into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut node_metadata = HashMap::with_capacity(nodes.len());
|
||||
for (node_id, node_template) in nodes {
|
||||
let (document_node, persistent_metadata) = node_template.into_parts();
|
||||
network.nodes.insert(node_id, document_node);
|
||||
node_metadata.insert(
|
||||
node_id,
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata,
|
||||
transient_metadata: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let network_metadata = NodeNetworkMetadata {
|
||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
||||
reference,
|
||||
node_metadata,
|
||||
pinned_node_order,
|
||||
previewing,
|
||||
navigation_metadata,
|
||||
selection_undo_history: Default::default(),
|
||||
selection_redo_history: Default::default(),
|
||||
},
|
||||
transient_metadata: Default::default(),
|
||||
};
|
||||
(DocumentNodeImplementation::Network(network), Some(network_metadata))
|
||||
}
|
||||
NodeTemplateImplementation::ProtoNode(identifier) => (DocumentNodeImplementation::ProtoNode(identifier), None),
|
||||
NodeTemplateImplementation::Extract => (DocumentNodeImplementation::Extract, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects resource IDs referenced by a template and its nested networks.
|
||||
pub fn collect_template_resources(template: &NodeTemplate, out: &mut HashSet<ResourceId>) {
|
||||
for input in &template.inputs {
|
||||
collect_input_resource(input, out);
|
||||
}
|
||||
|
||||
if let NodeTemplateImplementation::Network(network_template) = &template.implementation {
|
||||
for export in &network_template.exports {
|
||||
collect_input_resource(export, out);
|
||||
}
|
||||
for nested_template in network_template.nodes.values() {
|
||||
collect_template_resources(nested_template, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The legacy two-tree shape of [`NodeTemplate`], kept as its serde representation so serialized clipboard data round-trips across versions.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct NodeTemplateParts {
|
||||
document_node: DocumentNode,
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata,
|
||||
}
|
||||
|
||||
impl From<NodeTemplateParts> for NodeTemplate {
|
||||
fn from(parts: NodeTemplateParts) -> Self {
|
||||
NodeTemplate::from_parts(parts.document_node, parts.persistent_node_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeTemplate> for NodeTemplateParts {
|
||||
fn from(template: NodeTemplate) -> Self {
|
||||
let (document_node, persistent_node_metadata) = template.into_parts();
|
||||
NodeTemplateParts {
|
||||
document_node,
|
||||
persistent_node_metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
use super::*;
|
||||
use graphene_std::ParameterRef;
|
||||
use graphene_std::vector::misc::dvec2_to_point;
|
||||
use kurbo::{DEFAULT_ACCURACY, Shape};
|
||||
|
||||
/// Axis-aligned rectangle spanning the two opposite corners, used for node graph chrome click targets.
|
||||
pub fn rectangle_path(corner1: DVec2, corner2: DVec2) -> BezPath {
|
||||
kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2)).to_path(DEFAULT_ACCURACY)
|
||||
}
|
||||
|
||||
/// Same as [`rectangle_path`] but with per-corner radii ordered top left, top right, bottom right, bottom left.
|
||||
pub fn rounded_rectangle_path(corner1: DVec2, corner2: DVec2, radii: [f64; 4]) -> BezPath {
|
||||
let rect = kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2));
|
||||
if radii.iter().all(|radius| *radius == 0.) {
|
||||
return rect.to_path(DEFAULT_ACCURACY);
|
||||
}
|
||||
|
||||
let [top_left, top_right, bottom_right, bottom_left] = radii;
|
||||
kurbo::RoundedRect::from_rect(rect, kurbo::RoundedRectRadii::new(top_left, top_right, bottom_right, bottom_left)).to_path(DEFAULT_ACCURACY)
|
||||
}
|
||||
|
||||
/// Ellipse inscribed in the box spanning the two opposite corners.
|
||||
pub fn ellipse_path(corner1: DVec2, corner2: DVec2) -> BezPath {
|
||||
let rect = kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2));
|
||||
let mut path = kurbo::Ellipse::new(rect.center(), (rect.width() / 2., rect.height() / 2.), 0.).to_path(DEFAULT_ACCURACY);
|
||||
|
||||
// Kurbo emits the ellipse as an unclosed 360 degree arc, but hit testing only fills explicitly closed contours
|
||||
path.close_path();
|
||||
path
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum FlowType {
|
||||
/// Iterate over all upstream nodes (inclusive) from every input (the primary and all secondary).
|
||||
UpstreamFlow,
|
||||
/// Iterate over nodes (inclusive) connected to the primary input.
|
||||
PrimaryFlow,
|
||||
/// Iterate over the secondary input (inclusive) for layer nodes and primary input for non layer nodes.
|
||||
HorizontalFlow,
|
||||
/// Same as horizontal flow, but only iterates over connections to primary outputs
|
||||
HorizontalPrimaryOutputFlow,
|
||||
/// Upstream flow starting from the either the node (inclusive) or secondary input of the layer (not inclusive).
|
||||
LayerChildrenUpstreamFlow,
|
||||
}
|
||||
/// Iterate over upstream nodes. The behavior changes based on the `flow_type` that's set.
|
||||
/// - [`FlowType::UpstreamFlow`]: iterates over all upstream nodes from every input (the primary and all secondary).
|
||||
/// - [`FlowType::PrimaryFlow`]: iterates along the horizontal inputs of nodes, so in the case of a node chain `a -> b -> c`, this would yield `c, b, a` if we started from `c`.
|
||||
/// - [`FlowType::HorizontalFlow`]: iterates over the secondary input for layer nodes and primary input for non layer nodes.
|
||||
/// - [`FlowType::LayerChildrenUpstreamFlow`]: iterates over all upstream nodes from the secondary input of the node.
|
||||
pub(crate) struct FlowIter<'a> {
|
||||
pub(crate) stack: Vec<NodeId>,
|
||||
pub(crate) network: &'a NodeNetwork,
|
||||
pub(crate) network_metadata: &'a NodeNetworkMetadata,
|
||||
pub(crate) flow_type: FlowType,
|
||||
}
|
||||
impl Iterator for FlowIter<'_> {
|
||||
type Item = NodeId;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let node_id = self.stack.pop()?;
|
||||
|
||||
if let (Some(document_node), Some(node_metadata)) = (self.network.nodes.get(&node_id), self.network_metadata.persistent_metadata.node_metadata.get(&node_id)) {
|
||||
let skip = if matches!(self.flow_type, FlowType::HorizontalFlow | FlowType::HorizontalPrimaryOutputFlow) && node_metadata.persistent_metadata.is_layer() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
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 {
|
||||
NodeInput::Node { output_index, .. } if self.flow_type == FlowType::HorizontalPrimaryOutputFlow && *output_index != 0 => None,
|
||||
NodeInput::Node { node_id, .. } => Some(node_id),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
self.stack.extend(node_ids);
|
||||
|
||||
return Some(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ImportOrExport {
|
||||
Import(usize),
|
||||
Export(usize),
|
||||
}
|
||||
|
||||
/// The primary input (index 0) of any node: a chain node's horizontal wire continuing from the left, or a layer's vertical stack wire from the sibling rendered below it.
|
||||
pub const PRIMARY_INPUT_INDEX: usize = 0;
|
||||
/// The secondary input (index 1) of a layer-shaped node: the horizontal wire from the left, carrying the node chain or child stack that the layer renders.
|
||||
pub const LAYER_SECONDARY_INPUT_INDEX: usize = 1;
|
||||
/// The primary output (index 0) of a node, which most nodes expose as their only output.
|
||||
pub const PRIMARY_OUTPUT_INDEX: usize = 0;
|
||||
|
||||
/// Represents an input connector with index based on the [`DocumentNode::inputs`] index, not the visible input index
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InputConnector {
|
||||
#[serde(rename = "node")]
|
||||
Node {
|
||||
#[serde(rename = "nodeId")]
|
||||
node_id: NodeId,
|
||||
#[serde(rename = "inputIndex")]
|
||||
input_index: usize,
|
||||
},
|
||||
#[serde(rename = "export")]
|
||||
Export(usize),
|
||||
}
|
||||
|
||||
impl Default for InputConnector {
|
||||
fn default() -> Self {
|
||||
InputConnector::Export(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputConnector {
|
||||
/// Reference a node's input by its parameter symbol, e.g. `InputConnector::node(node_id, stroke::WeightInput)`, or by an erased [`ParameterRef`] chosen at runtime.
|
||||
pub fn node(node_id: NodeId, parameter: impl Into<ParameterRef>) -> Self {
|
||||
InputConnector::Node {
|
||||
node_id,
|
||||
input_index: parameter.into().input_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference a node's primary input: a chain node's continuation from the left, or a layer's stack wire from the bottom.
|
||||
pub fn primary_input(node_id: NodeId) -> Self {
|
||||
InputConnector::Node {
|
||||
node_id,
|
||||
input_index: PRIMARY_INPUT_INDEX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference a layer-shaped node's secondary input, the wire from the left carrying the content that the layer renders.
|
||||
pub fn layer_secondary_input(node_id: NodeId) -> Self {
|
||||
InputConnector::Node {
|
||||
node_id,
|
||||
input_index: LAYER_SECONDARY_INPUT_INDEX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference a node's input by a runtime index, for genuinely dynamic cases like clicked ports, input enumeration, and document upgrades.
|
||||
pub fn node_at_index(node_id: NodeId, input_index: usize) -> Self {
|
||||
InputConnector::Node { node_id, input_index }
|
||||
}
|
||||
|
||||
pub fn input_index(&self) -> usize {
|
||||
match self {
|
||||
InputConnector::Node { input_index, .. } => *input_index,
|
||||
InputConnector::Export(input_index) => *input_index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> Option<NodeId> {
|
||||
match self {
|
||||
InputConnector::Node { node_id, .. } => Some(*node_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an output connector
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum OutputConnector {
|
||||
#[serde(rename = "node")]
|
||||
Node {
|
||||
#[serde(rename = "nodeId")]
|
||||
node_id: NodeId,
|
||||
#[serde(rename = "outputIndex")]
|
||||
output_index: usize,
|
||||
},
|
||||
#[serde(rename = "import")]
|
||||
Import(usize),
|
||||
}
|
||||
|
||||
impl Default for OutputConnector {
|
||||
fn default() -> Self {
|
||||
OutputConnector::Import(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputConnector {
|
||||
pub fn node(node_id: NodeId, output_index: usize) -> Self {
|
||||
OutputConnector::Node { node_id, output_index }
|
||||
}
|
||||
|
||||
/// Reference a node's primary (first) output.
|
||||
pub fn primary_output(node_id: NodeId) -> Self {
|
||||
OutputConnector::Node {
|
||||
node_id,
|
||||
output_index: PRIMARY_OUTPUT_INDEX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
match self {
|
||||
OutputConnector::Node { output_index, .. } => *output_index,
|
||||
OutputConnector::Import(output_index) => *output_index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> Option<NodeId> {
|
||||
match self {
|
||||
OutputConnector::Node { node_id, .. } => Some(*node_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_input(input: &NodeInput) -> Option<Self> {
|
||||
match input {
|
||||
NodeInput::Import { import_index, .. } => Some(Self::Import(*import_index)),
|
||||
NodeInput::Node { node_id, output_index, .. } => Some(Self::node(*node_id, *output_index)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ports {
|
||||
pub(crate) input_ports: Vec<(usize, ClickTarget)>,
|
||||
pub(crate) output_ports: Vec<(usize, ClickTarget)>,
|
||||
}
|
||||
|
||||
impl Default for Ports {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ports {
|
||||
pub fn new() -> Ports {
|
||||
Ports {
|
||||
input_ports: Vec::new(),
|
||||
output_ports: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn click_targets(&self) -> impl Iterator<Item = &ClickTarget> {
|
||||
self.input_ports
|
||||
.iter()
|
||||
.map(|(_, click_target)| click_target)
|
||||
.chain(self.output_ports.iter().map(|(_, click_target)| click_target))
|
||||
}
|
||||
|
||||
pub fn input_ports(&self) -> impl Iterator<Item = &(usize, ClickTarget)> {
|
||||
self.input_ports.iter()
|
||||
}
|
||||
|
||||
pub fn output_ports(&self) -> impl Iterator<Item = &(usize, ClickTarget)> {
|
||||
self.output_ports.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) {
|
||||
let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
|
||||
self.insert_custom_input_port(input_index, ClickTarget::new_with_path(path, 0.));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) {
|
||||
self.input_ports.push((input_index, click_target));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) {
|
||||
let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
|
||||
self.insert_custom_output_port(output_index, ClickTarget::new_with_path(path, 0.));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) {
|
||||
self.output_ports.push((output_index, click_target));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_node_input(&mut self, input_index: usize, row_index: usize, node_top_left: DVec2) {
|
||||
// The center of the click target is always 24 px down from the top left corner of the node
|
||||
let center = node_top_left + DVec2::new(0., 24. + 24. * row_index as f64);
|
||||
self.insert_input_port_at_center(input_index, center);
|
||||
}
|
||||
|
||||
pub(crate) fn insert_node_output(&mut self, output_index: usize, node_top_left: DVec2) {
|
||||
// The center of the click target is always 24 px down from the top left corner of the node
|
||||
let center = node_top_left + DVec2::new(5. * 24., 24. + 24. * output_index as f64);
|
||||
self.insert_output_port_at_center(output_index, center);
|
||||
}
|
||||
|
||||
pub(crate) fn insert_layer_input(&mut self, input_index: usize, node_top_left: DVec2) {
|
||||
let center = if input_index == 0 {
|
||||
node_top_left + DVec2::new(2. * 24., 24. * 2. + 8.)
|
||||
} else {
|
||||
node_top_left + DVec2::new(0., 24. * 1.)
|
||||
};
|
||||
self.insert_input_port_at_center(input_index, center);
|
||||
}
|
||||
|
||||
pub(crate) fn insert_layer_output(&mut self, node_top_left: DVec2) {
|
||||
// The center of the click target is always 24 px down from the top left corner of the node
|
||||
let center = node_top_left + DVec2::new(2. * 24., -8.);
|
||||
self.insert_output_port_at_center(0, center);
|
||||
}
|
||||
|
||||
pub fn clicked_input_port_from_point(&self, point: DVec2) -> Option<usize> {
|
||||
self.input_ports.iter().find_map(|(port, click_target)| click_target.intersect_point_no_stroke(point).then_some(*port))
|
||||
}
|
||||
|
||||
pub fn clicked_output_port_from_point(&self, point: DVec2) -> Option<usize> {
|
||||
self.output_ports.iter().find_map(|(port, click_target)| click_target.intersect_point_no_stroke(point).then_some(*port))
|
||||
}
|
||||
|
||||
pub fn input_port_position(&self, index: usize) -> Option<DVec2> {
|
||||
self.input_ports
|
||||
.iter()
|
||||
.find_map(|(port_index, click_target)| if *port_index == index { click_target.bounding_box_center() } else { None })
|
||||
}
|
||||
|
||||
pub fn output_port_position(&self, index: usize) -> Option<DVec2> {
|
||||
self.output_ports
|
||||
.iter()
|
||||
.find_map(|(port_index, click_target)| if *port_index == index { click_target.bounding_box_center() } else { None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RootNode {
|
||||
pub node_id: NodeId,
|
||||
pub output_index: usize,
|
||||
}
|
||||
|
||||
impl RootNode {
|
||||
pub fn to_connector(&self) -> OutputConnector {
|
||||
OutputConnector::Node {
|
||||
node_id: self.node_id,
|
||||
output_index: self.output_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Previewing {
|
||||
/// If there is a node to restore the connection to the export for, then it is stored in the option.
|
||||
/// Otherwise, nothing gets restored and the primary export is disconnected.
|
||||
Yes { root_node_to_restore: Option<RootNode> },
|
||||
#[default]
|
||||
No,
|
||||
}
|
||||
|
||||
/// All fields in NetworkMetadata should automatically be updated by using the network interface API. If a field is none then it should be calculated based on the network state.
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodeNetworkMetadata {
|
||||
pub persistent_metadata: NodeNetworkPersistentMetadata,
|
||||
#[serde(skip)]
|
||||
pub transient_metadata: NodeNetworkTransientMetadata,
|
||||
}
|
||||
|
||||
impl Clone for NodeNetworkMetadata {
|
||||
fn clone(&self) -> Self {
|
||||
NodeNetworkMetadata {
|
||||
persistent_metadata: self.persistent_metadata.clone(),
|
||||
transient_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for NodeNetworkMetadata {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.persistent_metadata == other.persistent_metadata
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeNetworkMetadata {
|
||||
pub fn nested_metadata(&self, nested_path: &[NodeId]) -> Option<&Self> {
|
||||
let mut network_metadata = Some(self);
|
||||
|
||||
for segment in nested_path {
|
||||
network_metadata = network_metadata
|
||||
.and_then(|network| network.persistent_metadata.node_metadata.get(segment))
|
||||
.and_then(|node| node.persistent_metadata.network_metadata.as_ref());
|
||||
}
|
||||
network_metadata
|
||||
}
|
||||
|
||||
/// Get the mutable nested network given by the path of node ids
|
||||
pub fn nested_metadata_mut(&mut self, nested_path: &[NodeId]) -> Option<&mut Self> {
|
||||
let mut network_metadata = Some(self);
|
||||
|
||||
for segment in nested_path {
|
||||
network_metadata = network_metadata
|
||||
.and_then(|network: &mut NodeNetworkMetadata| network.persistent_metadata.node_metadata.get_mut(segment))
|
||||
.and_then(|node| node.persistent_metadata.network_metadata.as_mut());
|
||||
}
|
||||
network_metadata
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodeNetworkPersistentMetadata {
|
||||
/// The identifier for the node definition created for custom network nodes in [`DocumentNodeDefinition`].
|
||||
/// It is only used to associate network nodes with their definition. Protonodes use their ProtonodeIdentifier.
|
||||
/// The reference is removed once the node is modified, since the node now stores its own implementation and inputs.
|
||||
/// TODO: Used during serialization/deserialization to prevent storing implementation or inputs (and possible other fields) if they are the same as the definition.
|
||||
/// TODO: Implement node versioning so that references to old nodes can be updated to the new node definition.
|
||||
pub reference: Option<String>,
|
||||
/// Node metadata must exist for every document node in the network
|
||||
#[serde(serialize_with = "graphene_std::vector::serialize_hashmap", deserialize_with = "graphene_std::vector::deserialize_hashmap")]
|
||||
pub node_metadata: HashMap<NodeId, DocumentNodeMetadata>,
|
||||
/// The display order of pinned nodes in the Properties panel (shown when nothing is selected in this network), keyed by node ID.
|
||||
#[serde(default)]
|
||||
pub pinned_node_order: Vec<NodeId>,
|
||||
/// Cached metadata for each node, which is calculated when adding a node to node_metadata
|
||||
/// Indicates whether the network is currently rendered with a particular node that is previewed, and if so, which connection should be restored when the preview ends.
|
||||
pub previewing: Previewing,
|
||||
// Stores the transform and navigation state for the network
|
||||
pub navigation_metadata: NavigationMetadata,
|
||||
/// Stack of selection snapshots for previous history states. Session state that is not persisted into saved documents.
|
||||
#[serde(skip)]
|
||||
pub selection_undo_history: VecDeque<SelectedNodes>,
|
||||
/// Stack of selection snapshots for future history states.
|
||||
#[serde(skip)]
|
||||
pub selection_redo_history: VecDeque<SelectedNodes>,
|
||||
}
|
||||
|
||||
/// A lazily computed cache slot whose load and read paths work through &self, with interior mutability guarding the stored value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TransientCache<T>(std::cell::RefCell<Option<T>>);
|
||||
|
||||
impl<T> Default for TransientCache<T> {
|
||||
fn default() -> Self {
|
||||
TransientCache(std::cell::RefCell::new(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TransientCache<T> {
|
||||
pub(crate) fn is_loaded(&self) -> bool {
|
||||
self.0.borrow().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn store(&self, value: T) {
|
||||
*self.0.borrow_mut() = Some(value);
|
||||
}
|
||||
|
||||
pub(crate) fn unload(&self) {
|
||||
*self.0.borrow_mut() = None;
|
||||
}
|
||||
|
||||
/// Runs `read` on the cached value if it is loaded.
|
||||
pub(crate) fn with_loaded<R>(&self, read: impl FnOnce(&T) -> R) -> Option<R> {
|
||||
self.0.borrow().as_ref().map(read)
|
||||
}
|
||||
|
||||
/// Direct access without runtime borrow tracking, for callers already holding exclusive access.
|
||||
pub(crate) fn get_loaded_mut(&mut self) -> Option<&mut T> {
|
||||
self.0.get_mut().as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// If some network calculation is too slow to compute for every usage, cache the data here
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NodeNetworkTransientMetadata {
|
||||
pub selected_nodes: SelectedNodes,
|
||||
/// Sole dependents of the top of the stacks of all selected nodes. Used to determine which nodes are checked for collision when shifting.
|
||||
/// The LayerOwner is used to determine whether the collided node should be shifted, or the layer that owns it.
|
||||
pub(crate) stack_dependents: TransientCache<HashMap<NodeId, LayerOwner>>,
|
||||
/// Cache for the bounding box around all nodes in node graph space.
|
||||
pub(crate) all_nodes_bounding_box: TransientCache<[DVec2; 2]>,
|
||||
/// Cache for all outward wire connections
|
||||
pub(crate) outward_wires: TransientCache<HashMap<OutputConnector, Vec<InputConnector>>>,
|
||||
/// All export connector click targets
|
||||
pub(crate) import_export_ports: TransientCache<Ports>,
|
||||
/// Click targets for adding, removing, and moving import/export ports
|
||||
pub(crate) modify_import_export: TransientCache<ModifyImportExportClickTarget>,
|
||||
|
||||
/// Cached wire SVG paths per input connector, where an entry's presence means that wire is loaded.
|
||||
pub(crate) wires: std::cell::RefCell<HashMap<InputConnector, WirePathUpdate>>,
|
||||
/// Drag-session state: how far each unowned stack dependent has been pushed vertically from its resting position. Cleared when the drag ends.
|
||||
pub(crate) drag_offsets: std::cell::RefCell<HashMap<NodeId, i32>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModifyImportExportClickTarget {
|
||||
// Subtract icon that appears when hovering over an import/export
|
||||
pub remove_imports_exports: Ports,
|
||||
// Grip drag icon that appears when hovering over an import/export
|
||||
pub reorder_imports_exports: Ports,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkEdgeDistance {
|
||||
/// The viewport pixel distance between the left edge of the node graph and the exports.
|
||||
pub exports_to_edge_distance: DVec2,
|
||||
/// The viewport pixel distance between the left edge of the node graph and the imports.
|
||||
pub imports_to_edge_distance: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LayerOwner {
|
||||
// Used to get the layer that should be shifted when there is a collision.
|
||||
Layer(NodeId),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DocumentNodeMetadata {
|
||||
#[serde(deserialize_with = "deserialize_node_persistent_metadata")]
|
||||
pub persistent_metadata: DocumentNodePersistentMetadata,
|
||||
#[serde(skip)]
|
||||
pub transient_metadata: DocumentNodeTransientMetadata,
|
||||
}
|
||||
|
||||
impl Clone for DocumentNodeMetadata {
|
||||
fn clone(&self) -> Self {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: self.persistent_metadata.clone(),
|
||||
transient_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DocumentNodeMetadata {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.persistent_metadata == other.persistent_metadata
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NumberInputSettings {
|
||||
pub unit: Option<String>,
|
||||
pub min: Option<f64>,
|
||||
pub max: Option<f64>,
|
||||
pub step: Option<f64>,
|
||||
pub mode: NumberInputMode,
|
||||
pub range_min: Option<f64>,
|
||||
pub range_max: Option<f64>,
|
||||
pub is_integer: bool,
|
||||
pub blank_assist: bool,
|
||||
}
|
||||
|
||||
impl Default for NumberInputSettings {
|
||||
fn default() -> Self {
|
||||
NumberInputSettings {
|
||||
unit: None,
|
||||
min: None,
|
||||
max: None,
|
||||
step: None,
|
||||
mode: NumberInputMode::default(),
|
||||
range_min: None,
|
||||
range_max: None,
|
||||
is_integer: false,
|
||||
blank_assist: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vec2InputSettings {
|
||||
pub x: String,
|
||||
pub y: String,
|
||||
pub unit: String,
|
||||
pub min: Option<f64>,
|
||||
pub is_integer: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum WidgetOverride {
|
||||
None,
|
||||
Hidden,
|
||||
String(String),
|
||||
Number(NumberInputSettings),
|
||||
Vec2(Vec2InputSettings),
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
// TODO: Custom deserialization/serialization to ensure number of properties row matches number of node inputs
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputPersistentMetadata {
|
||||
/// A general datastore than can store key value pairs of any types for any input
|
||||
/// Each instance of the input node needs to store its own data, since it can lose the reference to its
|
||||
/// node definition if the node signature is modified by the user. For example adding/removing/renaming an import/export of a network node.
|
||||
#[serde(serialize_with = "graphene_std::vector::serialize_hashmap_as_sorted_object")]
|
||||
pub input_data: HashMap<String, Value>,
|
||||
// An input can override a widget, which would otherwise be automatically generated from the type
|
||||
// The string is the identifier to the widget override function stored in INPUT_OVERRIDES
|
||||
pub widget_override: Option<String>,
|
||||
/// An empty input name means to use the type as the name.
|
||||
pub input_name: String,
|
||||
/// Displayed as the tooltip description.
|
||||
pub input_description: String,
|
||||
}
|
||||
|
||||
impl InputPersistentMetadata {
|
||||
pub fn with_name(mut self, input_name: &str) -> Self {
|
||||
self.input_name = input_name.to_string();
|
||||
self
|
||||
}
|
||||
pub fn with_override(mut self, widget_override: WidgetOverride) -> Self {
|
||||
match widget_override {
|
||||
// Uses the default widget for the type
|
||||
WidgetOverride::None => {
|
||||
self.widget_override = None;
|
||||
}
|
||||
WidgetOverride::Hidden => {
|
||||
self.widget_override = Some("hidden".to_string());
|
||||
}
|
||||
WidgetOverride::String(string_properties) => {
|
||||
self.input_data.insert("string_properties".to_string(), Value::String(string_properties));
|
||||
self.widget_override = Some("string".to_string());
|
||||
}
|
||||
WidgetOverride::Number(mut number_properties) => {
|
||||
if let Some(unit) = number_properties.unit.take() {
|
||||
self.input_data.insert("unit".to_string(), json!(unit));
|
||||
}
|
||||
if let Some(min) = number_properties.min.take() {
|
||||
self.input_data.insert("min".to_string(), json!(min));
|
||||
}
|
||||
if let Some(max) = number_properties.max.take() {
|
||||
self.input_data.insert("max".to_string(), json!(max));
|
||||
}
|
||||
if let Some(step) = number_properties.step.take() {
|
||||
self.input_data.insert("step".to_string(), json!(step));
|
||||
}
|
||||
if let Some(range_min) = number_properties.range_min.take() {
|
||||
self.input_data.insert("range_min".to_string(), json!(range_min));
|
||||
}
|
||||
if let Some(range_max) = number_properties.range_max.take() {
|
||||
self.input_data.insert("range_max".to_string(), json!(range_max));
|
||||
}
|
||||
self.input_data.insert("mode".to_string(), json!(number_properties.mode));
|
||||
self.input_data.insert("is_integer".to_string(), Value::Bool(number_properties.is_integer));
|
||||
self.input_data.insert("blank_assist".to_string(), Value::Bool(number_properties.blank_assist));
|
||||
self.widget_override = Some("number".to_string());
|
||||
}
|
||||
WidgetOverride::Vec2(vec2_properties) => {
|
||||
self.input_data.insert("x".to_string(), json!(vec2_properties.x));
|
||||
self.input_data.insert("y".to_string(), json!(vec2_properties.y));
|
||||
self.input_data.insert("unit".to_string(), json!(vec2_properties.unit));
|
||||
self.input_data.insert("is_integer".to_string(), Value::Bool(vec2_properties.is_integer));
|
||||
if let Some(min) = vec2_properties.min {
|
||||
self.input_data.insert("min".to_string(), json!(min));
|
||||
}
|
||||
self.widget_override = Some("vec2".to_string());
|
||||
}
|
||||
WidgetOverride::Custom(lambda_name) => {
|
||||
self.widget_override = Some(lambda_name);
|
||||
}
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: &str) -> Self {
|
||||
self.input_description = description.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DocumentNodePersistentMetadata {
|
||||
/// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the implementation name is displayed to the user in italics.
|
||||
#[serde(default)]
|
||||
pub display_name: String,
|
||||
/// Stores metadata to override the properties in the properties panel for each input. These can either be generated automatically based on the type, or with a custom function.
|
||||
/// Must match the length of node inputs
|
||||
pub input_metadata: Vec<InputMetadata>,
|
||||
pub output_names: Vec<String>,
|
||||
/// Represents the lock icon for locking/unlocking the node in the graph UI. When locked, a node cannot be moved in the graph UI.
|
||||
#[serde(default)]
|
||||
pub locked: bool,
|
||||
/// Indicates that the node will be shown in the Properties panel when it would otherwise be empty, letting a user easily edit its properties by just deselecting everything.
|
||||
#[serde(default)]
|
||||
pub pinned: bool,
|
||||
/// Metadata that is specific to either nodes or layers, which are chosen states for displaying as a left-to-right node or bottom-to-top layer.
|
||||
/// All fields in NodeTypePersistentMetadata should automatically be updated by using the network interface API
|
||||
pub node_type_metadata: NodeTypePersistentMetadata,
|
||||
/// This should always be Some for nodes with a [`DocumentNodeImplementation::Network`], and none for [`DocumentNodeImplementation::ProtoNode`]
|
||||
pub network_metadata: Option<NodeNetworkMetadata>,
|
||||
}
|
||||
|
||||
impl DocumentNodePersistentMetadata {
|
||||
pub fn is_layer(&self) -> bool {
|
||||
matches!(self.node_type_metadata, NodeTypePersistentMetadata::Layer(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputMetadata {
|
||||
pub persistent_metadata: InputPersistentMetadata,
|
||||
}
|
||||
|
||||
impl PartialEq for InputMetadata {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.persistent_metadata == other.persistent_metadata
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, &str)> for InputMetadata {
|
||||
fn from(input_name_and_description: (&str, &str)) -> Self {
|
||||
InputMetadata {
|
||||
persistent_metadata: InputPersistentMetadata::default()
|
||||
.with_name(input_name_and_description.0)
|
||||
.with_description(input_name_and_description.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputMetadata {
|
||||
pub fn with_name_description_override(input_name: &str, description: &str, widget_override: WidgetOverride) -> Self {
|
||||
InputMetadata {
|
||||
persistent_metadata: InputPersistentMetadata::default().with_name(input_name).with_description(description).with_override(widget_override),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NodeTypePersistentMetadata {
|
||||
Layer(LayerPersistentMetadata),
|
||||
Node(NodePersistentMetadata),
|
||||
}
|
||||
|
||||
impl Default for NodeTypePersistentMetadata {
|
||||
fn default() -> Self {
|
||||
NodeTypePersistentMetadata::node(IVec2::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTypePersistentMetadata {
|
||||
pub fn node(position: IVec2) -> NodeTypePersistentMetadata {
|
||||
NodeTypePersistentMetadata::Node(NodePersistentMetadata {
|
||||
position: NodePosition::Absolute(position),
|
||||
})
|
||||
}
|
||||
pub fn layer(position: IVec2) -> NodeTypePersistentMetadata {
|
||||
NodeTypePersistentMetadata::Layer(LayerPersistentMetadata {
|
||||
position: LayerPosition::Absolute(position),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// All fields in LayerMetadata should automatically be updated by using the network interface API
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct LayerPersistentMetadata {
|
||||
/// Stores the position of a layer node, which can either be Absolute or Stack
|
||||
pub position: LayerPosition,
|
||||
}
|
||||
|
||||
impl PartialEq for LayerPersistentMetadata {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.position == other.position
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodePersistentMetadata {
|
||||
/// Stores the position of a non layer node, which can either be Absolute or Chain
|
||||
pub(crate) position: NodePosition,
|
||||
}
|
||||
|
||||
impl NodePersistentMetadata {
|
||||
pub fn new(position: NodePosition) -> Self {
|
||||
Self { position }
|
||||
}
|
||||
pub fn position(&self) -> &NodePosition {
|
||||
&self.position
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer can either be position as Absolute or in a Stack
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LayerPosition {
|
||||
// Position of the node in grid spaces
|
||||
Absolute(IVec2),
|
||||
// A layer is in a Stack when it feeds into the bottom input of a layer. The Y position stores the vertical distance between the layer and its upstream sibling/parent.
|
||||
Stack(u32),
|
||||
}
|
||||
|
||||
/// A node can either be position as Absolute or in a Chain
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NodePosition {
|
||||
// Position of the node in grid spaces
|
||||
Absolute(IVec2),
|
||||
// In a chain the position is based on the number of nodes to the first layer node
|
||||
Chain,
|
||||
}
|
||||
|
||||
/// Cached metadata that should be calculated when creating a node, and should be recalculated when modifying a node property that affects one of the cached fields.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DocumentNodeTransientMetadata {
|
||||
// The click targets are stored as a single struct since it is very rare for only one to be updated, and recomputing all click targets in one function is more efficient than storing them separately.
|
||||
pub(crate) click_targets: TransientCache<DocumentNodeClickTargets>,
|
||||
/// All nodes that should be moved when this layer is moved, kept here since only layers own nodes.
|
||||
pub(crate) owned_nodes: TransientCache<HashSet<NodeId>>,
|
||||
/// Width in grid units from the left edge of the layer's thumbnail to its left end, cached since text measurement is slow. Only loaded for layers.
|
||||
pub(crate) layer_width: TransientCache<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentNodeClickTargets {
|
||||
/// In order to keep the displayed position of the node in sync with the click target, the displayed position of a node is derived from the top left of the click target
|
||||
/// Ensure node_click_target is kept in sync when modifying a node property that changes its size. Currently this is alias, inputs, is_layer, and metadata
|
||||
pub node_click_target: ClickTarget,
|
||||
/// Stores all port click targets in node graph space.
|
||||
pub port_click_targets: Ports,
|
||||
// Click targets that are specific to either nodes or layers, which are chosen states for displaying as a left-to-right node or bottom-to-top layer.
|
||||
pub node_type_metadata: NodeTypeClickTargets,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NodeTypeClickTargets {
|
||||
Layer(Box<LayerClickTargets>),
|
||||
Node, // No transient click targets are stored exclusively for nodes
|
||||
}
|
||||
|
||||
/// All fields in TransientLayerMetadata should automatically be updated by using the network interface API
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LayerClickTargets {
|
||||
/// Cache for all visibility buttons. Should be automatically updated when update_click_target is called
|
||||
pub visibility_click_target: ClickTarget,
|
||||
/// Cache for the lock icon button, only present when the layer is locked.
|
||||
pub lock_click_target: Option<ClickTarget>,
|
||||
/// Cache for the grip icon, which is next to the visibility button.
|
||||
pub grip_click_target: ClickTarget,
|
||||
/// Cache for the layer's display-name text bounds. Used to detect double-click rename and
|
||||
/// to skip the drill-into-subgraph behavior when the click lands on the name itself.
|
||||
/// `None` for layers whose display name is empty.
|
||||
pub name_click_target: Option<ClickTarget>,
|
||||
}
|
||||
|
||||
pub enum LayerClickTargetTypes {
|
||||
Visibility,
|
||||
Lock,
|
||||
Grip,
|
||||
Name,
|
||||
// Preview,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NavigationMetadata {
|
||||
/// The current pan, and zoom state of the viewport's view of the node graph.
|
||||
/// Ensure `DocumentMessage::UpdateDocumentTransform` is called when the pan, zoom, or transform changes.
|
||||
pub node_graph_ptz: PTZ,
|
||||
// TODO: Eventually remove once te click targets are extracted from the native render
|
||||
/// Transform from node graph space to viewport space.
|
||||
pub node_graph_to_viewport: DAffine2,
|
||||
// TODO: Eventually remove once the import/export positions are extracted from the native render
|
||||
/// The width of the node graph in viewport space
|
||||
#[serde(default)]
|
||||
pub node_graph_width: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransactionStatus {
|
||||
Started,
|
||||
Modified,
|
||||
#[default]
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// How [`NodeNetworkInterface::is_sole_dependent`] should treat a downstream connector it encounters.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum SoleDependentStep {
|
||||
/// The downstream path ends here, inside the dependent set.
|
||||
Terminate,
|
||||
/// Keep walking downstream through this node.
|
||||
Continue,
|
||||
/// The path leaves the dependent set, so the candidate is not a sole dependent.
|
||||
Escape,
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet<ResourceId>) {
|
||||
for export in &network.exports {
|
||||
collect_input_resource(export, out);
|
||||
}
|
||||
for node in network.nodes.values() {
|
||||
collect_node_resources(node, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects resource IDs referenced by a node and its nested networks.
|
||||
pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet<ResourceId>) {
|
||||
for input in &node.inputs {
|
||||
collect_input_resource(input, out);
|
||||
}
|
||||
if let DocumentNodeImplementation::Network(nested) = &node.implementation {
|
||||
collect_network_resources(nested, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the resource ID held by a value input, covering node inputs and export slots alike.
|
||||
pub(crate) fn collect_input_resource(input: &NodeInput, out: &mut HashSet<ResourceId>) {
|
||||
if let NodeInput::Value { tagged_value, .. } = input
|
||||
&& let TaggedValue::Resource(id) = &**tagged_value
|
||||
{
|
||||
out.insert(*id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn port_click_targets_are_clickable_at_their_center() {
|
||||
let center = DVec2::new(100., 50.);
|
||||
let mut ports = Ports::new();
|
||||
ports.insert_input_port_at_center(0, center);
|
||||
ports.insert_output_port_at_center(0, center + DVec2::new(200., 0.));
|
||||
|
||||
assert_eq!(ports.clicked_input_port_from_point(center), Some(0));
|
||||
assert_eq!(ports.clicked_input_port_from_point(center + DVec2::new(5., 5.)), Some(0));
|
||||
assert_eq!(ports.clicked_input_port_from_point(center + DVec2::new(20., 0.)), None);
|
||||
|
||||
assert_eq!(ports.clicked_output_port_from_point(center + DVec2::new(200., 0.)), Some(0));
|
||||
assert_eq!(ports.clicked_output_port_from_point(center), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use super::{NodeNetworkInterface, NodeNetworkMetadata};
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
|
||||
|
||||
impl NodeNetworkInterface {
|
||||
/// Checks the structural invariants between the document network and its parallel metadata tree at every nesting level.
|
||||
/// Returns a description of each violation found, or an empty list if the interface is internally consistent.
|
||||
/// Intended for tests and debugging; violations indicate a desync bug in the mutation methods, not a user error.
|
||||
// O(nodes + wires) summed across all nesting levels, plus an acyclicity walk per network
|
||||
pub fn validate_invariants(&self) -> Vec<String> {
|
||||
let mut violations = Vec::new();
|
||||
validate_network(self.document_network(), &self.network_metadata, &mut Vec::new(), 0, &mut violations);
|
||||
violations
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_network(network: &NodeNetwork, network_metadata: &NodeNetworkMetadata, path: &mut Vec<NodeId>, import_count: usize, violations: &mut Vec<String>) {
|
||||
let node_metadata = &network_metadata.persistent_metadata.node_metadata;
|
||||
|
||||
// The network and its metadata tree must contain exactly the same set of nodes
|
||||
for node_id in network.nodes.keys() {
|
||||
if !node_metadata.contains_key(node_id) {
|
||||
violations.push(format!("Node {node_id} in network {path:?} has no metadata entry"));
|
||||
}
|
||||
}
|
||||
for node_id in node_metadata.keys() {
|
||||
if !network.nodes.contains_key(node_id) {
|
||||
violations.push(format!("Metadata entry {node_id} in network {path:?} has no corresponding node"));
|
||||
}
|
||||
}
|
||||
|
||||
// The pinned display order may only reference existing nodes
|
||||
for node_id in &network_metadata.persistent_metadata.pinned_node_order {
|
||||
if !network.nodes.contains_key(node_id) {
|
||||
violations.push(format!("Pinned node order in network {path:?} references nonexistent node {node_id}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Every wire must reference an existing endpoint within this network
|
||||
let validate_input = |input: &NodeInput, location: &str, violations: &mut Vec<String>| match input {
|
||||
NodeInput::Node { node_id, output_index, .. } => match network.nodes.get(node_id) {
|
||||
None => violations.push(format!("{location} in network {path:?} references nonexistent node {node_id}")),
|
||||
Some(target) if *output_index >= target.implementation.output_count() => {
|
||||
violations.push(format!("{location} in network {path:?} references nonexistent output {output_index} of node {node_id}"));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
NodeInput::Import { import_index, .. } if *import_index >= import_count => {
|
||||
violations.push(format!("{location} in network {path:?} references nonexistent import {import_index} of {import_count}"));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
for (node_id, node) in &network.nodes {
|
||||
for input in &node.inputs {
|
||||
validate_input(input, &format!("Input of node {node_id}"), violations);
|
||||
}
|
||||
}
|
||||
for (export_index, export) in network.exports.iter().enumerate() {
|
||||
validate_input(export, &format!("Export {export_index}"), violations);
|
||||
}
|
||||
|
||||
if !network.is_acyclic() {
|
||||
violations.push(format!("Network {path:?} contains a cycle"));
|
||||
}
|
||||
|
||||
for (node_id, node) in &network.nodes {
|
||||
let Some(metadata) = node_metadata.get(node_id) else { continue };
|
||||
let persistent = &metadata.persistent_metadata;
|
||||
|
||||
// Input metadata is a parallel array to the node's inputs and the lengths must stay in sync
|
||||
if persistent.input_metadata.len() != node.inputs.len() {
|
||||
violations.push(format!(
|
||||
"Node {node_id} in network {path:?} has {} inputs but {} input metadata entries",
|
||||
node.inputs.len(),
|
||||
persistent.input_metadata.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Nested network metadata must exist exactly for nodes implemented as networks
|
||||
match (&node.implementation, &persistent.network_metadata) {
|
||||
(DocumentNodeImplementation::Network(nested_network), Some(nested_metadata)) => {
|
||||
if persistent.output_names.len() != nested_network.exports.len() {
|
||||
violations.push(format!(
|
||||
"Node {node_id} in network {path:?} has {} exports but {} output names",
|
||||
nested_network.exports.len(),
|
||||
persistent.output_names.len()
|
||||
));
|
||||
}
|
||||
|
||||
path.push(*node_id);
|
||||
validate_network(nested_network, nested_metadata, path, node.inputs.len(), violations);
|
||||
path.pop();
|
||||
}
|
||||
(DocumentNodeImplementation::Network(_), None) => {
|
||||
violations.push(format!("Network node {node_id} in network {path:?} is missing its nested network metadata"));
|
||||
}
|
||||
(_, Some(_)) => {
|
||||
violations.push(format!("Non-network node {node_id} in network {path:?} has nested network metadata"));
|
||||
}
|
||||
(_, None) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
use super::*;
|
||||
|
||||
/// A failure to resolve part of the network or its metadata, returned by [`NetworkView`] accessors.
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum NetworkError {
|
||||
#[error("Network not found at path {path:?}")]
|
||||
NetworkNotFound { path: Vec<NodeId> },
|
||||
#[error("Node {node_id} not found")]
|
||||
NodeNotFound { node_id: NodeId },
|
||||
#[error("Node metadata for {node_id} not found")]
|
||||
NodeMetadataNotFound { node_id: NodeId },
|
||||
#[error("Nested network metadata for node {node_id} not found")]
|
||||
NestedMetadataNotFound { node_id: NodeId },
|
||||
#[error("Input {connector:?} not found")]
|
||||
InputNotFound { connector: InputConnector },
|
||||
#[error("The document network has no encapsulating node")]
|
||||
NoEncapsulatingNode,
|
||||
}
|
||||
|
||||
/// A read cursor over one network nesting level, resolving the network and its parallel metadata once instead of per query.
|
||||
/// Obtained from [`NodeNetworkInterface::view`]; accessors return typed errors that callers at the message boundary may log.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct NetworkView<'a, 'p> {
|
||||
pub(crate) interface: &'a NodeNetworkInterface,
|
||||
pub(crate) path: &'p [NodeId],
|
||||
pub(crate) network: &'a NodeNetwork,
|
||||
pub(crate) metadata: &'a NodeNetworkMetadata,
|
||||
}
|
||||
|
||||
impl NodeNetworkInterface {
|
||||
/// Resolves a read cursor for the network at `network_path`, walking the nested network and metadata trees once.
|
||||
pub fn view<'a, 'p>(&'a self, network_path: &'p [NodeId]) -> Result<NetworkView<'a, 'p>, NetworkError> {
|
||||
let network = self
|
||||
.document_network()
|
||||
.nested_network(network_path)
|
||||
.ok_or_else(|| NetworkError::NetworkNotFound { path: network_path.to_vec() })?;
|
||||
let metadata = self
|
||||
.document_network_metadata()
|
||||
.nested_metadata(network_path)
|
||||
.ok_or_else(|| NetworkError::NetworkNotFound { path: network_path.to_vec() })?;
|
||||
|
||||
Ok(NetworkView {
|
||||
interface: self,
|
||||
path: network_path,
|
||||
network,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'p> NetworkView<'a, 'p> {
|
||||
pub fn network_metadata(&self) -> &'a NodeNetworkMetadata {
|
||||
self.metadata
|
||||
}
|
||||
|
||||
pub fn node(&self, node_id: &NodeId) -> Result<&'a DocumentNode, NetworkError> {
|
||||
self.network.nodes.get(node_id).ok_or(NetworkError::NodeNotFound { node_id: *node_id })
|
||||
}
|
||||
|
||||
pub fn node_metadata(&self, node_id: &NodeId) -> Result<&'a DocumentNodeMetadata, NetworkError> {
|
||||
self.metadata
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.get(node_id)
|
||||
.ok_or(NetworkError::NodeMetadataNotFound { node_id: *node_id })
|
||||
}
|
||||
|
||||
pub fn input(&self, input_connector: &InputConnector) -> Result<&'a NodeInput, NetworkError> {
|
||||
let input = match input_connector {
|
||||
InputConnector::Node { node_id, input_index } => self.node(node_id)?.inputs.get(*input_index),
|
||||
InputConnector::Export(export_index) => self.network.exports.get(*export_index),
|
||||
};
|
||||
input.ok_or(NetworkError::InputNotFound { connector: *input_connector })
|
||||
}
|
||||
|
||||
/// The view over the network which contains this network's encapsulating node, or an error in the document network.
|
||||
pub fn encapsulating(&self) -> Result<NetworkView<'a, 'p>, NetworkError> {
|
||||
let (_, encapsulating_path) = self.path.split_last().ok_or(NetworkError::NoEncapsulatingNode)?;
|
||||
self.interface.view(encapsulating_path)
|
||||
}
|
||||
|
||||
/// The node which encapsulates this network, or an error in the document network.
|
||||
pub fn encapsulating_node(&self) -> Result<&'a DocumentNode, NetworkError> {
|
||||
let (node_id, _) = self.path.split_last().ok_or(NetworkError::NoEncapsulatingNode)?;
|
||||
self.encapsulating()?.node(node_id)
|
||||
}
|
||||
|
||||
/// The metadata of the node which encapsulates this network, or an error in the document network.
|
||||
pub fn encapsulating_node_metadata(&self) -> Result<&'a DocumentNodeMetadata, NetworkError> {
|
||||
let (node_id, _) = self.path.split_last().ok_or(NetworkError::NoEncapsulatingNode)?;
|
||||
self.encapsulating()?.node_metadata(node_id)
|
||||
}
|
||||
|
||||
pub fn implementation(&self, node_id: &NodeId) -> Result<&'a DocumentNodeImplementation, NetworkError> {
|
||||
Ok(&self.node(node_id)?.implementation)
|
||||
}
|
||||
|
||||
pub fn reference(&self, node_id: &NodeId) -> Result<Option<DefinitionIdentifier>, NetworkError> {
|
||||
match self.implementation(node_id)? {
|
||||
DocumentNodeImplementation::Network(_) => {
|
||||
let node_metadata = self.node_metadata(node_id)?;
|
||||
let network_metadata = node_metadata
|
||||
.persistent_metadata
|
||||
.network_metadata
|
||||
.as_ref()
|
||||
.ok_or(NetworkError::NestedMetadataNotFound { node_id: *node_id })?;
|
||||
Ok(network_metadata.persistent_metadata.reference.clone().map(DefinitionIdentifier::Network))
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(protonode_id) => Ok(Some(DefinitionIdentifier::ProtoNode(protonode_id.clone()))),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_layer(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(self.node_metadata(node_id)?.persistent_metadata.is_layer())
|
||||
}
|
||||
|
||||
/// Whether the node is an Artboard node by identity, regardless of whether it currently participates in the scene.
|
||||
pub fn is_artboard(&self, node_id: &NodeId) -> bool {
|
||||
self.reference(node_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|reference| reference == DefinitionIdentifier::Network("Artboard".into()))
|
||||
}
|
||||
|
||||
/// Whether the node is a Merge node by identity, regardless of whether it currently participates in the scene.
|
||||
pub fn is_merge(&self, node_id: &NodeId) -> bool {
|
||||
self.reference(node_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|reference| reference == DefinitionIdentifier::Network("Merge".into()))
|
||||
}
|
||||
|
||||
/// The uneditable name in the Properties panel which represents the function name of the node implementation.
|
||||
pub fn implementation_name(&self, node_id: &NodeId) -> String {
|
||||
self.reference(node_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|identifier| identifier.implementation_name_from_identifier())
|
||||
.unwrap_or("Custom Node".to_string())
|
||||
}
|
||||
|
||||
/// The display name of the node, falling back to "Untitled Layer" or the implementation name when unnamed.
|
||||
pub fn display_name(&self, node_id: &NodeId) -> String {
|
||||
let display_name = self.node_metadata(node_id).map(|metadata| metadata.persistent_metadata.display_name.clone()).unwrap_or_default();
|
||||
|
||||
if display_name.is_empty() {
|
||||
if self.is_layer(node_id).unwrap_or_default() {
|
||||
"Untitled Layer".to_string()
|
||||
} else {
|
||||
// TODO: Have this displayed in italics in the UI
|
||||
self.implementation_name(node_id)
|
||||
}
|
||||
} else {
|
||||
display_name
|
||||
}
|
||||
}
|
||||
|
||||
/// The current selection of this network, filtered to nodes that still exist.
|
||||
pub fn selected_nodes(&self) -> SelectedNodes {
|
||||
self.metadata
|
||||
.persistent_metadata
|
||||
.selection_undo_history
|
||||
.back()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.filtered_selected_nodes(|node_id| self.metadata.persistent_metadata.node_metadata.contains_key(node_id))
|
||||
}
|
||||
|
||||
pub fn primary_input_connected_to_layer(&self, node_id: &NodeId) -> bool {
|
||||
self.input(&InputConnector::primary_input(*node_id))
|
||||
.ok()
|
||||
.and_then(|input| input.as_node())
|
||||
.is_some_and(|upstream_id| self.is_layer(&upstream_id).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Whether the node's signature fits the layer form: one output, at most two exposed leading inputs, and no exposed parameters.
|
||||
pub fn is_eligible_to_be_layer(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
let node = self.node(node_id)?;
|
||||
let input_count = node.inputs.iter().take(2).filter(|input| input.is_exposed()).count();
|
||||
let parameters_hidden = node.inputs.iter().skip(2).all(|input| !input.is_exposed());
|
||||
let output_count = self.number_of_outputs(node_id)?;
|
||||
|
||||
Ok(!self.hidden_primary_output(node_id)? && output_count == 1 && (input_count <= 2) && parameters_hidden)
|
||||
}
|
||||
|
||||
pub fn is_locked(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(self.node_metadata(node_id)?.persistent_metadata.locked)
|
||||
}
|
||||
|
||||
pub fn is_pinned(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(self.node_metadata(node_id)?.persistent_metadata.pinned)
|
||||
}
|
||||
|
||||
pub fn is_visible(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(self.node(node_id)?.visible)
|
||||
}
|
||||
|
||||
pub fn is_absolute(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(match &self.node_metadata(node_id)?.persistent_metadata.node_type_metadata {
|
||||
NodeTypePersistentMetadata::Layer(layer_metadata) => matches!(layer_metadata.position, LayerPosition::Absolute(_)),
|
||||
NodeTypePersistentMetadata::Node(node_metadata) => matches!(node_metadata.position, NodePosition::Absolute(_)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_chain(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(match &self.node_metadata(node_id)?.persistent_metadata.node_type_metadata {
|
||||
NodeTypePersistentMetadata::Node(node_metadata) => matches!(node_metadata.position, NodePosition::Chain),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_stack(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(match &self.node_metadata(node_id)?.persistent_metadata.node_type_metadata {
|
||||
NodeTypePersistentMetadata::Layer(layer_metadata) => matches!(layer_metadata.position, LayerPosition::Stack(_)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn number_of_inputs(&self, node_id: &NodeId) -> Result<usize, NetworkError> {
|
||||
Ok(self.node(node_id)?.inputs.len())
|
||||
}
|
||||
|
||||
pub fn number_of_displayed_inputs(&self, node_id: &NodeId) -> Result<usize, NetworkError> {
|
||||
Ok(self.node(node_id)?.inputs.iter().filter(|input| input.is_exposed()).count())
|
||||
}
|
||||
|
||||
pub fn number_of_outputs(&self, node_id: &NodeId) -> Result<usize, NetworkError> {
|
||||
Ok(match self.implementation(node_id)? {
|
||||
DocumentNodeImplementation::Network(nested_network) => nested_network.exports.len(),
|
||||
_ => 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// The number of imports as defined by the encapsulating node's input count, or zero for the document network.
|
||||
pub fn number_of_imports(&self) -> usize {
|
||||
self.encapsulating_node().map_or(0, |node| node.inputs.len())
|
||||
}
|
||||
|
||||
pub fn number_of_exports(&self) -> usize {
|
||||
self.network.exports.len()
|
||||
}
|
||||
|
||||
pub fn has_primary_input(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(self.input(&InputConnector::primary_input(*node_id)).is_ok_and(|input| input.is_exposed()))
|
||||
}
|
||||
|
||||
pub fn hidden_primary_output(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
|
||||
Ok(match self.implementation(node_id)? {
|
||||
DocumentNodeImplementation::Network(network) => network.exports.first().is_none_or(|input| !input.is_exposed()),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hidden_primary_export(&self) -> bool {
|
||||
let Some((node_id, _)) = self.path.split_last() else { return false };
|
||||
self.encapsulating().and_then(|parent| parent.hidden_primary_output(node_id)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn hidden_primary_import(&self) -> bool {
|
||||
self.encapsulating_node().is_ok_and(|node| node.inputs.first().is_some_and(|input| !input.is_exposed()))
|
||||
}
|
||||
|
||||
pub fn previewing(&self) -> Previewing {
|
||||
self.metadata.persistent_metadata.previewing
|
||||
}
|
||||
|
||||
/// The root node (the node that the solid line is connected to), or None if no nodes are connected to the output.
|
||||
pub fn root_node(&self) -> Option<RootNode> {
|
||||
match &self.metadata.persistent_metadata.previewing {
|
||||
Previewing::Yes { root_node_to_restore } => *root_node_to_restore,
|
||||
Previewing::No => self.network.exports.first().and_then(|export| {
|
||||
if let NodeInput::Node { node_id, output_index, .. } = export {
|
||||
Some(RootNode {
|
||||
node_id: *node_id,
|
||||
output_index: *output_index,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_data(&self, node_id: &NodeId, index: usize, key: &str) -> Result<Option<&'a Value>, NetworkError> {
|
||||
Ok(self.persistent_input_metadata(node_id, index)?.input_data.get(key))
|
||||
}
|
||||
|
||||
pub fn persistent_input_metadata(&self, node_id: &NodeId, index: usize) -> Result<&'a InputPersistentMetadata, NetworkError> {
|
||||
let metadata = self.node_metadata(node_id)?;
|
||||
let input_metadata = metadata.persistent_metadata.input_metadata.get(index).ok_or(NetworkError::InputNotFound {
|
||||
connector: InputConnector::node_at_index(*node_id, index),
|
||||
})?;
|
||||
Ok(&input_metadata.persistent_metadata)
|
||||
}
|
||||
|
||||
pub fn upstream_output_connector(&self, input_connector: &InputConnector) -> Result<Option<OutputConnector>, NetworkError> {
|
||||
Ok(OutputConnector::from_input(self.input(input_connector)?))
|
||||
}
|
||||
|
||||
/// Whether the node reaches the exports by following wires downstream.
|
||||
// O(nodes + wires) reachability walk
|
||||
pub fn connected_to_output(&self, target_node_id: &NodeId) -> bool {
|
||||
if self
|
||||
.network
|
||||
.exports
|
||||
.iter()
|
||||
.any(|export| if let NodeInput::Node { node_id, .. } = export { node_id == target_node_id } else { false })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut stack = self
|
||||
.network
|
||||
.exports
|
||||
.iter()
|
||||
.filter_map(|output| if let NodeInput::Node { node_id, .. } = output { self.network.nodes.get(node_id) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
let mut already_visited = HashSet::new();
|
||||
already_visited.extend(
|
||||
self.network
|
||||
.exports
|
||||
.iter()
|
||||
.filter_map(|output| if let NodeInput::Node { node_id, .. } = output { Some(node_id) } else { None }),
|
||||
);
|
||||
|
||||
while let Some(node) = stack.pop() {
|
||||
for input in &node.inputs {
|
||||
if let &NodeInput::Node { node_id: upstream_id, .. } = input {
|
||||
if already_visited.contains(&upstream_id) {
|
||||
continue;
|
||||
}
|
||||
if upstream_id == *target_node_id {
|
||||
return true;
|
||||
}
|
||||
let Some(upstream_node) = self.network.nodes.get(&upstream_id) else { continue };
|
||||
already_visited.insert(upstream_id);
|
||||
stack.push(upstream_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// An iterator of all nodes connected to the given nodes by the flow type, traversing backwards upstream from the given nodes' inputs.
|
||||
pub(crate) fn upstream_flow(&self, mut node_ids: Vec<NodeId>, mut flow_type: FlowType) -> FlowIter<'a> {
|
||||
if matches!(flow_type, FlowType::LayerChildrenUpstreamFlow) {
|
||||
node_ids = node_ids
|
||||
.iter()
|
||||
.filter_map(|node_id| {
|
||||
if self.is_layer(node_id).unwrap_or_default() {
|
||||
self.network.nodes.get(node_id).and_then(|node| node.inputs.get(1)).and_then(|input| input.as_node())
|
||||
} else {
|
||||
Some(*node_id)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
flow_type = FlowType::UpstreamFlow;
|
||||
};
|
||||
FlowIter {
|
||||
stack: node_ids,
|
||||
network: self.network,
|
||||
network_metadata: self.metadata,
|
||||
flow_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// The distance in grid cells from the layer to the end of its chain, or zero for a layer with no chain.
|
||||
pub fn chain_width(&self, node_id: &NodeId) -> u32 {
|
||||
if self.number_of_displayed_inputs(node_id).unwrap_or_default() > 1 {
|
||||
let mut last_chain_node_distance = 0_u32;
|
||||
for (index, node_id) in self.upstream_flow(vec![*node_id], FlowType::HorizontalPrimaryOutputFlow).skip(1).enumerate() {
|
||||
if self.is_chain(&node_id).unwrap_or_default() {
|
||||
last_chain_node_distance = (index as u32) + 1;
|
||||
} else {
|
||||
return last_chain_node_distance * NODE_CHAIN_WIDTH as u32 + 1;
|
||||
}
|
||||
}
|
||||
|
||||
last_chain_node_distance * NODE_CHAIN_WIDTH as u32 + 1
|
||||
} else {
|
||||
// A layer with no secondary input has no chain
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// The given network's pinned nodes in display order: pinning appends, dragging rearranges, and any not yet recorded go last.
|
||||
pub fn ordered_pinned_nodes(&self) -> Vec<NodeId> {
|
||||
let order = &self.metadata.persistent_metadata.pinned_node_order;
|
||||
let is_pinned = |node_id: &NodeId| self.is_pinned(node_id).unwrap_or_default();
|
||||
|
||||
// Follow the saved order, keeping only nodes that still exist and are still pinned
|
||||
let mut pinned_nodes = order
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|node_id| self.network.nodes.contains_key(node_id) && is_pinned(node_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Append any pinned nodes missing from the saved order at the end
|
||||
let mut unordered = self.network.nodes.keys().copied().filter(|node_id| is_pinned(node_id) && !order.contains(node_id)).collect::<Vec<_>>();
|
||||
unordered.sort();
|
||||
pinned_nodes.extend(unordered);
|
||||
|
||||
pinned_nodes
|
||||
}
|
||||
|
||||
/// Create a node template from an existing node.
|
||||
pub fn create_node_template(&self, node_id: &NodeId) -> Result<NodeTemplate, NetworkError> {
|
||||
let node = self.node(node_id)?;
|
||||
let node_metadata = self.node_metadata(node_id)?;
|
||||
|
||||
Ok(NodeTemplate::from_parts(node.clone(), node_metadata.persistent_metadata.clone()))
|
||||
}
|
||||
}
|
||||
@@ -52,13 +52,7 @@ pub struct SelectedNodes(pub Vec<NodeId>);
|
||||
|
||||
impl SelectedNodes {
|
||||
pub fn layer_visible(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||
layer.ancestors(network_interface.document_metadata()).all(|layer| {
|
||||
if layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
network_interface.is_visible(&layer.to_node(), &[])
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
network_interface.is_layer_visible(layer)
|
||||
}
|
||||
|
||||
pub fn selected_visible_layers<'a>(&'a self, network_interface: &'a NodeNetworkInterface) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
||||
|
||||
pub fn text_width(text: &str, font_size: f64) -> f64 {
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
line_height_ratio: 1.2,
|
||||
letter_spacing: 0.,
|
||||
letter_tilt: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
align: TextAlign::AlignLeft,
|
||||
};
|
||||
|
||||
TextContext::with_thread_local(|text_context| text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false).x)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ impl OriginalTransforms {
|
||||
|
||||
let mut selected_points = selected_points.clone();
|
||||
|
||||
for (segment_id, _, start, end) in vector.segment_bezier_iter() {
|
||||
for (segment_id, _, start, end) in vector.segment_iter() {
|
||||
if selected_segments.contains(&segment_id) {
|
||||
selected_points.insert(ManipulatorPointId::Anchor(start));
|
||||
selected_points.insert(ManipulatorPointId::Anchor(end));
|
||||
@@ -392,7 +392,7 @@ impl TransformOperation {
|
||||
}
|
||||
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::keyboard::{Key, MouseMotion};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
let mut input_hints = Vec::new();
|
||||
|
||||
@@ -12,6 +12,12 @@ pub struct WirePath {
|
||||
pub data_type: FrontendGraphDataType,
|
||||
pub thick: bool,
|
||||
pub dashed: bool,
|
||||
// A rank-1 `List<T>` wire renders as a doubled-up pair of parallel lines to distinguish it from a rank-0 `Item<T>` wire
|
||||
#[serde(rename = "isList")]
|
||||
pub is_list: bool,
|
||||
// A thick wire's center line reaches past the wire into the cleaved connector slots, so it needs its own longer path; empty otherwise
|
||||
#[serde(rename = "centerPathString")]
|
||||
pub center_path_string: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
@@ -57,6 +63,19 @@ impl GraphWireStyle {
|
||||
|
||||
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> BezPath {
|
||||
let grid_spacing = 24.;
|
||||
|
||||
// A thick layer-stack wire (vertical at both ends) is skipped across a single straight grid cell where its connectors
|
||||
// already meet, and otherwise trimmed 3px inward at each end since it overshoots the connectors.
|
||||
let (output_position, input_position) = if vertical_out && vertical_in {
|
||||
if thick_wire_spans_single_cell(output_position, input_position) {
|
||||
return BezPath::new();
|
||||
}
|
||||
let trim = 3. * (input_position.y - output_position.y).signum();
|
||||
(output_position + DVec2::new(0., trim), input_position - DVec2::new(0., trim))
|
||||
} else {
|
||||
(output_position, input_position)
|
||||
};
|
||||
|
||||
match graph_wire_style {
|
||||
GraphWireStyle::Direct => {
|
||||
let horizontal_gap = (output_position.x - input_position.x).abs();
|
||||
@@ -101,6 +120,31 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical
|
||||
}
|
||||
}
|
||||
|
||||
fn thick_wire_spans_single_cell(output_position: DVec2, input_position: DVec2) -> bool {
|
||||
let grid_spacing = 24.;
|
||||
(output_position.x - input_position.x).abs() < 1. && (output_position.y - input_position.y).abs() <= grid_spacing
|
||||
}
|
||||
|
||||
/// The center line that cleaves a thick layer-stack wire. Its ends reach past the wire (1.5px toward the output
|
||||
/// connector and 2px toward the input) so the color runs through the full cleaved connector slots. Empty for other wires.
|
||||
pub fn build_thick_wire_center_line(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> BezPath {
|
||||
if !(vertical_out && vertical_in) || thick_wire_spans_single_cell(output_position, input_position) {
|
||||
return BezPath::new();
|
||||
}
|
||||
|
||||
// The 8px wire trims 3px at each end; the center line trims less so it reaches further into the cleaved slots
|
||||
let sign = (input_position.y - output_position.y).signum();
|
||||
let output_trim = 1.5;
|
||||
let input_trim = 1.;
|
||||
let start = output_position + DVec2::new(0., output_trim * sign);
|
||||
let end = input_position - DVec2::new(0., input_trim * sign);
|
||||
|
||||
let mut center_line = BezPath::new();
|
||||
center_line.move_to(dvec2_to_point(start));
|
||||
center_line.line_to(dvec2_to_point(end));
|
||||
center_line
|
||||
}
|
||||
|
||||
fn straight_wire_path(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
|
||||
let grid_spacing = 24;
|
||||
let line_width = 2;
|
||||
@@ -435,7 +479,7 @@ fn straight_wire_to_bezpath(locations: Vec<IVec2>) -> BezPath {
|
||||
if locations.len() == 2 {
|
||||
let p1 = to_point(locations[0]);
|
||||
let p2 = to_point(locations[1]);
|
||||
Line::new(p1, p2).to_path(DEFAULT_ACCURACY);
|
||||
return Line::new(p1, p2).to_path(DEFAULT_ACCURACY);
|
||||
}
|
||||
|
||||
let corner_radius = 10;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@ use crate::application::{Editor, generate_uuid};
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION};
|
||||
use crate::messages::animation::TimingInformation;
|
||||
use crate::messages::dialog::simple_dialogs;
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, FileFilter, PersistedState};
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::DocumentMessageContext;
|
||||
@@ -501,6 +501,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
responses.add(FrontendMessage::TriggerSaveFile {
|
||||
name: filename,
|
||||
folder: None,
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite Document".into(),
|
||||
extensions: vec![FILE_EXTENSION.into()],
|
||||
}],
|
||||
content: serde_bytes::ByteBuf::from(content),
|
||||
});
|
||||
} else {
|
||||
@@ -508,6 +512,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
Ok(zip_bytes) => responses.add(FrontendMessage::TriggerSaveFile {
|
||||
name: format!("{FOLDER_NAME}.zip"),
|
||||
folder: None,
|
||||
filters: vec![FileFilter {
|
||||
name: "Zip Archive".into(),
|
||||
extensions: vec!["zip".into()],
|
||||
}],
|
||||
content: serde_bytes::ByteBuf::from(zip_bytes),
|
||||
}),
|
||||
Err(e) => {
|
||||
@@ -652,11 +660,28 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
}
|
||||
PortfolioMessage::Open => {
|
||||
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
|
||||
responses.add(FrontendMessage::TriggerOpen);
|
||||
responses.add(FrontendMessage::TriggerOpen {
|
||||
filters: vec![
|
||||
FileFilter {
|
||||
name: "Graphite Document".into(),
|
||||
extensions: vec![FILE_EXTENSION.into(), GDD_FILE_EXTENSION.into()],
|
||||
},
|
||||
FileFilter {
|
||||
name: "Image".into(),
|
||||
extensions: vec!["svg".into(), "png".into(), "jpg".into(), "jpeg".into(), "bmp".into(), "gif".into()],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
PortfolioMessage::Import => {
|
||||
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
|
||||
responses.add(FrontendMessage::TriggerImport);
|
||||
// TODO: Also offer the Graphite document filter once importing Graphite documents as nodes is supported
|
||||
responses.add(FrontendMessage::TriggerImport {
|
||||
filters: vec![FileFilter {
|
||||
name: "Image".into(),
|
||||
extensions: vec!["svg".into(), "png".into(), "jpg".into(), "jpeg".into(), "bmp".into(), "gif".into()],
|
||||
}],
|
||||
});
|
||||
}
|
||||
PortfolioMessage::OpenFile { path, content } => {
|
||||
let name = path.file_stem().map(|n| n.to_string_lossy().to_string());
|
||||
@@ -912,7 +937,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
let Some((downstream_node, input_index)) = document
|
||||
.network_interface
|
||||
.outward_wires(&[])
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::primary_output(layer.to_node())))
|
||||
.and_then(|outward_wires| outward_wires.first())
|
||||
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
|
||||
else {
|
||||
|
||||
@@ -93,6 +93,7 @@ impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for Prefe
|
||||
zoom_with_scroll: self.zoom_with_scroll,
|
||||
});
|
||||
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
|
||||
responses.add(ToolMessage::RefreshToolShelf);
|
||||
}
|
||||
PreferencesMessage::ResetToDefaults => {
|
||||
responses.add(PreferencesMessage::Load { preferences: Self::default() });
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::utility_types::DocumentToolData;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
||||
pub struct ToolColorOptions {
|
||||
@@ -85,8 +86,8 @@ impl ToolColorOptions {
|
||||
// In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position.
|
||||
// `FillChoice::None` gives it a neutral starting point.
|
||||
let mixed_color = self.fill_choice.is_none();
|
||||
// Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoiceUI` (with `SRGBA8` colors) for the widget value.
|
||||
let widget_value = FillChoiceUI::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None));
|
||||
// Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoice<SRGBA8>` (with `SRGBA8` colors) for the widget value.
|
||||
let widget_value = FillChoice::<SRGBA8>::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None));
|
||||
let mixed_enabled = self.enabled.is_none();
|
||||
// In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash.
|
||||
// The frontend's click handler sends `true` when the user resolves the mixed state by clicking.
|
||||
@@ -124,6 +125,9 @@ pub struct DrawingToolState {
|
||||
pub miter_limit: Option<f64>,
|
||||
/// Paint order from the selection. `None` = mixed.
|
||||
pub paint_order: Option<PaintOrder>,
|
||||
/// Whether any selected layer has a lone adjacent Fill/Stroke pair for the paint order to swap.
|
||||
/// Stays true with an empty selection, where the radio still sets the default for new shapes.
|
||||
pub paint_order_applicable: bool,
|
||||
/// Dash lengths from the selection. `None` = mixed.
|
||||
pub dash_lengths: Option<Vec<f64>>,
|
||||
/// Dash offset from the selection. `None` = mixed.
|
||||
@@ -149,6 +153,7 @@ impl DrawingToolState {
|
||||
stroke_join: Some(StrokeJoin::default()),
|
||||
miter_limit: Some(4.),
|
||||
paint_order: Some(PaintOrder::default()),
|
||||
paint_order_applicable: true,
|
||||
dash_lengths: Some(Vec::new()),
|
||||
dash_offset: Some(0.),
|
||||
last_synced_selection: Vec::new(),
|
||||
@@ -182,13 +187,21 @@ impl DrawingToolState {
|
||||
cap: self.stroke_cap.unwrap_or_default(),
|
||||
join: self.stroke_join.unwrap_or_default(),
|
||||
join_miter_limit: self.miter_limit.unwrap_or(4.),
|
||||
paint_order: self.paint_order.unwrap_or_default(),
|
||||
dash_lengths: self.effective_dash_lengths(),
|
||||
dash_offset: self.dash_offset.unwrap_or(0.),
|
||||
transform: glam::DAffine2::IDENTITY,
|
||||
};
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
|
||||
}
|
||||
|
||||
/// Queues the paint order rewrite for a freshly created `layer`. The order is the relative position of the
|
||||
/// Stroke and Fill nodes in the chain, so this must run after both the stroke and fill have been applied.
|
||||
pub fn apply_stroke_order_to_new_layer(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
let paint_order = self.paint_order.unwrap_or_default();
|
||||
if paint_order != PaintOrder::default() {
|
||||
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `FillChoice::Solid` from a color.
|
||||
@@ -303,14 +316,23 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
|
||||
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
|
||||
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
|
||||
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
|
||||
let strokes: Vec<_> = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers_except_artboards(&document.network_interface)
|
||||
let layers = graph_modification_utils::paintable_selected_layers(document);
|
||||
|
||||
// The order radio only swaps a layer's lone adjacent Fill/Stroke pair, so it grays out when no selected
|
||||
// layer has one; with nothing selected it still sets the default order for new shapes
|
||||
let paint_order_applicable = layers.is_empty() || layers.iter().any(|&layer| graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface));
|
||||
let mut applicability_changed = false;
|
||||
if drawing.paint_order_applicable != paint_order_applicable {
|
||||
drawing.paint_order_applicable = paint_order_applicable;
|
||||
applicability_changed = true;
|
||||
}
|
||||
|
||||
let strokes: Vec<_> = layers
|
||||
.into_iter()
|
||||
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
|
||||
.collect();
|
||||
if strokes.is_empty() {
|
||||
return false;
|
||||
return applicability_changed;
|
||||
}
|
||||
|
||||
fn unanimous<T: PartialEq + Clone>(values: impl IntoIterator<Item = T>) -> Option<T> {
|
||||
@@ -327,7 +349,7 @@ fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessag
|
||||
let new_dash_lengths = unanimous(strokes.iter().map(|s| &s.dash_lengths)).cloned();
|
||||
let new_dash_offset = unanimous(strokes.iter().map(|s| s.dash_offset));
|
||||
|
||||
let mut changed = false;
|
||||
let mut changed = applicability_changed;
|
||||
|
||||
if drawing.stroke_align != new_align {
|
||||
drawing.stroke_align = new_align;
|
||||
@@ -390,14 +412,9 @@ pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, f
|
||||
}
|
||||
}
|
||||
|
||||
/// True if at least one (non-artboard) layer is currently selected.
|
||||
pub fn has_selection(document: &DocumentMessageHandler) -> bool {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers_except_artboards(&document.network_interface)
|
||||
.next()
|
||||
.is_some()
|
||||
/// True if at least one selected layer can take paint, making the swatches edit the selection instead of the tool's own colors.
|
||||
pub fn has_paintable_selection(document: &DocumentMessageHandler) -> bool {
|
||||
!graph_modification_utils::paintable_selected_layers(document).is_empty()
|
||||
}
|
||||
|
||||
/// Applies a user-picked fill (gradient or solid). With a selection, writes to the layers; with none, pushes a solid to the swap-routed working color slot.
|
||||
@@ -410,7 +427,7 @@ pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: Fill
|
||||
fill.fill_choice = Some(fill_choice.clone());
|
||||
fill.enabled = Some(true);
|
||||
fill.tracks_working_color = false;
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
if document.network_interface.transaction_status() == TransactionStatus::Finished {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
}
|
||||
@@ -425,7 +442,7 @@ pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option<Col
|
||||
drawing.stroke.fill_choice = Some(color.map_or(FillChoice::None, FillChoice::Solid));
|
||||
drawing.stroke.enabled = Some(true);
|
||||
drawing.stroke.tracks_working_color = false;
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
if document.network_interface.transaction_status() == TransactionStatus::Finished {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
}
|
||||
@@ -446,7 +463,7 @@ pub fn apply_fill_enabled(drawing: &mut DrawingToolState, enabled: bool, global:
|
||||
/// Single-slot variant of [`apply_fill_enabled`]. `working_color` is the fallback used when re-ticking or unticking from a mixed state.
|
||||
pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, working_color: Color, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
fill.enabled = Some(enabled);
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
if enabled {
|
||||
@@ -470,7 +487,7 @@ pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, worki
|
||||
/// Toggles the stroke checkbox: mirrors [`apply_fill_enabled`].
|
||||
pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, global: &DocumentToolData, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.stroke.enabled = Some(enabled);
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
if enabled {
|
||||
@@ -492,7 +509,7 @@ pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, globa
|
||||
/// Applies a user-edited stroke weight to the selection, also persisting it as the no-selection default.
|
||||
pub fn apply_line_weight(drawing: &mut DrawingToolState, line_weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.line_weight = Some(line_weight);
|
||||
if !has_selection(document) {
|
||||
if !has_paintable_selection(document) {
|
||||
drawing.default_line_weight = line_weight;
|
||||
}
|
||||
graph_modification_utils::set_stroke_weight_for_selected_layers(line_weight, document, responses);
|
||||
@@ -506,7 +523,7 @@ pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToo
|
||||
|
||||
/// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`].
|
||||
pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) {
|
||||
if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) {
|
||||
if slot.fill_choice.is_some() && (!has_paintable_selection(document) || slot.tracks_working_color) {
|
||||
slot.fill_choice = Some(solid(working_color));
|
||||
}
|
||||
}
|
||||
@@ -523,7 +540,7 @@ pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &Doc
|
||||
pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.colors_swapped = !drawing.colors_swapped;
|
||||
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
}
|
||||
|
||||
@@ -538,7 +555,7 @@ pub fn swap_fill_and_stroke(drawing: &mut DrawingToolState, document: &DocumentM
|
||||
drawing.fill.tracks_working_color = new_fill_tracks;
|
||||
drawing.stroke.tracks_working_color = new_stroke_tracks;
|
||||
|
||||
if has_selection(document) {
|
||||
if has_paintable_selection(document) {
|
||||
// Apply to layers only when we have a concrete value (`None` means mixed, no single value to broadcast).
|
||||
if drawing.fill.is_active()
|
||||
&& let Some(choice) = new_fill
|
||||
@@ -578,7 +595,7 @@ pub enum WeightSyncOutcome {
|
||||
|
||||
/// Inspects the selection and returns how the weight widget should update.
|
||||
pub fn compute_weight_sync(document: &DocumentMessageHandler) -> WeightSyncOutcome {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
let layers = graph_modification_utils::paintable_selected_layers(document);
|
||||
|
||||
if layers.is_empty() {
|
||||
return WeightSyncOutcome::NoSelection;
|
||||
|
||||
+9
-2
@@ -11,6 +11,7 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::{extract
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::ParameterRef;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
@@ -147,7 +148,13 @@ impl RadiusHandle {
|
||||
|
||||
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else {
|
||||
|
||||
// This gizmo serves both Circle and Arc layers, so resolve which node is present to know whose radius parameter to write
|
||||
let (node_id, radius_parameter) = if let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) {
|
||||
(node_id, ParameterRef::from(graphene_std::vector::generator_nodes::circle::RadiusInput))
|
||||
} else if let Some(node_id) = get_arc_id(layer, &document.network_interface) {
|
||||
(node_id, ParameterRef::from(graphene_std::vector::generator_nodes::arc::RadiusInput))
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
|
||||
@@ -165,7 +172,7 @@ impl RadiusHandle {
|
||||
self.previous_mouse_position = input.mouse.position;
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, radius_parameter),
|
||||
input: NodeInput::value(TaggedValue::F64(current_radius + net_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
+6
-6
@@ -13,7 +13,7 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::extract_
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::ParameterRef;
|
||||
use graphene_std::vector::misc::{GridType, dvec2_to_point, get_line_endpoints};
|
||||
use kurbo::{Line, ParamCurveNearest, Rect};
|
||||
use std::collections::VecDeque;
|
||||
@@ -123,7 +123,7 @@ impl RowColumnGizmo {
|
||||
let transform = self.transform_grid(dimensions_delta, self.spacing, grid_type, angles, viewport);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, self.gizmo_type.index()),
|
||||
input_connector: InputConnector::node(node_id, self.gizmo_type.parameter()),
|
||||
input: NodeInput::value(TaggedValue::U32((self.initial_dimension() as i32 + dimensions_to_add).max(1) as u32), false),
|
||||
});
|
||||
|
||||
@@ -411,13 +411,13 @@ impl RowColumnGizmoType {
|
||||
}
|
||||
}
|
||||
|
||||
fn index(&self) -> usize {
|
||||
fn parameter(&self) -> ParameterRef {
|
||||
use graphene_std::vector::generator_nodes::grid::*;
|
||||
|
||||
match self {
|
||||
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => RowsInput::INDEX,
|
||||
RowColumnGizmoType::Left | RowColumnGizmoType::Right => ColumnsInput::INDEX,
|
||||
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not have a mouse_icon"),
|
||||
RowColumnGizmoType::Top | RowColumnGizmoType::Bottom => RowsInput.into(),
|
||||
RowColumnGizmoType::Left | RowColumnGizmoType::Right => ColumnsInput.into(),
|
||||
RowColumnGizmoType::None => panic!("RowColumnGizmoType::None does not reference a grid input"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -13,6 +13,7 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::{extract
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::ParameterRef;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
@@ -194,14 +195,20 @@ impl NumberOfPointsDial {
|
||||
let net_delta = (delta.length() / 25.).round() * sign;
|
||||
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
|
||||
// This dial serves both Star and Polygon layers, so resolve which node is present to know whose sides parameter to write
|
||||
let (node_id, sides_parameter) = if let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) {
|
||||
(node_id, ParameterRef::from(graphene_std::vector::generator_nodes::star::SidesInput))
|
||||
} else if let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) {
|
||||
(node_id, ParameterRef::from(graphene_std::vector::generator_nodes::regular_polygon::SidesInput))
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, sides_parameter),
|
||||
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
+28
-22
@@ -8,12 +8,14 @@ use crate::messages::portfolio::document::{overlays::utility_types::OverlayConte
|
||||
use crate::messages::prelude::FrontendMessage;
|
||||
use crate::messages::prelude::Responses;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, polygon_outline, polygon_vertex_position, star_outline};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::ParameterRef;
|
||||
use graphene_std::vector::generator_nodes::{regular_polygon, star};
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2};
|
||||
|
||||
@@ -30,7 +32,8 @@ pub enum PointRadiusHandleState {
|
||||
pub struct PointRadiusHandle {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
point: u32,
|
||||
radius_index: usize,
|
||||
/// The radius parameter the hovered or dragged handle writes to: a star's first or second radius, or a polygon's radius.
|
||||
radius_parameter: Option<ParameterRef>,
|
||||
snap_radii: Vec<f64>,
|
||||
initial_radius: f64,
|
||||
handle_state: PointRadiusHandleState,
|
||||
@@ -63,7 +66,11 @@ impl PointRadiusHandle {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..2 * sides {
|
||||
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
|
||||
let (radius, radius_parameter) = if i % 2 == 0 {
|
||||
(radius1, ParameterRef::from(star::Radius1Input))
|
||||
} else {
|
||||
(radius2, ParameterRef::from(star::Radius2Input))
|
||||
};
|
||||
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
@@ -73,10 +80,10 @@ impl PointRadiusHandle {
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = radius_index;
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, &radius_parameter);
|
||||
self.radius_parameter = Some(radius_parameter);
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
|
||||
self.initial_radius = radius;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
self.update_state(PointRadiusHandleState::Hover);
|
||||
@@ -100,7 +107,7 @@ impl PointRadiusHandle {
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = 2;
|
||||
self.radius_parameter = Some(regular_polygon::RadiusInput.into());
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii.clear();
|
||||
@@ -329,21 +336,20 @@ impl PointRadiusHandle {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec<f64> {
|
||||
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_parameter: &ParameterRef) -> Vec<f64> {
|
||||
let mut snap_radii = Vec::new();
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::star::IDENTIFIER))
|
||||
else {
|
||||
let Some(parameters) = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(star::IDENTIFIER) else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (node_inputs[2].as_value(), node_inputs[3].as_value()) else {
|
||||
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (parameters.value(star::Radius1Input), parameters.value(star::Radius2Input)) else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
let other_radius = if radius_index == 3 { radius_1 } else { radius_2 };
|
||||
let other_radius = if *radius_parameter == ParameterRef::from(star::Radius2Input) { radius_1 } else { radius_2 };
|
||||
|
||||
let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
|
||||
let Some(&TaggedValue::U32(sides)) = parameters.value(star::SidesInput) else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
@@ -415,14 +421,15 @@ impl PointRadiusHandle {
|
||||
|
||||
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(radius_parameter) = self.radius_parameter.clone() else { return };
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
// The stored parameter names the node it belongs to (Star or Polygon), so locate that same node on the layer
|
||||
let Some(node_id) = NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(radius_parameter.node_identifier.clone())) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let center = viewport_transform.transform_point2(DVec2::ZERO);
|
||||
let radius_index = self.radius_index;
|
||||
|
||||
let original_radius = self.initial_radius;
|
||||
|
||||
@@ -436,7 +443,7 @@ impl PointRadiusHandle {
|
||||
|
||||
self.update_state(PointRadiusHandleState::Dragging);
|
||||
|
||||
self.check_if_radius_flipped(original_radius, new_radius, document, layer, radius_index);
|
||||
self.check_if_radius_flipped(original_radius, new_radius, document, layer, &radius_parameter);
|
||||
|
||||
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
|
||||
net_delta = snapped_delta;
|
||||
@@ -444,29 +451,28 @@ impl PointRadiusHandle {
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, radius_index),
|
||||
input_connector: InputConnector::node(node_id, radius_parameter),
|
||||
input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
fn check_if_radius_flipped(&mut self, original_radius: f64, new_radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) {
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::star::IDENTIFIER))
|
||||
else {
|
||||
fn check_if_radius_flipped(&mut self, original_radius: f64, new_radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_parameter: &ParameterRef) {
|
||||
let Some(parameters) = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(star::IDENTIFIER) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (node_inputs[2].as_value(), node_inputs[3].as_value()) else {
|
||||
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (parameters.value(star::Radius1Input), parameters.value(star::Radius2Input)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let other_radius = if radius_index == 3 { radius_1 } else { radius_2 };
|
||||
let other_radius = if *radius_parameter == ParameterRef::from(star::Radius2Input) { radius_1 } else { radius_2 };
|
||||
|
||||
let flipped = (other_radius.is_sign_positive() && original_radius.is_sign_negative() && new_radius.is_sign_positive())
|
||||
|| (other_radius.is_sign_negative() && original_radius.is_sign_positive() && new_radius.is_sign_negative());
|
||||
|
||||
if flipped {
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -12,8 +12,7 @@ use crate::messages::tool::common_functionality::shapes::spiral_shape::calculate
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
|
||||
use graphene_std::vector::algorithms::shapes::{calculate_growth_factor, spiral_point};
|
||||
use graphene_std::vector::misc::SpiralType;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::TAU;
|
||||
@@ -190,15 +189,15 @@ impl SpiralTurns {
|
||||
let new_outer_radius = (self.initial_outer_radius + outer_radius_change * sign).max(0.1);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, StartAngleInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, StartAngleInput),
|
||||
input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, TurnsInput),
|
||||
input: NodeInput::value(TaggedValue::F64(new_turns), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(new_outer_radius), false),
|
||||
});
|
||||
}
|
||||
@@ -207,11 +206,11 @@ impl SpiralTurns {
|
||||
let new_outer_radius = (self.initial_outer_radius + outer_radius_change).max(0.1);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, TurnsInput),
|
||||
input: NodeInput::value(TaggedValue::F64(new_turns), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(new_outer_radius), false),
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -332,11 +332,11 @@ impl SweepAngleGizmo {
|
||||
self.snap_angles = Self::calculate_snap_angles();
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::arc::StartAngleInput),
|
||||
input: NodeInput::value(TaggedValue::F64(start_angle), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 3),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::arc::SweepAngleInput),
|
||||
input: NodeInput::value(TaggedValue::F64(sweep_angle), false),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{self, DefinitionIdentifier};
|
||||
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::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{CPU, GPU, Image, Raster};
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
|
||||
use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType};
|
||||
use graphene_std::vector::{Gradient, GradientForm, GradientRamp, GradientSettings, PointId, SegmentId, VectorModificationType};
|
||||
use graphene_std::{NodeParameter, ParameterRef};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
||||
@@ -92,29 +91,29 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
|
||||
parent: first_layer,
|
||||
});
|
||||
responses.add(NodeGraphMessage::ConnectUpstreamOutputToInput {
|
||||
downstream_input: InputConnector::node(second_layer.to_node(), 1),
|
||||
input_connector: InputConnector::node(merge_node_id, 1),
|
||||
downstream_input: InputConnector::layer_secondary_input(second_layer.to_node()),
|
||||
input_connector: InputConnector::layer_secondary_input(merge_node_id),
|
||||
});
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![second_layer.to_node()],
|
||||
delete_children: false,
|
||||
});
|
||||
|
||||
// Add a Flatten Path node after the merge
|
||||
let flatten_node_id = NodeId::new();
|
||||
let flatten_node = document_node_definitions::resolve_proto_node_type(graphene_std::vector::flatten_path::IDENTIFIER)
|
||||
.expect("Failed to create flatten node")
|
||||
// Add a Combine Paths node after the merge
|
||||
let combine_paths_node_id = NodeId::new();
|
||||
let combine_paths_node = document_node_definitions::resolve_proto_node_type(graphene_std::vector::combine_paths::IDENTIFIER)
|
||||
.expect("Failed to create combine paths node")
|
||||
.default_node_template();
|
||||
responses.add(NodeGraphMessage::InsertNode {
|
||||
node_id: flatten_node_id,
|
||||
node_template: Box::new(flatten_node),
|
||||
node_id: combine_paths_node_id,
|
||||
node_template: Box::new(combine_paths_node),
|
||||
});
|
||||
responses.add(NodeGraphMessage::MoveNodeToChainStart {
|
||||
node_id: flatten_node_id,
|
||||
node_id: combine_paths_node_id,
|
||||
parent: first_layer,
|
||||
});
|
||||
|
||||
// Add a path node after the flatten node
|
||||
// Add a path node after the combine paths node
|
||||
let path_node_id = NodeId::new();
|
||||
let path_node = document_node_definitions::resolve_network_node_type("Path")
|
||||
.expect("Failed to create path node")
|
||||
@@ -169,7 +168,7 @@ pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifie
|
||||
let transform = document.metadata().transform_to_document(layer);
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
|
||||
|
||||
let segment = vector.segment_bezier_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
|
||||
let segment = vector.segment_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
|
||||
let Some((segment, _, mut segment_start_point, mut segment_end_point)) = segment else {
|
||||
log::error!("Could not get the segment for second_endpoint.");
|
||||
return;
|
||||
@@ -207,15 +206,6 @@ pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifie
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
|
||||
/// Create a new vector layer.
|
||||
pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
|
||||
let insert_index = 0;
|
||||
responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index });
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
|
||||
|
||||
LayerNodeIdentifier::new_unchecked(id)
|
||||
}
|
||||
|
||||
/// Create a new bitmap layer.
|
||||
pub fn new_image_layer(image: Image<Color>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
|
||||
let insert_index = 0;
|
||||
@@ -250,9 +240,7 @@ pub fn new_custom(id: NodeId, nodes: Vec<(NodeId, NodeTemplate)>, parent: LayerN
|
||||
pub fn get_origin(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<DVec2> {
|
||||
use graphene_std::transform_nodes::transform::*;
|
||||
|
||||
if let TaggedValue::DVec2(origin) =
|
||||
NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER), TranslationInput::INDEX)?
|
||||
{
|
||||
if let TaggedValue::DVec2(origin) = NodeGraphLayer::new(layer, network_interface).parameter_value(TranslationInput)? {
|
||||
Some(*origin)
|
||||
} else {
|
||||
None
|
||||
@@ -274,64 +262,196 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN
|
||||
pub fn get_fill_node_id_with_direct_fill_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
matches!(fill_node.inputs.get(graphene_std::vector::fill::FillInput::INDEX)?, NodeInput::Value { .. }).then_some(fill_node_id)
|
||||
matches!(fill_node.input(graphene_std::vector::fill::PaintInput)?, NodeInput::Value { .. }).then_some(fill_node_id)
|
||||
}
|
||||
|
||||
/// Determine the input connector where the gradient chain enters the layer.
|
||||
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
|
||||
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
|
||||
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::INDEX)
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::PaintInput)
|
||||
} else {
|
||||
InputConnector::node(layer.to_node(), 1)
|
||||
InputConnector::layer_secondary_input(layer.to_node())
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find a "Gradient Value" node that is connected to a "Fill" node, or to a layer directly.
|
||||
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
/// Try to find the paint value node feeding a 'Fill' node, or a layer directly.
|
||||
fn get_upstream_paint_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, identifier: ProtoNodeIdentifier) -> Option<NodeId> {
|
||||
let target_input = gradient_chain_target_input(layer, network_interface);
|
||||
let walk_from = network_interface.upstream_output_connector(&target_input, &[])?.node_id()?;
|
||||
let reference = DefinitionIdentifier::ProtoNode(identifier);
|
||||
|
||||
network_interface
|
||||
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
|
||||
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
|
||||
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
|
||||
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&reference))
|
||||
}
|
||||
|
||||
/// Try to find a 'Gradient Value' node that is connected to a 'Fill' node, or to a layer directly.
|
||||
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
get_upstream_paint_value_node_id(layer, network_interface, graphene_std::math_nodes::gradient_value::IDENTIFIER)
|
||||
}
|
||||
|
||||
/// A whole-expanse paint to clear off a 'Merge' layer so a paint tool can start its own chain there.
|
||||
pub struct ReplaceablePaintChain {
|
||||
/// Where the fresh paint attaches, which is also the link severed to reach it.
|
||||
pub attachment_input: InputConnector,
|
||||
/// The paint's nodes, ordered downstream to upstream, empty when there is nothing to clear away.
|
||||
pub nodes: Vec<NodeId>,
|
||||
}
|
||||
|
||||
/// The whole-expanse paint on a 'Merge' layer that a paint tool may take over, or `None`
|
||||
/// when the layer isn't one or its chain holds anything the tools didn't put there.
|
||||
/// The walk stops at the first node the rest of the graph also draws from, since repainting through it would change
|
||||
/// what those other consumers see, and it passes through 'Transform' nodes so a swap keeps the layer's placement.
|
||||
pub fn replaceable_paint_chain(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<ReplaceablePaintChain> {
|
||||
if !network_interface.is_merge(&layer.to_node(), &[]) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// A generator discards its primary input, so the chain ends at whichever one is reached
|
||||
let generators = [graphene_std::math_nodes::color_value::IDENTIFIER, graphene_std::math_nodes::gradient_value::IDENTIFIER];
|
||||
let setters = [
|
||||
graphene_std::math_nodes::gradient_form::IDENTIFIER,
|
||||
graphene_std::math_nodes::gradient_spread::IDENTIFIER,
|
||||
graphene_std::math_nodes::gradient_positions::IDENTIFIER,
|
||||
graphene_std::math_nodes::gradient_midpoints::IDENTIFIER,
|
||||
];
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
let mut input = InputConnector::layer_secondary_input(layer.to_node());
|
||||
// Trails the walk over the 'Transform' nodes that survive, then holds still once the paint to clear away begins
|
||||
let mut attachment_input = input;
|
||||
|
||||
while let Some(upstream) = network_interface.upstream_output_connector(&input, &[]) {
|
||||
let node_id = upstream.node_id()?;
|
||||
|
||||
// Arriving here means this layer consumes the node, so a further consumer makes it shared
|
||||
let shared = network_interface
|
||||
.with_outward_wires(&[], |outward_wires| outward_wires.get(&OutputConnector::primary_output(node_id)).is_some_and(|inputs| inputs.len() > 1))
|
||||
.unwrap_or(true);
|
||||
if shared {
|
||||
break;
|
||||
}
|
||||
|
||||
let Some(DefinitionIdentifier::ProtoNode(identifier)) = network_interface.reference(&node_id, &[]) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if identifier == graphene_std::transform_nodes::transform::IDENTIFIER {
|
||||
input = InputConnector::primary_input(node_id);
|
||||
if nodes.is_empty() {
|
||||
attachment_input = input;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let generator = generators.contains(&identifier);
|
||||
if !generator && !setters.contains(&identifier) {
|
||||
return None;
|
||||
}
|
||||
|
||||
nodes.push(node_id);
|
||||
|
||||
if generator {
|
||||
break;
|
||||
}
|
||||
|
||||
input = InputConnector::primary_input(node_id);
|
||||
}
|
||||
|
||||
Some(ReplaceablePaintChain { attachment_input, nodes })
|
||||
}
|
||||
|
||||
/// Try to find a 'Color Value' node that is connected to a 'Fill' node, or to a layer directly.
|
||||
pub fn get_upstream_color_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
get_upstream_paint_value_node_id(layer, network_interface, graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||
}
|
||||
|
||||
/// Get the node connected to Fill's fill input, if any.
|
||||
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::INDEX)? else {
|
||||
let NodeInput::Node { node_id, .. } = fill_node.input(graphene_std::vector::fill::PaintInput)? else {
|
||||
return None;
|
||||
};
|
||||
Some(*node_id)
|
||||
}
|
||||
|
||||
/// The ramp held by the 'Gradient Value' node feeding a layer's chain, which carries the whole-ramp settings.
|
||||
fn get_chain_source_gradient_ramp(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<&GradientRamp> {
|
||||
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
||||
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(ramp)
|
||||
}
|
||||
|
||||
/// The whole-ramp settings baked into the 'Gradient Value' node feeding a layer's chain.
|
||||
pub fn get_chain_source_gradient_settings(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSettings> {
|
||||
Some(get_chain_source_gradient_ramp(layer, network_interface)?.into())
|
||||
}
|
||||
|
||||
/// Get the gradient stops of a layer, if any.
|
||||
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> {
|
||||
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
// Try to find the gradient stops value that is created by a Fill node first
|
||||
if let Some(fill_node_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
||||
return network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&fill_node_id)
|
||||
.and_then(|node| node.inputs.get(graphene_std::vector::fill::FillInput::INDEX))
|
||||
.and_then(|node| node.input(graphene_std::vector::fill::PaintInput))
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::Gradient(gradient) = value { Some(gradient.clone()) } else { None });
|
||||
.and_then(|value| if let TaggedValue::GradientRamp(ramp) = value { Some(Gradient::from(ramp)) } else { None });
|
||||
}
|
||||
|
||||
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
||||
let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
|
||||
let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(stops.clone())
|
||||
let mut stops = Gradient::from(ramp);
|
||||
|
||||
// The chain's stop placement comes from the closest-to-layer 'Gradient Positions'/'Gradient Midpoints' nodes,
|
||||
// matching the runtime where each later node overwrites the whole attribute
|
||||
let target_input = gradient_chain_target_input(layer, network_interface);
|
||||
let walk_from = network_interface.upstream_output_connector(&target_input, &[])?.node_id()?;
|
||||
let positions_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_positions::IDENTIFIER);
|
||||
let midpoints_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_midpoints::IDENTIFIER);
|
||||
|
||||
let (mut positions_pending, mut midpoints_pending) = (true, true);
|
||||
for node_id in network_interface
|
||||
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
|
||||
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
|
||||
{
|
||||
let Some(reference) = network_interface.reference(&node_id, &[]) else { continue };
|
||||
let node = network_interface.document_network().nodes.get(&node_id);
|
||||
|
||||
if positions_pending && reference == positions_reference {
|
||||
positions_pending = false;
|
||||
if let Some(TaggedValue::F64Array(positions)) = node
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_positions::PositionsInput))
|
||||
.and_then(|input| input.as_value())
|
||||
{
|
||||
stops.set_positions(positions);
|
||||
}
|
||||
} else if midpoints_pending && reference == midpoints_reference {
|
||||
midpoints_pending = false;
|
||||
if let Some(TaggedValue::F64Array(midpoints)) = node
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_midpoints::MidpointsInput))
|
||||
.and_then(|input| input.as_value())
|
||||
{
|
||||
stops.set_midpoints(midpoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(stops)
|
||||
}
|
||||
|
||||
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<GradientStops>`
|
||||
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<Gradient>`
|
||||
/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
|
||||
/// transform with the [0,1]² → bounding-box mapping.
|
||||
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
|
||||
pub fn gradient_to_viewport_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
|
||||
let metadata = network_interface.document_metadata();
|
||||
@@ -347,6 +467,14 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
|
||||
metadata.transform_to_viewport(layer)
|
||||
}
|
||||
|
||||
/// Tooltip description for a "Reverse Direction" gradient button, phrased for the given Gradient Form.
|
||||
pub fn reverse_direction_tooltip_description(gradient_form: GradientForm) -> &'static str {
|
||||
match gradient_form {
|
||||
GradientForm::Radial => "Reverse which end the gradient radiates from.",
|
||||
GradientForm::Linear => "Swap the start and end points of the gradient line.",
|
||||
}
|
||||
}
|
||||
|
||||
/// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely
|
||||
/// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives.
|
||||
pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
|
||||
@@ -361,11 +489,10 @@ pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
|
||||
|
||||
/// Get the current fill of a layer from the closest "Fill" node.
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||
let &TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::INDEX)?.as_value()? else {
|
||||
let TaggedValue::Color(color) = NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::fill::PaintInput)? else {
|
||||
return None;
|
||||
};
|
||||
color
|
||||
Some(*color)
|
||||
}
|
||||
|
||||
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
||||
@@ -471,42 +598,35 @@ pub fn get_text<'a>(
|
||||
fonts: &FontsMessageHandler,
|
||||
resources: &ResourceMessageHandler,
|
||||
) -> Option<(&'a String, Font, TypesettingConfig)> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?;
|
||||
use graphene_std::text::text;
|
||||
let parameters = NodeGraphLayer::new(layer, network_interface).find_node_parameters(text::IDENTIFIER)?;
|
||||
|
||||
let Some(TaggedValue::String(text)) = inputs.get(graphene_std::text::text::TextInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let font = match inputs.get(graphene_std::text::text::FontInput::INDEX)?.as_value() {
|
||||
let Some(TaggedValue::String(text)) = parameters.value(text::TextInput) else { return None };
|
||||
let font = match parameters.value(text::FontInput) {
|
||||
Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(),
|
||||
_ => Font::default(),
|
||||
};
|
||||
let Some(&TaggedValue::F64(font_size)) = inputs.get(graphene_std::text::text::SizeInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::F64(font_size)) = parameters.value(text::SizeInput) else { return None };
|
||||
let Some(&TaggedValue::F64(line_height_ratio)) = parameters.value(text::LineHeightInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(line_height_ratio)) = inputs.get(graphene_std::text::text::LineHeightInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::F64(letter_spacing)) = parameters.value(text::LetterSpacingInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(letter_spacing)) = inputs.get(graphene_std::text::text::LetterSpacingInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::Bool(has_max_width)) = parameters.value(text::HasMaxWidthInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::Bool(has_max_width)) = inputs.get(graphene_std::text::text::HasMaxWidthInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::F64(max_width)) = parameters.value(text::MaxWidthInput) else { return None };
|
||||
let Some(&TaggedValue::Bool(has_max_height)) = parameters.value(text::HasMaxHeightInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(max_width)) = inputs.get(graphene_std::text::text::MaxWidthInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::F64(max_height)) = parameters.value(text::MaxHeightInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::Bool(has_max_height)) = inputs.get(graphene_std::text::text::HasMaxHeightInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(max_height)) = inputs.get(graphene_std::text::text::MaxHeightInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::F64(letter_tilt)) = inputs.get(graphene_std::text::text::LetterTiltInput::INDEX)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::TextAlign(align)) = inputs.get(graphene_std::text::text::AlignInput::INDEX)?.as_value() else {
|
||||
let Some(&TaggedValue::F64(letter_tilt)) = parameters.value(text::LetterTiltInput) else {
|
||||
return None;
|
||||
};
|
||||
let Some(&TaggedValue::TextAlign(align)) = parameters.value(text::AlignInput) else { return None };
|
||||
|
||||
let typesetting = TypesettingConfig {
|
||||
font_size,
|
||||
@@ -521,8 +641,7 @@ pub fn get_text<'a>(
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), weight_node_input_index)? {
|
||||
if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::stroke::WeightInput)? {
|
||||
Some(*width)
|
||||
} else {
|
||||
None
|
||||
@@ -544,36 +663,34 @@ pub struct StrokeOptionsState {
|
||||
/// Reads the non-color stroke option inputs from a layer's Stroke proto node. Returns `None` when the layer has no Stroke node.
|
||||
/// Inputs that aren't a static value (e.g. wired to another node) fall back to per-field defaults so the layer still participates in the sync.
|
||||
pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<StrokeOptionsState> {
|
||||
let stroke = &DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let layer_view = NodeGraphLayer::new(layer, network_interface);
|
||||
layer_view.upstream_node_id_from_name(stroke)?;
|
||||
let read = |index: usize| layer_view.find_input(stroke, index);
|
||||
use graphene_std::vector::stroke;
|
||||
let parameters = NodeGraphLayer::new(layer, network_interface).find_node_parameters(stroke::IDENTIFIER)?;
|
||||
|
||||
let align = match read(graphene_std::vector::stroke::AlignInput::INDEX) {
|
||||
let align = match parameters.value(stroke::AlignInput) {
|
||||
Some(TaggedValue::StrokeAlign(value)) => *value,
|
||||
_ => StrokeAlign::default(),
|
||||
};
|
||||
let cap = match read(graphene_std::vector::stroke::CapInput::INDEX) {
|
||||
let cap = match parameters.value(stroke::CapInput) {
|
||||
Some(TaggedValue::StrokeCap(value)) => *value,
|
||||
_ => StrokeCap::default(),
|
||||
};
|
||||
let join = match read(graphene_std::vector::stroke::JoinInput::INDEX) {
|
||||
let join = match parameters.value(stroke::JoinInput) {
|
||||
Some(TaggedValue::StrokeJoin(value)) => *value,
|
||||
_ => StrokeJoin::default(),
|
||||
};
|
||||
let miter_limit = match read(graphene_std::vector::stroke::MiterLimitInput::INDEX) {
|
||||
let miter_limit = match parameters.value(stroke::MiterLimitInput) {
|
||||
Some(TaggedValue::F64(value)) => *value,
|
||||
_ => 4.,
|
||||
};
|
||||
let paint_order = match read(graphene_std::vector::stroke::PaintOrderInput::INDEX) {
|
||||
let paint_order = match parameters.value(stroke::PaintOrderInput) {
|
||||
Some(TaggedValue::PaintOrder(value)) => *value,
|
||||
_ => PaintOrder::default(),
|
||||
};
|
||||
let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::INDEX) {
|
||||
Some(TaggedValue::F64Array(value)) => value.clone(),
|
||||
let dash_lengths = match parameters.value(stroke::DashPatternInput) {
|
||||
Some(TaggedValue::DashPattern(lengths)) => lengths.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let dash_offset = match read(graphene_std::vector::stroke::DashOffsetInput::INDEX) {
|
||||
let dash_offset = match parameters.value(stroke::DashOffsetInput) {
|
||||
Some(TaggedValue::F64(value)) => *value,
|
||||
_ => 0.,
|
||||
};
|
||||
@@ -594,27 +711,30 @@ pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
||||
}
|
||||
|
||||
/// Stroke weight of the first selected non-artboard layer, used by tool control bars to mirror the selection's weight.
|
||||
/// Returns `Some(0.)` if the layer has no Stroke node so the widget reads "0 px", and `None` only when no layer is selected.
|
||||
pub fn first_selected_stroke_weight(document: &DocumentMessageHandler) -> Option<f64> {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers_except_artboards(&document.network_interface)
|
||||
.next()
|
||||
.map(|layer| get_stroke_width(layer, &document.network_interface).unwrap_or(0.))
|
||||
/// Whether the paint order can act on the layer: it rides the Stroke node's paint order input, so any layer with a Stroke node qualifies.
|
||||
pub fn stroke_paint_order_applicable(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||
get_stroke_id(layer, network_interface).is_some()
|
||||
}
|
||||
|
||||
/// The paint order of a layer's stroke, read from the Stroke node's paint order input rather than from the chain topology.
|
||||
pub fn get_stroke_paint_order(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> PaintOrder {
|
||||
match NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::stroke::PaintOrderInput) {
|
||||
Some(TaggedValue::PaintOrder(paint_order)) => *paint_order,
|
||||
_ => PaintOrder::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their
|
||||
/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new
|
||||
/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes.
|
||||
pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
for layer in layers {
|
||||
for layer in paintable_selected_layers(document) {
|
||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||
let input_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
let value = TaggedValue::F64(weight);
|
||||
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
||||
responses.add(NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: graphene_std::vector::stroke::WeightInput::INDEX,
|
||||
value: TaggedValue::F64(weight).into(),
|
||||
});
|
||||
} else if weight > 0. {
|
||||
let color = Some(Color::BLACK);
|
||||
let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight);
|
||||
@@ -625,9 +745,9 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
|
||||
|
||||
/// A Fill node's decoded gradient inputs, with the transform kept in its raw form (not yet baked into `start`/`end`).
|
||||
pub struct FillNodeGradient {
|
||||
pub stops: GradientStops,
|
||||
pub gradient_type: GradientType,
|
||||
pub spread_method: GradientSpreadMethod,
|
||||
pub stops: Gradient,
|
||||
pub gradient_form: GradientForm,
|
||||
pub settings: GradientSettings,
|
||||
pub transform: DAffine2,
|
||||
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
|
||||
pub transform_is_value: bool,
|
||||
@@ -637,36 +757,39 @@ pub struct FillNodeGradient {
|
||||
pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option<FillNodeGradient> {
|
||||
use graphene_std::vector::fill;
|
||||
|
||||
let TaggedValue::Gradient(stops) = fill_node.inputs.get(fill::FillInput::INDEX)?.as_value()? else {
|
||||
let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::PaintInput)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
let gradient_type = match fill_node.inputs.get(fill::GradientTypeInput::INDEX).and_then(|input| input.as_value()) {
|
||||
Some(&TaggedValue::GradientType(value)) => value,
|
||||
_ => GradientType::default(),
|
||||
let settings = GradientSettings::from(ramp);
|
||||
let stops = Gradient::from(ramp);
|
||||
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
|
||||
Some(&TaggedValue::GradientForm(value)) => value,
|
||||
_ => GradientForm::default(),
|
||||
};
|
||||
let spread_method = match fill_node.inputs.get(fill::SpreadMethodInput::INDEX).and_then(|input| input.as_value()) {
|
||||
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
|
||||
_ => GradientSpreadMethod::default(),
|
||||
};
|
||||
let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value());
|
||||
let transform = match transform_input {
|
||||
Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())),
|
||||
let has_transform = matches!(fill_node.input(fill::HasTransformInput).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true)));
|
||||
let transform_input = fill_node.input(fill::TransformInput).and_then(|input| input.as_value());
|
||||
let transform = match (has_transform, transform_input) {
|
||||
(true, Some(&TaggedValue::DAffine2(value))) => value,
|
||||
(false, _) => initial_gradient_transform_for_bounding_box(bounding_box()),
|
||||
_ => DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
Some(FillNodeGradient {
|
||||
stops: stops.clone(),
|
||||
gradient_type,
|
||||
spread_method,
|
||||
stops,
|
||||
gradient_form,
|
||||
settings,
|
||||
transform,
|
||||
transform_is_value: transform_input.is_some(),
|
||||
})
|
||||
}
|
||||
/// Returns the stroke color from a layer's upstream Stroke node.
|
||||
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
|
||||
let color_index = graphene_std::vector::stroke::PaintInput::INDEX;
|
||||
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?;
|
||||
if let TaggedValue::Color(color) = tagged { Some(*color) } else { None }
|
||||
let tagged = NodeGraphLayer::new(layer, network_interface).parameter_value(graphene_std::vector::stroke::PaintInput)?;
|
||||
match tagged {
|
||||
TaggedValue::Color(color) => Some(Some(*color)),
|
||||
value if value.is_no_paint() => Some(None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregated fill state across all selected non-artboard layers.
|
||||
@@ -685,12 +808,21 @@ pub struct SelectedStrokeState {
|
||||
pub optional_color: Option<Option<Color>>,
|
||||
}
|
||||
|
||||
/// Reads the fill state across all selected non-artboard layers, including whether their enabled states or colors differ.
|
||||
/// "Enabled" tracks node attachment: a layer counts as enabled whenever a Fill node is attached, even when that fill's value is [`FillChoice::None`].
|
||||
/// Unticked means there is no Fill node. Returns `None` only when no layer is selected.
|
||||
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
|
||||
/// The selected layers (artboards excluded) whose chains can host Fill and Stroke nodes.
|
||||
/// The tool options bar's paint widgets treat these as the whole selection, so other layers act as if unselected.
|
||||
pub fn paintable_selected_layers(document: &DocumentMessageHandler) -> Vec<LayerNodeIdentifier> {
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| {
|
||||
selected_nodes
|
||||
.selected_layers_except_artboards(&document.network_interface)
|
||||
.filter(|layer| document.network_interface.layer_hosts_paint_nodes(&layer.to_node(), &[]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reads the fill state across all selected non-artboard layers, including whether their enabled states or colors differ.
|
||||
/// "Enabled" tracks node attachment: a layer counts as enabled whenever a Fill node is attached, even when that fill's value is the no-paint choice.
|
||||
/// Unticked means there is no Fill node. Returns `None` only when no paintable layer is selected.
|
||||
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
|
||||
let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| {
|
||||
let Some(fill_node_id) = get_fill_id(layer, &document.network_interface) else {
|
||||
return (false, FillChoice::None);
|
||||
};
|
||||
@@ -698,9 +830,10 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<Selected
|
||||
let fill_choice = (|| {
|
||||
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
|
||||
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::INDEX)?.as_value()? {
|
||||
&TaggedValue::Color(color) => Some(color.map_or(FillChoice::None, FillChoice::Solid)),
|
||||
TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())),
|
||||
match fill_node.input(graphene_std::vector::fill::PaintInput)?.as_value()? {
|
||||
TaggedValue::Color(color) => Some(FillChoice::Solid(*color)),
|
||||
TaggedValue::GradientRamp(ramp) => Some(FillChoice::Gradient(ramp.clone())),
|
||||
value if value.is_no_paint() => Some(FillChoice::None),
|
||||
_ => None,
|
||||
}
|
||||
})()
|
||||
@@ -740,8 +873,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<Selected
|
||||
/// "Enabled" tracks node attachment: a layer counts as enabled whenever a Stroke node is attached, even when that stroke's color is `None`.
|
||||
/// Unticked means there is no Stroke node. Returns `None` only when no layer is selected.
|
||||
pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option<SelectedStrokeState> {
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| {
|
||||
let mut per_layer = paintable_selected_layers(document).into_iter().map(|layer| {
|
||||
if get_stroke_id(layer, &document.network_interface).is_none() {
|
||||
return (false, None);
|
||||
}
|
||||
@@ -778,38 +910,30 @@ pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option<Select
|
||||
|
||||
/// Sets the fill on all selected non-artboard layers, preserving gradient transform data when the layer already has a gradient fill.
|
||||
pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
for layer in layers {
|
||||
for layer in paintable_selected_layers(document) {
|
||||
match &fill_choice {
|
||||
FillChoice::None => responses.add(GraphOperationMessage::FillColorSet { layer, color: None }),
|
||||
FillChoice::Solid(color) => responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) }),
|
||||
FillChoice::Gradient(stops) => {
|
||||
let fill_node = NodeGraphLayer::new(layer, &document.network_interface)
|
||||
.upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))
|
||||
.and_then(|id| document.network_interface.document_network().nodes.get(&id));
|
||||
FillChoice::Gradient(ramp) => {
|
||||
use graphene_std::vector::fill;
|
||||
let fill_parameters = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(fill::IDENTIFIER);
|
||||
|
||||
let read = |index: usize| fill_node.and_then(|node| node.inputs.get(index)).and_then(|input| input.as_value());
|
||||
|
||||
let gradient_type = match read(graphene_std::vector::fill::GradientTypeInput::INDEX) {
|
||||
Some(TaggedValue::GradientType(value)) => *value,
|
||||
_ => GradientType::default(),
|
||||
let gradient_form = match fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::GradientFormInput)) {
|
||||
Some(TaggedValue::GradientForm(value)) => *value,
|
||||
_ => GradientForm::default(),
|
||||
};
|
||||
let spread_method = match read(graphene_std::vector::fill::SpreadMethodInput::INDEX) {
|
||||
Some(TaggedValue::GradientSpreadMethod(value)) => *value,
|
||||
_ => GradientSpreadMethod::default(),
|
||||
};
|
||||
let transform = match read(graphene_std::vector::fill::TransformInput::INDEX) {
|
||||
Some(TaggedValue::OptionalDAffine2(value)) => {
|
||||
value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)))
|
||||
}
|
||||
let has_transform = matches!(fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::HasTransformInput)), Some(TaggedValue::Bool(true)));
|
||||
let transform = match (has_transform, fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::TransformInput))) {
|
||||
(true, Some(TaggedValue::DAffine2(value))) => *value,
|
||||
(false, _) => initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)),
|
||||
_ => DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
responses.add(GraphOperationMessage::FillGradientSet {
|
||||
layer,
|
||||
gradient: stops.clone(),
|
||||
gradient_type,
|
||||
spread_method,
|
||||
gradient: Gradient::from(ramp),
|
||||
gradient_form,
|
||||
gradient_settings: ramp.into(),
|
||||
transform,
|
||||
});
|
||||
}
|
||||
@@ -821,12 +945,13 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
|
||||
/// the provided `weight`, so picking any color (including `None`) from an unticked stroke control bar entry both attaches
|
||||
/// the Stroke node and applies the chosen color.
|
||||
pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
for layer in layers {
|
||||
for layer in paintable_selected_layers(document) {
|
||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||
let input_index = graphene_std::vector::stroke::PaintInput::INDEX;
|
||||
let value = TaggedValue::Color(color);
|
||||
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
||||
responses.add(NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: graphene_std::vector::stroke::PaintInput::INDEX,
|
||||
value: color.map_or_else(TaggedValue::no_paint, TaggedValue::Color).into(),
|
||||
});
|
||||
} else {
|
||||
let stroke = graphene_std::vector::style::Stroke::new(weight);
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
|
||||
@@ -836,8 +961,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
|
||||
|
||||
/// Removes the Fill node from all selected non-artboard layers.
|
||||
pub fn remove_fill_for_selected_layers(document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
for layer in layers {
|
||||
for layer in paintable_selected_layers(document) {
|
||||
if let Some(node_id) = get_fill_id(layer, &document.network_interface) {
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![node_id],
|
||||
@@ -851,8 +975,7 @@ pub fn remove_fill_for_selected_layers(document: &DocumentMessageHandler, respon
|
||||
|
||||
/// Removes the Stroke node from all selected non-artboard layers.
|
||||
pub fn remove_stroke_for_selected_layers(document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
for layer in layers {
|
||||
for layer in paintable_selected_layers(document) {
|
||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![node_id],
|
||||
@@ -864,28 +987,26 @@ pub fn remove_stroke_for_selected_layers(document: &DocumentMessageHandler, resp
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
|
||||
/// Reads a specific input from the matching proto node on the first selected non-artboard layer that has one.
|
||||
/// Reads the given parameter from its proto node on the first selected non-artboard layer that has one.
|
||||
/// Used by tool control bars to mirror per-shape parameters (sides, arc type, turns, etc.) from the selection
|
||||
/// into the control bar's input widget state without each call site re-implementing the layer iteration.
|
||||
pub fn first_selected_proto_node_input(document: &DocumentMessageHandler, identifier: graph_craft::ProtoNodeIdentifier, input_index: usize) -> Option<&TaggedValue> {
|
||||
let identifier = DefinitionIdentifier::ProtoNode(identifier);
|
||||
pub fn first_selected_parameter(document: &DocumentMessageHandler, parameter: impl Into<ParameterRef>) -> Option<&TaggedValue> {
|
||||
let parameter = parameter.into();
|
||||
let identifier = DefinitionIdentifier::ProtoNode(parameter.node_identifier);
|
||||
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers_except_artboards(&document.network_interface)
|
||||
.find_map(|layer| NodeGraphLayer::new(layer, &document.network_interface).find_input(&identifier, input_index))
|
||||
.find_map(|layer| NodeGraphLayer::new(layer, &document.network_interface).find_input(&identifier, parameter.input_index))
|
||||
}
|
||||
|
||||
/// Writes a value to a specific input on the matching proto node of every selected non-artboard layer that has one.
|
||||
/// Writes a value to the given parameter on its proto node for every selected non-artboard layer that has one.
|
||||
/// Used by tool control bars to push per-shape parameter changes back onto all selected layers of that shape.
|
||||
pub fn set_proto_node_input_for_selected_layers(
|
||||
document: &DocumentMessageHandler,
|
||||
identifier: graph_craft::ProtoNodeIdentifier,
|
||||
input_index: usize,
|
||||
value: TaggedValue,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let identifier = DefinitionIdentifier::ProtoNode(identifier);
|
||||
/// The parameter symbol locates the node, so the value can never land on the right index of the wrong node.
|
||||
pub fn set_parameter_for_selected_layers(document: &DocumentMessageHandler, parameter: impl Into<ParameterRef>, value: TaggedValue, responses: &mut VecDeque<Message>) {
|
||||
let parameter = parameter.into();
|
||||
let identifier = DefinitionIdentifier::ProtoNode(parameter.node_identifier.clone());
|
||||
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||
|
||||
@@ -895,8 +1016,8 @@ pub fn set_proto_node_input_for_selected_layers(
|
||||
};
|
||||
responses.add(NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index,
|
||||
value: value.clone(),
|
||||
input_index: parameter.input_index,
|
||||
value: value.clone().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -966,18 +1087,60 @@ impl<'a> NodeGraphLayer<'a> {
|
||||
.and_then(|node_id| self.network_interface.document_network().nodes.get(&node_id).map(|node| &node.inputs))
|
||||
}
|
||||
|
||||
/// Find a specific input of a node within the layer's primary flow
|
||||
/// Find a specific input of a node within the layer's primary flow.
|
||||
/// Prefer [`Self::parameter_value`] where the input is statically known; this remains for network nodes and runtime-chosen indices.
|
||||
pub fn find_input(&self, identifier: &DefinitionIdentifier, index: usize) -> Option<&'a TaggedValue> {
|
||||
// TODO: Find a better way to accept a node input rather than using its index (which is quite unclear and fragile)
|
||||
self.find_node_inputs(identifier)?.get(index)?.as_value()
|
||||
}
|
||||
|
||||
/// The stored value of the given parameter on the matching proto node in the layer's primary flow, if that node exists and the input holds a value.
|
||||
/// The parameter symbol names both the node and the input, so the two can never disagree.
|
||||
/// For reading several parameters of the same node, prefer [`Self::find_node_parameters`] so the upstream flow is walked only once.
|
||||
pub fn parameter_value(&self, parameter: impl Into<ParameterRef>) -> Option<&'a TaggedValue> {
|
||||
let parameter = parameter.into();
|
||||
self.find_input(&DefinitionIdentifier::ProtoNode(parameter.node_identifier), parameter.input_index)
|
||||
}
|
||||
|
||||
/// Find a proto node in the layer's primary flow and return its inputs for reading by parameter symbol, walking upstream only once.
|
||||
pub fn find_node_parameters(&self, identifier: ProtoNodeIdentifier) -> Option<ProtoNodeParameters<'a>> {
|
||||
let inputs = self.find_node_inputs(&DefinitionIdentifier::ProtoNode(identifier.clone()))?;
|
||||
Some(ProtoNodeParameters { identifier, inputs })
|
||||
}
|
||||
|
||||
/// Check if a layer is a raster layer
|
||||
pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool {
|
||||
let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]);
|
||||
let layer_input_type = network_interface.input_type(&InputConnector::layer_secondary_input(layer.to_node()), &[]);
|
||||
|
||||
// A leveled wire is typed by its element; depth rides the layout.
|
||||
let compiled = layer_input_type.compiled_nested_type();
|
||||
compiled == Some(&concrete!(Raster<CPU>)) || compiled == Some(&concrete!(Raster<GPU>))
|
||||
matches!(layer_input_type.compiled_element_name().as_deref(), Some("Raster<CPU>" | "Raster<GPU>"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The inputs of a proto node located by [`NodeGraphLayer::find_node_parameters`], read by parameter symbol without re-walking the layer's upstream flow per read.
|
||||
pub struct ProtoNodeParameters<'a> {
|
||||
identifier: ProtoNodeIdentifier,
|
||||
inputs: &'a [NodeInput],
|
||||
}
|
||||
|
||||
impl<'a> ProtoNodeParameters<'a> {
|
||||
/// The input slot of the given parameter, or `None` if the parameter belongs to a different node than this view was built from.
|
||||
pub fn input(&self, parameter: impl Into<ParameterRef>) -> Option<&'a NodeInput> {
|
||||
let parameter = parameter.into();
|
||||
|
||||
// A mismatched symbol would otherwise read whatever sits at that index on the wrong node, so crash under tests but only log and read nothing in the running app
|
||||
if parameter.node_identifier != self.identifier {
|
||||
let message = format!("A parameter of {} was read from the inputs of {}", parameter.node_identifier, self.identifier);
|
||||
if cfg!(test) {
|
||||
panic!("{message}")
|
||||
}
|
||||
log::error!("{message}");
|
||||
return None;
|
||||
}
|
||||
|
||||
self.inputs.get(parameter.input_index)
|
||||
}
|
||||
|
||||
/// The stored value of the given parameter, if that input currently holds a value rather than a wire.
|
||||
pub fn value(&self, parameter: impl Into<ParameterRef>) -> Option<&'a TaggedValue> {
|
||||
self.input(parameter)?.as_value()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::keyboard::Key;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -12,10 +12,9 @@ use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration
|
||||
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
|
||||
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
|
||||
use glam::{DAffine2, DVec2};
|
||||
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::algorithms::shapes::polyline_bezpath;
|
||||
use graphene_std::vector::misc::{BezierHandles, HandleId, ManipulatorPointId, PathSegPoints, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2, segment_to_handles};
|
||||
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;
|
||||
@@ -251,7 +250,7 @@ impl ClosestSegment {
|
||||
// Transform to viewport space
|
||||
let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface);
|
||||
|
||||
// Split the Bezier at the parameter `t`
|
||||
// Split the segment at the parameter `t`
|
||||
let first = self.bezier.subsegment(0_f64..self.t);
|
||||
let second = self.bezier.subsegment(self.t..1.);
|
||||
|
||||
@@ -421,7 +420,7 @@ impl ShapeState {
|
||||
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
|
||||
}
|
||||
|
||||
/// Applies a dummy vector modification to the layer. In the case where a group containing some vector data is selected, this triggers the creation of a «Flatten Path» node.
|
||||
/// Applies a dummy vector modification to the layer. In the case where a group containing some vector data is selected, this triggers the creation of a Combine Paths node.
|
||||
fn add_dummy_modification_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, _end_point: PointId, responses: &mut VecDeque<Message>) {
|
||||
// Apply a zero-delta to one of the points to trigger reorganization
|
||||
let dummy_modification = VectorModificationType::ApplyPointDelta {
|
||||
@@ -750,9 +749,9 @@ impl ShapeState {
|
||||
|
||||
let mut selected_stack = Vec::new();
|
||||
// Find all subpaths that have been clicked
|
||||
for stroke in vector.stroke_bezier_paths() {
|
||||
if stroke.contains_point(layer_mouse)
|
||||
&& let Some(first) = stroke.manipulator_groups().first()
|
||||
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||
if bezpath_from_manipulator_groups(&groups, closed).contains(dvec2_to_point(layer_mouse))
|
||||
&& let Some(first) = groups.first()
|
||||
{
|
||||
selected_stack.push(first.id);
|
||||
}
|
||||
@@ -788,7 +787,7 @@ impl ShapeState {
|
||||
}
|
||||
|
||||
if segments {
|
||||
for (id, _, start, end) in vector.segment_bezier_iter() {
|
||||
for (id, _, start, end) in vector.segment_iter() {
|
||||
if connected_points.contains(&start) || connected_points.contains(&end) {
|
||||
state.select_segment(id);
|
||||
}
|
||||
@@ -950,14 +949,14 @@ impl ShapeState {
|
||||
|
||||
// Move the other handle for a quadratic bezier
|
||||
for segment in vector.end_connected(point) {
|
||||
let Some((start, _end, bezier)) = vector.segment_points_from_id(segment) else { continue };
|
||||
let Some((start, _end, path_segment)) = vector.segment_points_from_id(segment) else { continue };
|
||||
|
||||
if let BezierHandles::Quadratic { handle } = bezier.handles {
|
||||
if let BezierHandles::Quadratic { handle } = segment_to_handles(&path_segment) {
|
||||
if selected.is_some_and(|selected| selected.is_point_selected(ManipulatorPointId::Anchor(start))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_position = handle - bezier.start + delta;
|
||||
let relative_position = handle - point_to_dvec2(path_segment.start()) + delta;
|
||||
let modification_type = VectorModificationType::SetPrimaryHandle { segment, relative_position };
|
||||
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
@@ -1284,7 +1283,7 @@ impl ShapeState {
|
||||
// Make a new set of anchor points which needs to be moved
|
||||
let mut affected_points = state.selected_points.clone();
|
||||
|
||||
for (segment_id, _, start, end) in vector.segment_bezier_iter() {
|
||||
for (segment_id, _, start, end) in vector.segment_iter() {
|
||||
if state.is_segment_selected(segment_id) {
|
||||
affected_points.insert(ManipulatorPointId::Anchor(start));
|
||||
affected_points.insert(ManipulatorPointId::Anchor(end));
|
||||
@@ -1570,7 +1569,7 @@ impl ShapeState {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
for (segment, _, start, end) in vector.segment_bezier_iter() {
|
||||
for (segment, _, start, end) in vector.segment_iter() {
|
||||
if state.selected_segments.contains(&segment) {
|
||||
if start_transaction && !transaction_started {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
@@ -1785,20 +1784,23 @@ impl ShapeState {
|
||||
let viewspace = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
|
||||
|
||||
// Handles
|
||||
for (segment_id, bezier, _, _) in vector.segment_bezier_iter() {
|
||||
let bezier = bezier.apply_transformation(|point| viewspace.transform_point2(point));
|
||||
for (segment_id, segment, _, _) in vector.segment_iter() {
|
||||
let segment = Affine::new(viewspace.to_cols_array()) * segment;
|
||||
let handles = segment_to_handles(&segment);
|
||||
let segment_start = point_to_dvec2(segment.start());
|
||||
let segment_end = point_to_dvec2(segment.end());
|
||||
let valid = |handle: DVec2, control: DVec2| handle.distance_squared(control) > crate::consts::HIDE_HANDLE_DISTANCE.powi(2);
|
||||
|
||||
if let Some(primary_handle) = bezier.handle_start()
|
||||
&& valid(primary_handle, bezier.start)
|
||||
&& (bezier.handle_end().is_some() || valid(primary_handle, bezier.end))
|
||||
if let Some(primary_handle) = handles.start()
|
||||
&& valid(primary_handle, segment_start)
|
||||
&& (handles.end().is_some() || valid(primary_handle, segment_end))
|
||||
&& primary_handle.distance_squared(pos) <= closest_distance_squared
|
||||
{
|
||||
closest_distance_squared = primary_handle.distance_squared(pos);
|
||||
manipulator_point = Some(ManipulatorPointId::PrimaryHandle(segment_id));
|
||||
}
|
||||
if let Some(end_handle) = bezier.handle_end()
|
||||
&& valid(end_handle, bezier.end)
|
||||
if let Some(end_handle) = handles.end()
|
||||
&& valid(end_handle, segment_end)
|
||||
&& end_handle.distance_squared(pos) <= closest_distance_squared
|
||||
{
|
||||
closest_distance_squared = end_handle.distance_squared(pos);
|
||||
@@ -2071,9 +2073,9 @@ impl ShapeState {
|
||||
self.convert_manipulator_handles_to_colinear(&vector, point_id, responses, layer);
|
||||
} else {
|
||||
for handle in vector.all_connected(point_id) {
|
||||
let Some(bezier) = vector.segment_from_id(handle.segment) else { continue };
|
||||
let Some(path_segment) = vector.segment_from_id(handle.segment) else { continue };
|
||||
|
||||
match bezier.handles {
|
||||
match segment_to_handles(&path_segment) {
|
||||
BezierHandles::Linear => {}
|
||||
BezierHandles::Quadratic { .. } => {
|
||||
let segment = handle.segment;
|
||||
@@ -2173,7 +2175,7 @@ impl ShapeState {
|
||||
let Some(vector) = network_interface.compute_modified_vector(layer) else { continue };
|
||||
if !select_points && select_segments {
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.segment_iter()
|
||||
.filter(|(segment, _, _, _)| segments.contains(segment))
|
||||
.for_each(|(_, _, start, end)| match selection_change {
|
||||
SelectionChange::Shrink => {
|
||||
@@ -2220,12 +2222,11 @@ impl ShapeState {
|
||||
assert!(vector.point_domain.ids().contains(&end));
|
||||
}
|
||||
|
||||
let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
|
||||
let polygon_bezpath = if let SelectionShape::Lasso(polygon) = selection_shape {
|
||||
if polygon.len() < 2 {
|
||||
return (points_inside, segments_inside);
|
||||
}
|
||||
let polygon: Subpath<PointId> = Subpath::from_anchors(polygon.to_vec(), true);
|
||||
Some(polygon)
|
||||
Some(polyline_bezpath(polygon.iter().copied(), true))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2253,13 +2254,13 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
SelectionShape::Lasso(_) => {
|
||||
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
|
||||
let polygon = polygon_bezpath.as_ref().expect("If `selection_shape` is a polygon then its path is constructed beforehand.");
|
||||
|
||||
// Sample 10 points on the bezier and check if all or some lie inside the polygon
|
||||
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)),
|
||||
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains(dvec2_to_point(p))),
|
||||
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains(dvec2_to_point(p))),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2278,10 +2279,10 @@ impl ShapeState {
|
||||
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
||||
SelectionShape::Lasso(_) => polygon_subpath
|
||||
SelectionShape::Lasso(_) => polygon_bezpath
|
||||
.as_ref()
|
||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
||||
.contains_point(transformed_position),
|
||||
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
|
||||
.contains(dvec2_to_point(transformed_position)),
|
||||
};
|
||||
|
||||
if select && select_points {
|
||||
@@ -2303,10 +2304,10 @@ impl ShapeState {
|
||||
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
||||
SelectionShape::Lasso(_) => polygon_subpath
|
||||
SelectionShape::Lasso(_) => polygon_bezpath
|
||||
.as_ref()
|
||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
||||
.contains_point(transformed_position),
|
||||
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
|
||||
.contains(dvec2_to_point(transformed_position)),
|
||||
};
|
||||
|
||||
if select && select_points {
|
||||
|
||||
@@ -172,7 +172,7 @@ impl Arc {
|
||||
let radius = radius / viewport_zoom(document);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::arc::RadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Arrow {
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::arrow::ArrowToInput),
|
||||
input: NodeInput::value(TaggedValue::DVec2(arrow_to), false),
|
||||
});
|
||||
let downstream = document.metadata().downstream_transform_to_viewport(layer);
|
||||
|
||||
@@ -106,7 +106,7 @@ impl Circle {
|
||||
let radius: f64 = if dimensions.x > dimensions.y { dimensions.y / 2. } else { dimensions.x / 2. };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::circle::RadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ impl Ellipse {
|
||||
let radius = ((start - end) / 2. / viewport_zoom(document)).abs();
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::ellipse::RadiusXInput),
|
||||
input: NodeInput::value(TaggedValue::F64(radius.x), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::ellipse::RadiusYInput),
|
||||
input: NodeInput::value(TaggedValue::F64(radius.y), false),
|
||||
});
|
||||
responses.add(window_aligned_transform_set(document, layer, start.midpoint(end), DVec2::ONE));
|
||||
@@ -75,8 +75,8 @@ mod test_ellipse {
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::IDENTIFIER)?;
|
||||
Some(ResolvedEllipse {
|
||||
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_x: instrumented.grab_ranked_input::<ellipse::RadiusXInput, f64>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_ranked_input::<ellipse::RadiusYInput, f64>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGiz
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::vector::misc::GridType;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -119,14 +118,14 @@ impl Grid {
|
||||
|
||||
// Set dimensions/spacing
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, SpacingInput::<f64>::INDEX),
|
||||
input_connector: InputConnector::node(node_id, SpacingInput),
|
||||
input: NodeInput::value(TaggedValue::DVec2(dimensions), false),
|
||||
});
|
||||
|
||||
// Set angle for isometric grids
|
||||
if let Some(angle_deg) = angle {
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, AnglesInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, AnglesInput),
|
||||
input: NodeInput::value(TaggedValue::DVec2(DVec2::splat(angle_deg)), false),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Line {
|
||||
let line_to = document_points[1] - document_points[0];
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::line::LineToInput),
|
||||
input: NodeInput::value(TaggedValue::DVec2(line_to), false),
|
||||
});
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Polygon {
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::regular_polygon::RadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ impl Polygon {
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::regular_polygon::SidesInput),
|
||||
input: NodeInput::value(TaggedValue::U32(new_dimension), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
@@ -37,11 +37,11 @@ impl Rectangle {
|
||||
let size = ((start - end) / viewport_zoom(document)).abs();
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::rectangle::WidthInput),
|
||||
input: NodeInput::value(TaggedValue::F64(size.x), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::rectangle::HeightInput),
|
||||
input: NodeInput::value(TaggedValue::F64(size.y), false),
|
||||
});
|
||||
responses.add(window_aligned_transform_set(document, layer, start.midpoint(end), DVec2::ONE));
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::messages::tool::utility_types::*;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::subpath::{self, Subpath};
|
||||
use graphene_std::math::float_noise::round_away_float_noise;
|
||||
use graphene_std::vector::algorithms::shapes::{arc_bezpath, regular_polygon_bezpath, star_polygon_bezpath};
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
|
||||
use kurbo::{BezPath, PathEl, Shape};
|
||||
@@ -214,7 +214,7 @@ pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier,
|
||||
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
|
||||
if new_layer
|
||||
.find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER), 1)
|
||||
.parameter_value(graphene_std::vector::generator_nodes::regular_polygon::SidesInput)
|
||||
.unwrap_or(&TaggedValue::U32(0))
|
||||
.to_u32()
|
||||
% 2 == 1
|
||||
@@ -224,14 +224,14 @@ pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier,
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(polygon_node_id, 2),
|
||||
input_connector: InputConnector::node(polygon_node_id, graphene_std::vector::generator_nodes::regular_polygon::RadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if new_layer
|
||||
.find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::star::IDENTIFIER), 1)
|
||||
.parameter_value(graphene_std::vector::generator_nodes::star::SidesInput)
|
||||
.unwrap_or(&TaggedValue::U32(0))
|
||||
.to_u32()
|
||||
% 2 == 1
|
||||
@@ -241,11 +241,11 @@ pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier,
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 2),
|
||||
input_connector: InputConnector::node(star_node_id, graphene_std::vector::generator_nodes::star::Radius1Input),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 3),
|
||||
input_connector: InputConnector::node(star_node_id, graphene_std::vector::generator_nodes::star::Radius2Input),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
|
||||
});
|
||||
}
|
||||
@@ -353,7 +353,7 @@ pub fn extract_arc_parameters(layer: Option<LayerNodeIdentifier>, document: &Doc
|
||||
pub fn extract_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(SpiralType, f64, f64, f64, f64, f64)> {
|
||||
use graphene_std::vector::generator_nodes::spiral::*;
|
||||
|
||||
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::spiral::IDENTIFIER))?;
|
||||
let parameters = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(IDENTIFIER)?;
|
||||
|
||||
let (
|
||||
Some(&TaggedValue::SpiralType(spiral_type)),
|
||||
@@ -363,12 +363,12 @@ pub fn extract_spiral_parameters(layer: LayerNodeIdentifier, document: &Document
|
||||
Some(&TaggedValue::F64(turns)),
|
||||
Some(&TaggedValue::F64(angle_resolution)),
|
||||
) = (
|
||||
node_inputs.get(SpiralTypeInput::INDEX)?.as_value(),
|
||||
node_inputs.get(StartAngleInput::INDEX)?.as_value(),
|
||||
node_inputs.get(InnerRadiusInput::INDEX)?.as_value(),
|
||||
node_inputs.get(OuterRadiusInput::INDEX)?.as_value(),
|
||||
node_inputs.get(TurnsInput::INDEX)?.as_value(),
|
||||
node_inputs.get(AngularResolutionInput::INDEX)?.as_value(),
|
||||
parameters.value(SpiralTypeInput),
|
||||
parameters.value(StartAngleInput),
|
||||
parameters.value(InnerRadiusInput),
|
||||
parameters.value(OuterRadiusInput),
|
||||
parameters.value(TurnsInput),
|
||||
parameters.value(AngularResolutionInput),
|
||||
)
|
||||
else {
|
||||
return None;
|
||||
@@ -443,12 +443,9 @@ pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessa
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
let points = sides as u64;
|
||||
let diameter: f64 = radius1 * 2.;
|
||||
let inner_diameter = radius2 * 2.;
|
||||
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(star_polygon_bezpath(DVec2::ZERO, points, radius1, radius2))];
|
||||
|
||||
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))];
|
||||
|
||||
overlay_context.outline(subpath.iter(), viewport, None);
|
||||
overlay_context.outline(targets.iter(), viewport, None);
|
||||
}
|
||||
|
||||
/// Outlines the geometric shape made by polygon-node
|
||||
@@ -461,11 +458,9 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
let points = sides as u64;
|
||||
let radius: f64 = radius * 2.;
|
||||
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(regular_polygon_bezpath(DVec2::ZERO, points, radius))];
|
||||
|
||||
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))];
|
||||
|
||||
overlay_context.outline(subpath.iter(), viewport, None);
|
||||
overlay_context.outline(targets.iter(), viewport, None);
|
||||
}
|
||||
|
||||
/// Outlines the geometric shape made by an Arc node
|
||||
@@ -476,19 +471,11 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
|
||||
return;
|
||||
};
|
||||
|
||||
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_arc(
|
||||
radius,
|
||||
start_angle / 360. * std::f64::consts::TAU,
|
||||
sweep_angle / 360. * std::f64::consts::TAU,
|
||||
match arc_type {
|
||||
ArcType::Open => subpath::ArcType::Open,
|
||||
ArcType::Closed => subpath::ArcType::Closed,
|
||||
ArcType::PieSlice => subpath::ArcType::PieSlice,
|
||||
},
|
||||
))];
|
||||
let arc = arc_bezpath(radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type);
|
||||
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc)];
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
overlay_context.outline(subpath.iter(), viewport, None);
|
||||
overlay_context.outline(targets.iter(), viewport, None);
|
||||
}
|
||||
|
||||
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications
|
||||
@@ -571,7 +558,17 @@ pub fn wrap_to_tau(angle: f64) -> f64 {
|
||||
}
|
||||
|
||||
pub fn format_rounded(value: f64, precision: usize) -> String {
|
||||
format!("{value:.precision$}").trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
// Denoised values within floating point noise of zero (including -0) display as unsigned zero, unless the precision is fine enough to display them
|
||||
let value = round_away_float_noise(value);
|
||||
let value = if value.abs() < f64::min(1e-12, 0.5 * 10_f64.powi(-(precision as i32))) { 0. } else { value };
|
||||
let formatted = format!("{value:.precision$}");
|
||||
|
||||
// Trailing zeros are trimmed only when the display is exact, so a truncated value keeps its decimal places (like "0.00" or "3.10") to indicate the truncation
|
||||
if formatted.parse::<f64>() == Ok(value) {
|
||||
formatted.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
formatted
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the approximated angle to display in degrees, given an angle in degrees.
|
||||
@@ -599,17 +596,61 @@ pub fn calculate_arc_text_transform(angle: f64, offset_angle: f64, center: DVec2
|
||||
pub fn extract_grid_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(GridType, DVec2, u32, u32, DVec2)> {
|
||||
use graphene_std::vector::generator_nodes::grid::*;
|
||||
|
||||
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER))?;
|
||||
let parameters = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(IDENTIFIER)?;
|
||||
|
||||
let (Some(&TaggedValue::GridType(grid_type)), Some(&TaggedValue::DVec2(spacing)), Some(&TaggedValue::U32(columns)), Some(&TaggedValue::U32(rows)), Some(&TaggedValue::DVec2(angles))) = (
|
||||
node_inputs.get(GridTypeInput::INDEX)?.as_value(),
|
||||
node_inputs.get(SpacingInput::<f64>::INDEX)?.as_value(),
|
||||
node_inputs.get(ColumnsInput::INDEX)?.as_value(),
|
||||
node_inputs.get(RowsInput::INDEX)?.as_value(),
|
||||
node_inputs.get(AnglesInput::INDEX)?.as_value(),
|
||||
parameters.value(GridTypeInput),
|
||||
parameters.value(SpacingInput),
|
||||
parameters.value(ColumnsInput),
|
||||
parameters.value(RowsInput),
|
||||
parameters.value(AnglesInput),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((grid_type, spacing, columns, rows, angles))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_rounded_trims_trailing_zeros_when_exact() {
|
||||
assert_eq!(format_rounded(0.25, 2), "0.25");
|
||||
assert_eq!(format_rounded(3.1, 2), "3.1");
|
||||
assert_eq!(format_rounded(45., 2), "45");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_rounded_keeps_decimal_places_when_truncated() {
|
||||
assert_eq!(format_rounded(0.0003, 2), "0.00");
|
||||
assert_eq!(format_rounded(-0.0003, 2), "-0.00");
|
||||
assert_eq!(format_rounded(0.0003, 3), "0.000");
|
||||
assert_eq!(format_rounded(3.10001, 2), "3.10");
|
||||
assert_eq!(format_rounded(45.001, 2), "45.00");
|
||||
assert_eq!(format_rounded(std::f64::consts::PI, 2), "3.14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_rounded_denoises_before_judging_exactness() {
|
||||
assert_eq!(format_rounded(29.999999999999996, 2), "30");
|
||||
assert_eq!(format_rounded(45.00000000000001, 2), "45");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_rounded_shows_true_and_noise_zeros_plainly() {
|
||||
assert_eq!(format_rounded(0., 2), "0");
|
||||
assert_eq!(format_rounded(-0., 2), "0");
|
||||
assert_eq!(format_rounded(1e-15, 2), "0");
|
||||
assert_eq!(format_rounded(-1e-15, 2), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_rounded_keeps_tiny_values_at_displayable_precision() {
|
||||
assert_eq!(format_rounded(5e-13, 20), "0.0000000000005");
|
||||
assert_eq!(format_rounded(-5e-13, 20), "-0.0000000000005");
|
||||
assert_eq!(format_rounded(0., 20), "0");
|
||||
assert_eq!(format_rounded(-0., 20), "0");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
|
||||
use graphene_std::vector::algorithms::shapes::{calculate_growth_factor, spiral_point};
|
||||
use graphene_std::vector::misc::SpiralType;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -140,12 +139,11 @@ impl Spiral {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::spiral::IDENTIFIER))
|
||||
else {
|
||||
let Some(parameters) = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(graphene_std::vector::generator_nodes::spiral::IDENTIFIER) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(SpiralTypeInput::INDEX).unwrap().as_value() else {
|
||||
let Some(&TaggedValue::SpiralType(spiral_type)) = parameters.value(SpiralTypeInput) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -157,7 +155,7 @@ impl Spiral {
|
||||
responses.add(window_aligned_transform_set(document, layer, viewport_drag_start, DVec2::ONE));
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, OuterRadiusInput),
|
||||
input: NodeInput::value(TaggedValue::F64(new_radius), false),
|
||||
});
|
||||
}
|
||||
@@ -167,8 +165,7 @@ impl Spiral {
|
||||
pub fn update_turns(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
use graphene_std::vector::generator_nodes::spiral::*;
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::spiral::IDENTIFIER))
|
||||
else {
|
||||
let Some(parameters) = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(graphene_std::vector::generator_nodes::spiral::IDENTIFIER) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -176,7 +173,7 @@ impl Spiral {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(&TaggedValue::F64(mut turns)) = node_inputs.get(TurnsInput::INDEX).unwrap().as_value() else {
|
||||
let Some(&TaggedValue::F64(mut turns)) = parameters.value(TurnsInput) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -191,7 +188,7 @@ impl Spiral {
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
|
||||
input_connector: InputConnector::node(node_id, TurnsInput),
|
||||
input: NodeInput::value(TaggedValue::F64(turns), false),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,12 +152,12 @@ impl Star {
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::star::Radius1Input),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 3),
|
||||
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::star::Radius2Input),
|
||||
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,14 +6,12 @@ use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2, FloatExt};
|
||||
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, ParamCurve, PathSeg};
|
||||
use graphene_std::vector::algorithms::shapes::{ellipse_bezpath, line_bezpath};
|
||||
use graphene_std::vector::misc::{ManipulatorGroup, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2};
|
||||
use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LayerSnapper {
|
||||
@@ -47,7 +45,7 @@ impl LayerSnapper {
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve,
|
||||
layer,
|
||||
start: PointId::new(),
|
||||
start: PointId::generate(),
|
||||
target,
|
||||
bounds: Some(bounds),
|
||||
});
|
||||
@@ -74,26 +72,20 @@ impl LayerSnapper {
|
||||
}
|
||||
|
||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) {
|
||||
let mut push_candidates = |subpath: &Subpath<PointId>, transform: DAffine2| {
|
||||
for (start_index, curve) in subpath.iter().enumerate() {
|
||||
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;
|
||||
// Post-solidified outline (the layer's recorded geometry). Its anchors carry no point IDs,
|
||||
// so while this layer's manipulators are being dragged the whole outline is skipped instead of filtering per manipulator.
|
||||
if !snap_data.ignore_bounds(layer) {
|
||||
for bezpath in document.metadata().layer_outline(layer) {
|
||||
for curve in bezpath.segments() {
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve: Affine::new(transform.to_cols_array()) * curve,
|
||||
layer,
|
||||
start: PointId::generate(),
|
||||
target: SnapTarget::Path(PathSnapTarget::AlongPath),
|
||||
bounds: None,
|
||||
});
|
||||
}
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve,
|
||||
layer,
|
||||
start,
|
||||
target: SnapTarget::Path(PathSnapTarget::AlongPath),
|
||||
bounds: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Post-solidified outline (the layer's recorded geometry)
|
||||
for subpath in document.metadata().layer_outline(layer) {
|
||||
push_candidates(subpath, transform);
|
||||
}
|
||||
|
||||
// Pre-solidified centerline (the Path tool's view) when an upstream Path node exists,
|
||||
@@ -101,8 +93,22 @@ impl LayerSnapper {
|
||||
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
|
||||
let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
|
||||
if path_aware_transform.is_finite() {
|
||||
for subpath in vector.stroke_bezier_paths() {
|
||||
push_candidates(&subpath, path_aware_transform);
|
||||
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||
let bezpath = bezpath_from_manipulator_groups(&groups, closed);
|
||||
for (start_index, curve) in bezpath.segments().enumerate() {
|
||||
let start = groups[start_index].id;
|
||||
let end = groups[(start_index + 1) % groups.len()].id;
|
||||
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, end) {
|
||||
continue;
|
||||
}
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve: Affine::new(path_aware_transform.to_cols_array()) * curve,
|
||||
layer,
|
||||
start,
|
||||
target: SnapTarget::Path(PathSnapTarget::AlongPath),
|
||||
bounds: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,17 +162,17 @@ impl LayerSnapper {
|
||||
|
||||
let tolerance = snap_tolerance(document);
|
||||
let constraint_path = if let SnapConstraint::Circle { center, radius } = constraint {
|
||||
Subpath::new_ellipse(center - DVec2::splat(radius), center + DVec2::splat(radius))
|
||||
ellipse_bezpath(center - DVec2::splat(radius), center + DVec2::splat(radius))
|
||||
} else {
|
||||
let constrained_point = constraint.projection(point.document_point);
|
||||
let direction = constraint.direction().normalize_or_zero();
|
||||
let start = constrained_point - tolerance * direction;
|
||||
let end = constrained_point + tolerance * direction;
|
||||
Subpath::<PointId>::new_line(start, end)
|
||||
line_bezpath(start, end)
|
||||
};
|
||||
|
||||
for path in &self.paths_to_snap {
|
||||
for constraint_path in constraint_path.iter() {
|
||||
for constraint_path in constraint_path.segments() {
|
||||
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));
|
||||
|
||||
@@ -548,13 +554,14 @@ fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool {
|
||||
move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE
|
||||
}
|
||||
|
||||
fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<PointId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
|
||||
fn manipulator_groups_anchor_snap_points(layer: LayerNodeIdentifier, groups: &[ManipulatorGroup], closed: bool, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
|
||||
let document = snap_data.document;
|
||||
let bezpath = bezpath_from_manipulator_groups(groups, closed);
|
||||
|
||||
// Midpoints of linear segments
|
||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
|
||||
for (index, curve) in subpath.iter().enumerate() {
|
||||
if snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[index].id) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(index + 1) % subpath.len()].id) {
|
||||
for (index, curve) in bezpath.segments().enumerate() {
|
||||
if snap_data.ignore_manipulator(layer, groups[index].id) || snap_data.ignore_manipulator(layer, groups[(index + 1) % groups.len()].id) {
|
||||
continue;
|
||||
}
|
||||
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
|
||||
@@ -576,7 +583,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
|
||||
}
|
||||
|
||||
// Anchors
|
||||
for (index, manipulators) in subpath.manipulator_groups().iter().enumerate() {
|
||||
for (index, manipulators) in groups.iter().enumerate() {
|
||||
if snap_data.ignore_manipulator(layer, manipulators.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -585,7 +592,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
|
||||
return;
|
||||
}
|
||||
|
||||
let colinear = are_manipulator_handles_colinear(manipulators, to_document, subpath, index);
|
||||
let colinear = are_manipulator_handles_colinear(manipulators, to_document, closed, index, groups.len());
|
||||
|
||||
// Colinear handles
|
||||
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
|
||||
@@ -608,11 +615,101 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
|
||||
fn bezpath_anchor_snap_points(layer: LayerNodeIdentifier, bezpath: &BezPath, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
|
||||
let document = snap_data.document;
|
||||
|
||||
// Split the path into contours so endpoint and wraparound handling stays per-subpath
|
||||
let mut contours = Vec::new();
|
||||
let mut current = BezPath::new();
|
||||
for element in bezpath.elements() {
|
||||
if matches!(element, PathEl::MoveTo(_)) && !current.elements().is_empty() {
|
||||
contours.push(std::mem::take(&mut current));
|
||||
}
|
||||
current.push(*element);
|
||||
}
|
||||
if !current.elements().is_empty() {
|
||||
contours.push(current);
|
||||
}
|
||||
|
||||
for contour in contours {
|
||||
let closed = matches!(contour.elements().last(), Some(PathEl::ClosePath));
|
||||
let segments: Vec<PathSeg> = contour.segments().collect();
|
||||
if segments.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Midpoints of linear segments
|
||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
|
||||
for &segment in &segments {
|
||||
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
|
||||
return;
|
||||
}
|
||||
|
||||
let curve = pathseg_points(segment);
|
||||
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.p0 * 0.5 + curve.p3 * 0.5),
|
||||
SnapSource::Path(PathSnapSource::LineMidpoint),
|
||||
SnapTarget::Path(PathSnapTarget::LineMidpoint),
|
||||
Some(layer),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors
|
||||
let anchor_count = segments.len() + if closed { 0 } else { 1 };
|
||||
for index in 0..anchor_count {
|
||||
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
|
||||
return;
|
||||
}
|
||||
|
||||
let anchor = if index < segments.len() {
|
||||
pathseg_points(segments[index]).p0
|
||||
} else {
|
||||
pathseg_points(segments[index - 1]).p3
|
||||
};
|
||||
|
||||
let in_handle = if index > 0 {
|
||||
pathseg_points(segments[index - 1]).p2
|
||||
} else if closed {
|
||||
pathseg_points(segments[segments.len() - 1]).p2
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let out_handle = if index < segments.len() { pathseg_points(segments[index]).p1 } else { None };
|
||||
|
||||
let handle_in = in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
|
||||
let handle_out = out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
|
||||
let anchor_is_endpoint = !closed && (index == 0 || index == anchor_count - 1);
|
||||
let colinear = !anchor_is_endpoint && handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_to(handle_out) < 1e-5));
|
||||
|
||||
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(anchor),
|
||||
SnapSource::Path(PathSnapSource::AnchorPointWithColinearHandles),
|
||||
SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles),
|
||||
Some(layer),
|
||||
));
|
||||
} else if !colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(anchor),
|
||||
SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles),
|
||||
SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles),
|
||||
Some(layer),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup, to_document: DAffine2, closed: bool, index: usize, count: 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));
|
||||
let anchor_is_endpoint = !subpath.closed() && (index == 0 || index == subpath.len() - 1);
|
||||
let anchor_is_endpoint = !closed && (index == 0 || index == count - 1);
|
||||
|
||||
// Unless this is an endpoint, check if both handles are colinear (within an angular epsilon)
|
||||
!anchor_is_endpoint && handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_to(handle_out) < 1e-5))
|
||||
@@ -633,11 +730,11 @@ pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, p
|
||||
get_layer_snap_points(child, snap_data, points);
|
||||
}
|
||||
} else {
|
||||
// Post-solidified outline (the layer's recorded geometry)
|
||||
if document.metadata().layer_outline(layer).next().is_some() {
|
||||
// Post-solidified outline (the layer's recorded geometry), skipped wholesale while this layer's manipulators are being dragged since the outline carries no point IDs
|
||||
if !snap_data.ignore_bounds(layer) {
|
||||
let to_document = document.metadata().transform_to_document(layer);
|
||||
for subpath in document.metadata().layer_outline(layer) {
|
||||
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document);
|
||||
for bezpath in document.metadata().layer_outline(layer) {
|
||||
bezpath_anchor_snap_points(layer, bezpath, snap_data, points, to_document);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,8 +743,8 @@ pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, p
|
||||
// matching what the Path tool's overlay shows
|
||||
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
|
||||
let to_document = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
|
||||
for subpath in vector.stroke_bezier_paths() {
|
||||
subpath_anchor_snap_points(layer, &subpath, snap_data, points, to_document);
|
||||
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||
manipulator_groups_anchor_snap_points(layer, &groups, closed, snap_data, points, to_document);
|
||||
}
|
||||
|
||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::color_selector::{DrawingToolState, apply_line_weight};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||
use graphene_std::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
@@ -65,7 +64,9 @@ where
|
||||
if has_dash {
|
||||
rows.push(LayoutGroup::row(dash_offset_row(drawing.dash_offset, to_message.clone())));
|
||||
}
|
||||
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", drawing.paint_order, false, {
|
||||
// An inapplicable order (no Fill/Stroke pair to reorder) grays out and highlights no entry, since the synced value is only a fallback
|
||||
let paint_order = drawing.paint_order.filter(|_| drawing.paint_order_applicable);
|
||||
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", paint_order, !drawing.paint_order_applicable, {
|
||||
let to_message = to_message.clone();
|
||||
move |value| to_message(StrokeOptionsUpdate::PaintOrder(value))
|
||||
})));
|
||||
@@ -124,19 +125,9 @@ where
|
||||
E: ChoiceTypeStatic + 'static,
|
||||
F: Fn(E) -> Message + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let entries = E::list()
|
||||
.iter()
|
||||
.flat_map(|section| section.iter())
|
||||
.map(|(value, meta)| {
|
||||
let to_message = to_message.clone();
|
||||
let value = *value;
|
||||
let entry = RadioEntryData::new(meta.name)
|
||||
.tooltip_label(meta.label)
|
||||
.tooltip_description(meta.description.unwrap_or_default())
|
||||
.on_update(move |_| to_message(value))
|
||||
.on_commit(|_| DocumentMessage::StartTransaction.into());
|
||||
if let Some(icon) = meta.icon { entry.icon(icon) } else { entry.label(meta.label) }
|
||||
})
|
||||
let entries = RadioEntryData::list_from_choice_type(to_message)
|
||||
.into_iter()
|
||||
.map(|entry| entry.on_commit(|_| DocumentMessage::StartTransaction.into()))
|
||||
.collect();
|
||||
vec![
|
||||
TextLabel::new(label_text).table_align(true).widget_instance(),
|
||||
@@ -189,39 +180,40 @@ where
|
||||
|
||||
pub fn apply_stroke_align(drawing: &mut DrawingToolState, align: StrokeAlign, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.stroke_align = Some(align);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::AlignInput::INDEX, TaggedValue::StrokeAlign(align), responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::AlignInput, TaggedValue::StrokeAlign(align), responses);
|
||||
}
|
||||
|
||||
pub fn apply_stroke_cap(drawing: &mut DrawingToolState, cap: StrokeCap, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.stroke_cap = Some(cap);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::CapInput::INDEX, TaggedValue::StrokeCap(cap), responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::CapInput, TaggedValue::StrokeCap(cap), responses);
|
||||
}
|
||||
|
||||
pub fn apply_stroke_join(drawing: &mut DrawingToolState, join: StrokeJoin, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.stroke_join = Some(join);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::JoinInput::INDEX, TaggedValue::StrokeJoin(join), responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::JoinInput, TaggedValue::StrokeJoin(join), responses);
|
||||
}
|
||||
|
||||
pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.miter_limit = Some(limit);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::MiterLimitInput::INDEX, TaggedValue::F64(limit), responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::MiterLimitInput, TaggedValue::F64(limit), responses);
|
||||
}
|
||||
|
||||
pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.paint_order = Some(order);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::PaintOrderInput::INDEX, TaggedValue::PaintOrder(order), responses);
|
||||
// A mixed selection only grays the radio out when no layer can take the swap, so skip the ones that can't
|
||||
for layer in graph_modification_utils::paintable_selected_layers(document) {
|
||||
if graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface) {
|
||||
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order: order });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.dash_lengths = Some(lengths.clone());
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashLengthsInput::INDEX, TaggedValue::F64Array(lengths), responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::DashPatternInput, TaggedValue::DashPattern(lengths), responses);
|
||||
}
|
||||
|
||||
pub fn apply_dash_offset(drawing: &mut DrawingToolState, offset: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.dash_offset = Some(offset);
|
||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashOffsetInput::INDEX, TaggedValue::F64(offset), responses);
|
||||
}
|
||||
|
||||
fn set_stroke_input_for_selected(document: &DocumentMessageHandler, input_index: usize, value: TaggedValue, responses: &mut VecDeque<Message>) {
|
||||
graph_modification_utils::set_proto_node_input_for_selected_layers(document, graphene_std::vector::stroke::IDENTIFIER, input_index, value, responses);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::DashOffsetInput, TaggedValue::F64(offset), responses);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user