mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Convert the node catalog to rank-polymorphic Item and List kernels, materialize stored values as ranked wires, and display wire rank in the graph
This commit is contained in:
committed by
Dennis Kobert
parent
ead622b969
commit
d63ea46bd4
@@ -6,13 +6,24 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::{Affine2, DAffine2, Vec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::blending::BlendMode;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::gradient::Gradient;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::list::{Item, List};
|
||||
use graphene_std::raster::{
|
||||
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||
};
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
|
||||
use graphene_std::text::TextAlign;
|
||||
use graphene_std::text_nodes::StringCapitalization;
|
||||
use graphene_std::transform::{ReferencePoint, ScaleType};
|
||||
use graphene_std::vector::misc::{
|
||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||
};
|
||||
use graphene_std::vector::style::{DashPattern, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
||||
use graphene_std::{Artboard, Color, Graphic};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
@@ -195,24 +206,103 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
List<Gradient>,
|
||||
List<String>,
|
||||
List<f64>,
|
||||
List<u8>,
|
||||
List<f32>,
|
||||
List<u32>,
|
||||
List<u64>,
|
||||
List<i32>,
|
||||
List<i64>,
|
||||
List<bool>,
|
||||
List<DVec2>,
|
||||
List<DAffine2>,
|
||||
List<BlendMode>,
|
||||
List<GradientType>,
|
||||
List<GradientSpreadMethod>,
|
||||
List<DashPattern>,
|
||||
List<BoxCorners>,
|
||||
List<StrokeJoin>,
|
||||
List<StrokeAlign>,
|
||||
List<StrokeCap>,
|
||||
List<PaintOrder>,
|
||||
List<MergeByDistanceAlgorithm>,
|
||||
List<ExtrudeJoiningAlgorithm>,
|
||||
List<PointSpacingType>,
|
||||
List<StringCapitalization>,
|
||||
List<LuminanceCalculation>,
|
||||
List<RedGreenBlue>,
|
||||
List<RedGreenBlueAlpha>,
|
||||
List<RelativeAbsolute>,
|
||||
List<SelectiveColorChoice>,
|
||||
List<XY>,
|
||||
List<ScaleType>,
|
||||
List<ReferencePoint>,
|
||||
List<CentroidType>,
|
||||
List<BooleanOperation>,
|
||||
List<NoiseType>,
|
||||
List<FractalType>,
|
||||
List<CellularDistanceFunction>,
|
||||
List<CellularReturnType>,
|
||||
List<DomainWarpType>,
|
||||
List<RealTimeMode>,
|
||||
List<GridType>,
|
||||
List<ArcType>,
|
||||
List<SpiralType>,
|
||||
List<TextAlign>,
|
||||
List<QRCodeErrorCorrectionLevel>,
|
||||
List<InterpolationDistribution>,
|
||||
List<RowsOrColumns>,
|
||||
Artboard,
|
||||
Graphic,
|
||||
Vector,
|
||||
Raster<CPU>,
|
||||
Raster<GPU>,
|
||||
Color,
|
||||
Gradient,
|
||||
String,
|
||||
f64,
|
||||
f32,
|
||||
u32,
|
||||
u64,
|
||||
i32,
|
||||
i64,
|
||||
bool,
|
||||
String,
|
||||
Option<f64>,
|
||||
DVec2,
|
||||
DAffine2,
|
||||
BlendMode,
|
||||
GradientType,
|
||||
GradientSpreadMethod,
|
||||
DashPattern,
|
||||
BoxCorners,
|
||||
StrokeJoin,
|
||||
StrokeAlign,
|
||||
StrokeCap,
|
||||
PaintOrder,
|
||||
MergeByDistanceAlgorithm,
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
XY,
|
||||
ScaleType,
|
||||
ReferencePoint,
|
||||
CentroidType,
|
||||
BooleanOperation,
|
||||
NoiseType,
|
||||
FractalType,
|
||||
CellularDistanceFunction,
|
||||
CellularReturnType,
|
||||
DomainWarpType,
|
||||
RealTimeMode,
|
||||
GridType,
|
||||
ArcType,
|
||||
SpiralType,
|
||||
TextAlign,
|
||||
QRCodeErrorCorrectionLevel,
|
||||
InterpolationDistribution,
|
||||
RowsOrColumns,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -250,6 +340,57 @@ trait TableItemLayout {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TableItemLayout> TableItemLayout for Item<T> {
|
||||
fn type_name() -> &'static str {
|
||||
T::type_name()
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.element().identifier()
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
if let Some(step) = data.desired_path.get(data.current_depth).cloned() {
|
||||
match step {
|
||||
PathStep::Element(_) => {
|
||||
data.current_depth += 1;
|
||||
let result = self.element().layout_with_breadcrumb(data);
|
||||
data.current_depth -= 1;
|
||||
return result;
|
||||
}
|
||||
PathStep::Attribute { key, .. } => {
|
||||
if let Some(any) = self.attributes().get_any(&key) {
|
||||
data.current_depth += 1;
|
||||
if let Some(result) = drilldown_attribute_layout(any, data) {
|
||||
data.current_depth -= 1;
|
||||
return result;
|
||||
}
|
||||
data.current_depth -= 1;
|
||||
warn!("Drilldown unsupported for attribute {key:?}");
|
||||
}
|
||||
data.desired_path.truncate(data.current_depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attribute_keys: Vec<String> = self.attributes().keys().map(str::to_string).collect();
|
||||
|
||||
// A single element, so no leading ID column, unlike the `List` table
|
||||
let mut values = vec![self.element().value_widget(PathStep::Element(0), data)];
|
||||
for key in &attribute_keys {
|
||||
let target = PathStep::Attribute { row: 0, key: key.clone() };
|
||||
let widget = self.attributes().get_any(key).and_then(|any| dispatch_value_widget(any, target, data)).unwrap_or_else(|| {
|
||||
let text = self.attributes().display_value(key, display_value_override).unwrap_or_else(|| "-".to_string());
|
||||
TextLabel::new(text).narrow(true).widget_instance()
|
||||
});
|
||||
values.push(widget);
|
||||
}
|
||||
|
||||
let mut column_names = vec!["element"];
|
||||
column_names.extend(attribute_keys.iter().map(|s| s.as_str()));
|
||||
|
||||
vec![LayoutGroup::table(vec![column_headings(&column_names), values], false)]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TableItemLayout> TableItemLayout for List<T> {
|
||||
fn type_name() -> &'static str {
|
||||
"List"
|
||||
@@ -328,6 +469,46 @@ impl TableItemLayout for Artboard<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for DashPattern {
|
||||
fn type_name() -> &'static str {
|
||||
"DashPattern"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"DashPattern".to_string()
|
||||
}
|
||||
// The wrapping `Item` already contributes the breadcrumb; the inner list supplies the next level
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.value_page(data)
|
||||
}
|
||||
// Label the spreadsheet's element button with the inner list's identifier, like Artboard
|
||||
fn value_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance {
|
||||
self.0.value_widget(target, data)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.0.layout_with_breadcrumb(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for BoxCorners {
|
||||
fn type_name() -> &'static str {
|
||||
"BoxCorners"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"BoxCorners".to_string()
|
||||
}
|
||||
// The wrapping `Item` already contributes the breadcrumb; the inner list supplies the next level
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.value_page(data)
|
||||
}
|
||||
// Label the spreadsheet's element button with the inner list's identifier, like Artboard
|
||||
fn value_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance {
|
||||
self.0.value_widget(target, data)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.0.layout_with_breadcrumb(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for Graphic<'_> {
|
||||
fn type_name() -> &'static str {
|
||||
"Graphic"
|
||||
@@ -520,7 +701,7 @@ impl TableItemLayout for Raster<GPU> {
|
||||
format!("Raster ({} x {})", self.data().width(), self.data().height())
|
||||
}
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_instance()];
|
||||
let widgets = vec![TextLabel::new("This raster data is a texture on the GPU. It currently cannot be displayed here.").widget_instance()];
|
||||
vec![LayoutGroup::row(widgets)]
|
||||
}
|
||||
}
|
||||
@@ -593,6 +774,21 @@ impl TableItemLayout for u8 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for f32 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (f32)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("{self}")
|
||||
}
|
||||
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![
|
||||
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for u32 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (u32)"
|
||||
@@ -608,6 +804,37 @@ impl TableItemLayout for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for i32 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (i32)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("{self}")
|
||||
}
|
||||
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![
|
||||
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for i64 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (i64)"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
format!("{self}")
|
||||
}
|
||||
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
|
||||
// TODO: Make this robust for large i64 values that don't fit in f64 (beyond roughly 2^53), as with u64.
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![
|
||||
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for u64 {
|
||||
fn type_name() -> &'static str {
|
||||
"Number (u64)"
|
||||
@@ -734,45 +961,73 @@ impl TableItemLayout for Affine2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for BlendMode {
|
||||
fn type_name() -> &'static str {
|
||||
"BlendMode"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||
TextLabel::new(self.to_string()).narrow(true).widget_instance()
|
||||
}
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
||||
// Choice enums all display as their variant's label, shown inline as a plain text widget
|
||||
macro_rules! impl_table_item_layout_for_choice_enum {
|
||||
($($ty:ty),* $(,)?) => {
|
||||
$(
|
||||
impl TableItemLayout for $ty {
|
||||
fn type_name() -> &'static str {
|
||||
stringify!($ty)
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||
TextLabel::new(self.to_string()).narrow(true).widget_instance()
|
||||
}
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
impl_table_item_layout_for_choice_enum!(
|
||||
BlendMode,
|
||||
GradientType,
|
||||
GradientSpreadMethod,
|
||||
StrokeJoin,
|
||||
StrokeAlign,
|
||||
StrokeCap,
|
||||
PaintOrder,
|
||||
MergeByDistanceAlgorithm,
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
XY,
|
||||
ScaleType,
|
||||
CentroidType,
|
||||
BooleanOperation,
|
||||
NoiseType,
|
||||
FractalType,
|
||||
CellularDistanceFunction,
|
||||
CellularReturnType,
|
||||
DomainWarpType,
|
||||
RealTimeMode,
|
||||
GridType,
|
||||
ArcType,
|
||||
SpiralType,
|
||||
TextAlign,
|
||||
QRCodeErrorCorrectionLevel,
|
||||
InterpolationDistribution,
|
||||
RowsOrColumns,
|
||||
);
|
||||
|
||||
impl TableItemLayout for GradientType {
|
||||
// ReferencePoint is not a choice enum with display labels, so its variant name serves as the label
|
||||
impl TableItemLayout for ReferencePoint {
|
||||
fn type_name() -> &'static str {
|
||||
"GradientType"
|
||||
"ReferencePoint"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.to_string()
|
||||
format!("{self:?}")
|
||||
}
|
||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||
TextLabel::new(self.to_string()).narrow(true).widget_instance()
|
||||
}
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for GradientSpreadMethod {
|
||||
fn type_name() -> &'static str {
|
||||
"GradientSpreadMethod"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||
TextLabel::new(self.to_string()).narrow(true).widget_instance()
|
||||
TextLabel::new(self.identifier()).narrow(true).widget_instance()
|
||||
}
|
||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
||||
@@ -915,9 +1170,7 @@ macro_rules! known_item_types {
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
List<String>,
|
||||
List<NodeId>,
|
||||
List<f64>,
|
||||
List<u8>,
|
||||
Gradient,
|
||||
Color,
|
||||
NodeId,
|
||||
@@ -927,9 +1180,12 @@ macro_rules! known_item_types {
|
||||
Vec2,
|
||||
Option<f64>,
|
||||
f64,
|
||||
f32,
|
||||
u8,
|
||||
u32,
|
||||
u64,
|
||||
i32,
|
||||
i64,
|
||||
bool,
|
||||
String,
|
||||
Vector,
|
||||
@@ -937,6 +1193,42 @@ macro_rules! known_item_types {
|
||||
Raster<GPU>,
|
||||
Graphic,
|
||||
Artboard,
|
||||
DashPattern,
|
||||
BoxCorners,
|
||||
BlendMode,
|
||||
GradientType,
|
||||
GradientSpreadMethod,
|
||||
StrokeJoin,
|
||||
StrokeAlign,
|
||||
StrokeCap,
|
||||
PaintOrder,
|
||||
MergeByDistanceAlgorithm,
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
StringCapitalization,
|
||||
LuminanceCalculation,
|
||||
RedGreenBlue,
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
XY,
|
||||
ScaleType,
|
||||
ReferencePoint,
|
||||
CentroidType,
|
||||
BooleanOperation,
|
||||
NoiseType,
|
||||
FractalType,
|
||||
CellularDistanceFunction,
|
||||
CellularReturnType,
|
||||
DomainWarpType,
|
||||
RealTimeMode,
|
||||
GridType,
|
||||
ArcType,
|
||||
SpiralType,
|
||||
TextAlign,
|
||||
QRCodeErrorCorrectionLevel,
|
||||
InterpolationDistribution,
|
||||
RowsOrColumns,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2748,7 +2748,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>) {
|
||||
@@ -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_as` yields no records at all, which would otherwise pass without checking anything
|
||||
let base_lengths: Vec<usize> = instrumented
|
||||
.grab_all_input_as::<graphene_std::graphic::extend::BaseInput<graphene_std::Graphic>, 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_as::<graphene_std::graphic::extend::NewInput<graphene_std::Graphic>, 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
inputs: vec![NodeInput::import(generic!(T), 4)],
|
||||
..Default::default()
|
||||
},
|
||||
// 1: Count Elements (number of subpaths)
|
||||
// 1: List Length (number of subpaths)
|
||||
DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::list_length::IDENTIFIER),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
@@ -578,7 +578,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
NodeInput::node(NodeId(14), 0),
|
||||
NodeInput::value(TaggedValue::Bool(false), false),
|
||||
NodeInput::import(concrete!(vector::misc::InterpolationDistribution), 3),
|
||||
NodeInput::import(generic!(T), 4),
|
||||
NodeInput::import(concrete!(Vector), 4),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -637,7 +637,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
// 1: Count Elements
|
||||
// 1: List Length
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(2, 2)),
|
||||
@@ -1332,13 +1332,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
implementation: DocumentNodeImplementation::ProtoNode(text_nodes::regex::regex_find::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
// Node 1: extract_element at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them)
|
||||
// Node 1: item_at_index at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them)
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
// Node 2: omit_element at index 0, returns the capture group items as a List<String>, preserving each item's start/end/name attributes
|
||||
// Node 2: remove_at_index at index 0, returns the capture group items as a List<String>, preserving each item's start/end/name attributes
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER),
|
||||
@@ -1423,7 +1423,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
nodes: vec![
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::import(concrete!(List<Vector>), 0)],
|
||||
inputs: vec![NodeInput::import(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
@@ -2180,3 +2180,33 @@ impl DocumentNodeDefinition {
|
||||
self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::resolve_network_node_type;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use graph_craft::document::NodeId;
|
||||
|
||||
// Guards the embedded Map body chain (Read Vector -> Extract Transform -> Decompose Translation -> As Vector) against registry drift
|
||||
#[tokio::test]
|
||||
async fn origins_to_polyline_resolves_and_evaluates() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.draw_rect(0., 0., 10., 10.).await;
|
||||
|
||||
let layer = editor.active_document().metadata().all_layers().next().expect("drawing a rectangle should create a layer");
|
||||
let node_id = NodeId::new();
|
||||
let node_template = resolve_network_node_type("Origins to Polyline")
|
||||
.expect("the Origins to Polyline definition should exist")
|
||||
.default_node_template();
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::InsertNode {
|
||||
node_id,
|
||||
node_template: Box::new(node_template),
|
||||
})
|
||||
.await;
|
||||
editor.handle_message(NodeGraphMessage::MoveNodeToChainStart { node_id, parent: layer }).await;
|
||||
|
||||
editor.eval_graph().await.expect("the Origins to Polyline chain should type-resolve and evaluate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,6 +1179,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) });
|
||||
}
|
||||
@@ -1431,7 +1433,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);
|
||||
@@ -1726,7 +1728,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 {
|
||||
@@ -2168,7 +2176,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 {
|
||||
|
||||
@@ -240,6 +240,12 @@ pub(crate) fn property_from_type(
|
||||
// For all other types, use TypeId-based matching
|
||||
_ => {
|
||||
use std::any::TypeId;
|
||||
|
||||
// The compiler peels a rank-0 `Item` cell to its element before this arm runs, so widgets dispatch on the bare element `T`
|
||||
fn id_is<T: 'static>(id: TypeId) -> bool {
|
||||
id == TypeId::of::<T>()
|
||||
}
|
||||
|
||||
match concrete_type.id {
|
||||
// ===============
|
||||
// PRIMITIVE TYPES
|
||||
@@ -265,48 +271,48 @@ pub(crate) fn property_from_type(
|
||||
// ============
|
||||
// STRUCT TYPES
|
||||
// ============
|
||||
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
|
||||
Some(x) if x == TypeId::of::<Footprint>() => footprint_widget(default_info, &mut extra_widgets),
|
||||
Some(x) if x == TypeId::of::<Box<VectorModification>>() => vector_modification_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<Image<Color>>() => image_data_widget(default_info).into(),
|
||||
Some(x) if id_is::<Font>(x) => font_widget(default_info),
|
||||
Some(x) if id_is::<Footprint>(x) => footprint_widget(default_info, &mut extra_widgets),
|
||||
Some(x) if id_is::<Box<VectorModification>>(x) => vector_modification_widget(default_info).into(),
|
||||
Some(x) if id_is::<Image<Color>>(x) => image_data_widget(default_info).into(),
|
||||
// ===============================
|
||||
// MANUALLY IMPLEMENTED ENUM TYPES
|
||||
// ===============================
|
||||
Some(x) if x == TypeId::of::<ReferencePoint>() => reference_point_widget(default_info, false).into(),
|
||||
Some(x) if x == TypeId::of::<BlendMode>() => blend_mode_widget(default_info),
|
||||
Some(x) if id_is::<ReferencePoint>(x) => reference_point_widget(default_info, false).into(),
|
||||
Some(x) if id_is::<BlendMode>(x) => blend_mode_widget(default_info),
|
||||
// =========================
|
||||
// AUTO-GENERATED ENUM TYPES
|
||||
// =========================
|
||||
Some(x) if x == TypeId::of::<GradientType>() => enum_choice::<GradientType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<GradientSpreadMethod>() => enum_choice::<GradientSpreadMethod>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RealTimeMode>() => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlue>() => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<XY>() => enum_choice::<XY>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StringCapitalization>() => enum_choice::<StringCapitalization>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<NoiseType>() => enum_choice::<NoiseType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<FractalType>() => enum_choice::<FractalType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<CellularDistanceFunction>() => enum_choice::<CellularDistanceFunction>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<CellularReturnType>() => enum_choice::<CellularReturnType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<DomainWarpType>() => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<RelativeAbsolute>() => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<GridType>() => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeCap>() => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeJoin>() => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RowsOrColumns>() => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ExtrudeJoiningAlgorithm>() => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<QRCodeErrorCorrectionLevel>() => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ScaleType>() => enum_choice::<ScaleType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<InterpolationDistribution>() => enum_choice::<InterpolationDistribution>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<GradientType>(x) => enum_choice::<GradientType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<GradientSpreadMethod>(x) => enum_choice::<GradientSpreadMethod>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<XY>(x) => enum_choice::<XY>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StringCapitalization>(x) => enum_choice::<StringCapitalization>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<NoiseType>(x) => enum_choice::<NoiseType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<FractalType>(x) => enum_choice::<FractalType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<CellularDistanceFunction>(x) => enum_choice::<CellularDistanceFunction>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<CellularReturnType>(x) => enum_choice::<CellularReturnType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<DomainWarpType>(x) => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<RelativeAbsolute>(x) => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<GridType>(x) => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeJoin>(x) => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeAlign>(x) => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<PaintOrder>(x) => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<ArcType>(x) => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<RowsOrColumns>(x) => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<TextAlign>(x) => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<MergeByDistanceAlgorithm>(x) => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<ExtrudeJoiningAlgorithm>(x) => enum_choice::<ExtrudeJoiningAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<PointSpacingType>(x) => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<BooleanOperation>(x) => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<CentroidType>(x) => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<LuminanceCalculation>(x) => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<QRCodeErrorCorrectionLevel>(x) => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<ScaleType>(x) => enum_choice::<ScaleType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<InterpolationDistribution>(x) => enum_choice::<InterpolationDistribution>().for_socket(default_info).property_row(),
|
||||
// =====
|
||||
// OTHER
|
||||
// =====
|
||||
@@ -2370,10 +2376,14 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
||||
let mut unit_suffix = None;
|
||||
let input_type = match implementation {
|
||||
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
|
||||
// Clone to end the `network_interface` borrow held via `implementation`, freeing the mutable borrow `input_type` needs below
|
||||
let proto_node_identifier = proto_node_identifier.clone();
|
||||
|
||||
let mut default_type = None;
|
||||
if let Some(field) = graphene_std::registry::NODE_METADATA
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(proto_node_identifier)
|
||||
.get(&proto_node_identifier)
|
||||
.and_then(|metadata| metadata.fields.get(input_index))
|
||||
{
|
||||
number_options = NumberOptions {
|
||||
@@ -2386,12 +2396,14 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
||||
display_decimal_places = field.number_display_decimal_places;
|
||||
unit_suffix = field.unit;
|
||||
step = field.number_step;
|
||||
if let Some(ref default) = field.default_type {
|
||||
break 'early_return default.clone();
|
||||
}
|
||||
default_type = field.default_type.clone();
|
||||
}
|
||||
|
||||
let Some(implementations) = &interpreted_executor::node_registry::NODE_REGISTRY.get(proto_node_identifier) else {
|
||||
if let Some(default) = default_type {
|
||||
break 'early_return default;
|
||||
}
|
||||
|
||||
let Some(implementations) = &interpreted_executor::node_registry::NODE_REGISTRY.get(&proto_node_identifier) else {
|
||||
log::error!("Could not get implementation for protonode {proto_node_identifier:?}");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes;
|
||||
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
|
||||
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_thick_wire_center_line, build_vector_wire};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
|
||||
use deserialization::deserialize_node_persistent_metadata;
|
||||
@@ -2498,14 +2498,20 @@ impl NodeNetworkInterface {
|
||||
let vertical_start: bool = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path));
|
||||
let thick = vertical_end && vertical_start;
|
||||
let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style);
|
||||
let center_line = build_thick_wire_center_line(output_position, input_position, vertical_start, vertical_end);
|
||||
|
||||
let path_string = vector_wire.to_svg();
|
||||
let data_type = self.input_type(&input, network_path).displayed_type();
|
||||
let center_path_string = center_line.to_svg();
|
||||
let input_type = self.input_type(&input, network_path);
|
||||
let data_type = input_type.displayed_type();
|
||||
let is_list = input_type.is_list();
|
||||
let wire_path_update = Some(WirePath {
|
||||
path_string,
|
||||
data_type,
|
||||
thick,
|
||||
dashed: false,
|
||||
is_list,
|
||||
center_path_string,
|
||||
});
|
||||
|
||||
Some(WirePathUpdate {
|
||||
@@ -2515,15 +2521,15 @@ impl NodeNetworkInterface {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the vector subpath and a boolean of whether the wire should be thick.
|
||||
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> {
|
||||
/// Returns the wire subpath, its thick center-line subpath, and whether the wire should be thick.
|
||||
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, BezPath, bool)> {
|
||||
let Some(input_position) = self.get_input_center(input, network_path) else {
|
||||
log::error!("Could not get dom rect for wire end: {input:?}");
|
||||
return None;
|
||||
};
|
||||
// An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector
|
||||
let Some(upstream_output) = self.upstream_output_connector(input, network_path) else {
|
||||
return Some((BezPath::new(), false));
|
||||
return Some((BezPath::new(), BezPath::new(), false));
|
||||
};
|
||||
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
|
||||
log::error!("Could not get output port for wire start: {:?}", upstream_output);
|
||||
@@ -2532,21 +2538,29 @@ impl NodeNetworkInterface {
|
||||
let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0);
|
||||
let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path));
|
||||
let thick = vertical_end && vertical_start;
|
||||
Some((build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style), thick))
|
||||
let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style);
|
||||
let center_line = build_thick_wire_center_line(output_position, input_position, vertical_start, vertical_end);
|
||||
Some((vector_wire, center_line, thick))
|
||||
}
|
||||
|
||||
pub fn wire_path_from_input(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, dashed: bool, network_path: &[NodeId]) -> Option<WirePath> {
|
||||
let (vector_wire, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?;
|
||||
let (vector_wire, center_line, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?;
|
||||
let path_string = vector_wire.to_svg();
|
||||
let data_type = self
|
||||
let center_path_string = center_line.to_svg();
|
||||
let (data_type, is_list) = self
|
||||
.upstream_output_connector(input, network_path)
|
||||
.map(|output| self.output_type(&output, network_path).displayed_type())
|
||||
.unwrap_or(FrontendGraphDataType::General);
|
||||
.map(|output| {
|
||||
let output_type = self.output_type(&output, network_path);
|
||||
(output_type.displayed_type(), output_type.is_list())
|
||||
})
|
||||
.unwrap_or((FrontendGraphDataType::General, false));
|
||||
Some(WirePath {
|
||||
path_string,
|
||||
data_type,
|
||||
thick,
|
||||
dashed,
|
||||
is_list,
|
||||
center_path_string,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6092,6 +6106,19 @@ impl NodeNetworkInterface {
|
||||
|
||||
// Chain is empty: wire the node as the first (and only) entry in the chain
|
||||
if matches!(current_input, NodeInput::Value { .. }) {
|
||||
// A node whose exposed primary defaults to no value inherits the layer's content value, so the chain keeps producing the layer's content type
|
||||
let node_primary = InputConnector::node(*node_id, 0);
|
||||
let default_is_valueless = self
|
||||
.input_from_connector(&node_primary, network_path)
|
||||
.is_some_and(|input| matches!(input, NodeInput::Value { tagged_value, exposed: true } if matches!(**tagged_value, TaggedValue::None)));
|
||||
if default_is_valueless {
|
||||
if import {
|
||||
self.set_input_for_import(&node_primary, current_input.clone(), network_path);
|
||||
} else {
|
||||
self.set_input(&node_primary, current_input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire: [parent] -> [new node]
|
||||
if import {
|
||||
self.set_input_for_import(&parent_input, NodeInput::node(*node_id, 0), network_path);
|
||||
|
||||
@@ -4,11 +4,7 @@ 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 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,33 @@ 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's identifier name, so semantic type checks can be rank-agnostic.
|
||||
pub fn compiled_element_name(&self) -> Option<String> {
|
||||
Some(self.compiled_nested_type()?.identifier_name())
|
||||
}
|
||||
|
||||
pub fn compiled_nested_type(&self) -> Option<&Type> {
|
||||
match self {
|
||||
TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()),
|
||||
@@ -206,6 +207,8 @@ 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)
|
||||
}
|
||||
|
||||
@@ -335,12 +338,19 @@ impl NodeNetworkInterface {
|
||||
pub fn output_type(&mut 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::node(*node_id, 0), 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::node(*node_id, 0), 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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -739,8 +739,12 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
// vector
|
||||
// ================================
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::apply_transform::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::ApplyTransformNode", "graphene_core::vector::vector_modification::ApplyTransformNode"],
|
||||
node: graphene_std::vector::bake_transform::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::vector::ApplyTransformNode",
|
||||
"graphene_core::vector::vector_modification::ApplyTransformNode",
|
||||
"vector_nodes::vector_modification_nodes::ApplyTransformNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::area::IDENTIFIER,
|
||||
@@ -1289,9 +1293,9 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
migrate_node(node_id, node, network_path, document, reset_node_definitions_on_open);
|
||||
}
|
||||
|
||||
// The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> "Text to Vector" pair, which reuses the same
|
||||
// proto identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current
|
||||
// 12-input node by the trailing `separate_glyphs` input (index 12): forward inputs 0..=11 onto the new node and move it onto `text_to_vector`.
|
||||
// The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> converter pair, which reuses the same proto
|
||||
// identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current 12-input
|
||||
// node by the trailing `separate_glyphs` input (index 12): forward inputs 0..=11 onto the new node and splice the matching converter after it.
|
||||
let old_text_nodes: Vec<(NodeId, Vec<NodeId>)> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
@@ -1325,7 +1329,8 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, new_index), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
let separate_glyphs = old_inputs.get(12).cloned();
|
||||
// A `true` toggle at index 12 chose per-glyph geometry, which is now the dedicated "Text to Vector Glyphs" node
|
||||
let separate_glyphs = matches!(old_inputs.get(12).and_then(|input| input.as_value()), Some(TaggedValue::Bool(true)));
|
||||
|
||||
// Collect the inputs reading the old text node's output before any rewiring so the new node can be spliced onto those wires.
|
||||
let downstream_consumers: Vec<InputConnector> = document
|
||||
@@ -1337,40 +1342,35 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
|
||||
let text_was_in_chain = text_nodes_in_chain.contains(node_id);
|
||||
|
||||
// Insert the `text_to_vector` node that converts the `text` `String[]` output back into vector geometry.
|
||||
let Some(text_to_vector_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER)) else {
|
||||
// Insert the converter that turns the `text` `String[]` output back into vector geometry: "Text to Vector Glyphs" for the per-glyph case, otherwise "Text to Vector".
|
||||
let converter_identifier = if separate_glyphs {
|
||||
graphene_std::text::text_to_vector_glyphs::IDENTIFIER
|
||||
} else {
|
||||
graphene_std::text::text_to_vector::IDENTIFIER
|
||||
};
|
||||
let Some(converter_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(converter_identifier)) else {
|
||||
continue;
|
||||
};
|
||||
let text_to_vector_id = NodeId::new();
|
||||
document
|
||||
.network_interface
|
||||
.insert_node(text_to_vector_id, text_to_vector_definition.default_node_template(), network_path);
|
||||
let converter_id = NodeId::new();
|
||||
document.network_interface.insert_node(converter_id, converter_definition.default_node_template(), network_path);
|
||||
|
||||
// Splice `text_to_vector` onto the wire(s) leaving `text` (`insert_node_between` is the pure wire-splice the editor uses for
|
||||
// dropping a node on a wire), then carry the old `separate_glyphs` value onto its second input.
|
||||
// Splice the converter onto the wire(s) leaving `text` (`insert_node_between` is the pure wire-splice the editor uses for dropping a node on a wire).
|
||||
if let Some((first_consumer, remaining_consumers)) = downstream_consumers.split_first() {
|
||||
document.network_interface.insert_node_between(&text_to_vector_id, first_consumer, 0, network_path);
|
||||
document.network_interface.insert_node_between(&converter_id, first_consumer, 0, network_path);
|
||||
for consumer in remaining_consumers {
|
||||
document.network_interface.set_input(consumer, NodeInput::node(text_to_vector_id, 0), network_path);
|
||||
document.network_interface.set_input(consumer, NodeInput::node(converter_id, 0), network_path);
|
||||
}
|
||||
} else {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(text_to_vector_id, 0), NodeInput::node(*node_id, 0), network_path);
|
||||
}
|
||||
if let Some(separate_glyphs) = separate_glyphs {
|
||||
document.network_interface.set_input(&InputConnector::node(text_to_vector_id, 1), separate_glyphs, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(converter_id, 0), NodeInput::node(*node_id, 0), network_path);
|
||||
}
|
||||
|
||||
// If `text` was in a layer chain, re-chain `text_to_vector` and its upstream so both lay out by distance from the layer (the splice
|
||||
// broke the chain, like `move_node_to_chain_start`). Otherwise `text` is absolute, so place `text_to_vector` beside it instead of
|
||||
// If `text` was in a layer chain, re-chain the converter and its upstream so both lay out by distance from the layer (the splice
|
||||
// broke the chain, like `move_node_to_chain_start`). Otherwise `text` is absolute, so place the converter beside it instead of
|
||||
// leaving it at the origin.
|
||||
if text_was_in_chain {
|
||||
document.network_interface.force_set_upstream_to_chain(&text_to_vector_id, network_path);
|
||||
document.network_interface.force_set_upstream_to_chain(&converter_id, network_path);
|
||||
} else if let Some(text_position) = document.network_interface.position(node_id, network_path) {
|
||||
document
|
||||
.network_interface
|
||||
.shift_absolute_node_position(&text_to_vector_id, text_position + IVec2::new(7, 0), network_path);
|
||||
document.network_interface.shift_absolute_node_position(&converter_id, text_position + IVec2::new(7, 0), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1639,8 +1639,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
inputs_count = 5;
|
||||
}
|
||||
|
||||
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the
|
||||
// value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform).
|
||||
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the value-model
|
||||
// 8-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _has_transform, _transform).
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
@@ -2192,6 +2192,13 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
|
||||
// A brush node saved before `Item<Raster<CPU>>` had a default stored its unconnected background as the invalid `()`,
|
||||
// which fails type resolution against the raster primary; adopt the definition's empty-raster default instead.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && matches!(node.inputs.first().and_then(|input| input.as_value()), Some(TaggedValue::None)) {
|
||||
let default_background = resolve_document_node_type(&reference)?.node_template.document_node.inputs.first()?.clone();
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), default_background, network_path);
|
||||
}
|
||||
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::RemoveHandlesNode")) {
|
||||
let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::auto_tangents::IDENTIFIER))?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
@@ -2408,7 +2415,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
// Migrate from the v2 "Morph" node (2 inputs: content, progression) to the v3 "Morph" node (5 inputs: content, progression, reverse, distribution, path).
|
||||
// The old progression used integer part for pair selection (range 0..N-1 where N is the number of content objects).
|
||||
// The new progression uses fractional 0..1 for euclidean traversal through all objects.
|
||||
// We insert Count Elements → Subtract 1 → Divide to remap: new_progression = old_progression / (N - 1).
|
||||
// We insert List Length → Subtract 1 → Divide to remap: new_progression = old_progression / (N - 1).
|
||||
// For the common 2-object case (N=2), this divides by 1 which is a no-op, preserving identical behavior.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::morph::IDENTIFIER) && inputs_count == 2 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
@@ -2463,10 +2470,10 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.insert_node(divide_id, divide_template, network_path);
|
||||
document.network_interface.shift_absolute_node_position(÷_id, morph_position + IVec2::new(-7, 1), network_path);
|
||||
|
||||
// Wire: content source → Count Elements input 0
|
||||
// Wire: content source → List Length input 0
|
||||
document.network_interface.set_input(&InputConnector::node(list_length_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
// Wire: Count Elements output → Subtract input 0 (minuend)
|
||||
// Wire: List Length output → Subtract input 0 (minuend)
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(list_length_id, 0), network_path);
|
||||
@@ -2677,6 +2684,34 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
}
|
||||
}
|
||||
|
||||
// A value input stored as a List-form TypeDefault adopts the definition's current default when the connector's declared default has since changed (e.g. the connector was ranked down to Item).
|
||||
// The red-slash no-paint choice shares that stored form but is a deliberate value, not a stale disconnect default, so it is exempt.
|
||||
if let Some(definition) = resolve_document_node_type(&reference) {
|
||||
let definition_inputs = definition.node_template.document_node.inputs.clone();
|
||||
for (index, definition_input) in definition_inputs.iter().enumerate() {
|
||||
if !matches!(definition_input, NodeInput::Value { .. }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stale_list_default = document
|
||||
.network_interface
|
||||
.input_from_connector(&InputConnector::node(*node_id, index), network_path)
|
||||
.is_some_and(|stored_input| match stored_input {
|
||||
NodeInput::Value { tagged_value, .. } => match &**tagged_value {
|
||||
TaggedValue::TypeDefault(stored_type) if stored_type.name.contains("list::List<") && !tagged_value.is_no_paint() => {
|
||||
!matches!(definition_input, NodeInput::Value { tagged_value, .. } if matches!(&**tagged_value, TaggedValue::TypeDefault(definition_type) if definition_type == stored_type))
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if stale_list_default {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, index), definition_input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// PUT ALL MIGRATIONS ABOVE THIS LINE
|
||||
// ==================================
|
||||
|
||||
@@ -692,7 +692,7 @@ pub struct SelectedStrokeState {
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
/// "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 layer is selected.
|
||||
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
|
||||
@@ -421,7 +421,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 Flatten Path 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 {
|
||||
|
||||
@@ -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>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_ranked_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -568,7 +568,7 @@ pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterfac
|
||||
}
|
||||
for _ in selected_layers {}
|
||||
|
||||
// Must be a layer of type List<Vector>
|
||||
// Must be a vector layer, at either rank
|
||||
let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?;
|
||||
|
||||
let output_type = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
|
||||
|
||||
@@ -465,7 +465,6 @@ impl NodeGraphExecutor {
|
||||
resolved_types: incomplete_delta,
|
||||
node_graph_errors,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
|
||||
return Err(format!("Node graph evaluation failed:\n{e}"));
|
||||
}
|
||||
@@ -476,7 +475,6 @@ impl NodeGraphExecutor {
|
||||
resolved_types: type_delta,
|
||||
node_graph_errors,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
NodeGraphUpdate::EyedropperPreview(raster) => {
|
||||
let (data, width, height) = raster.to_flat_u8();
|
||||
@@ -836,8 +834,8 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Whether the fill node's transform input is still the unset `OptionalDAffine2(None)` placeholder that the migration leaves
|
||||
/// behind, meaning its gradient placement has not yet been baked (or set by the user), so a measured bake may safely be written.
|
||||
/// Whether the fill node's `_has_transform` is still `false`, meaning its gradient placement has not yet been baked
|
||||
/// (or set by the user), so a measured bake may safely be written.
|
||||
fn fill_transform_unbaked(document: &DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) -> bool {
|
||||
let Some(network) = document.network_interface.document_network().nested_network(network_path) else {
|
||||
return false;
|
||||
@@ -946,11 +944,17 @@ mod test {
|
||||
let mut monitor_node_ids = Vec::with_capacity(node.inputs.len());
|
||||
for input in &mut node.inputs {
|
||||
let node_id = NodeId::new();
|
||||
let old_input = std::mem::replace(input, NodeInput::node(node_id, 0));
|
||||
monitor_nodes.push((old_input, node_id));
|
||||
path.push(node_id);
|
||||
monitor_node_ids.push(path.clone());
|
||||
path.pop();
|
||||
|
||||
// A None value is a unit wire with nothing to record and no Monitor row, so its slot stays a dead path that introspects as absent
|
||||
if matches!(input, NodeInput::Value { tagged_value, .. } if matches!(&**tagged_value, graph_craft::document::value::TaggedValue::None)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let old_input = std::mem::replace(input, NodeInput::node(node_id, 0));
|
||||
monitor_nodes.push((old_input, node_id));
|
||||
}
|
||||
if let DocumentNodeImplementation::ProtoNode(identifier) = &mut node.implementation {
|
||||
path.push(*id);
|
||||
@@ -982,13 +986,18 @@ mod test {
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
{
|
||||
let element = dynamic.downcast_ref::<Input::Result>().cloned();
|
||||
let element = Self::downcast_record::<Input::Result>(dynamic);
|
||||
if element.is_none() {
|
||||
warn!("cannot downcast type for introspection");
|
||||
}
|
||||
element
|
||||
}
|
||||
|
||||
/// Our monitor introspects as the recorded value itself, not as an `IORecord` wrapper.
|
||||
fn downcast_record<Output: Send + Sync + Clone + 'static>(dynamic: Arc<dyn std::any::Any + Send + Sync>) -> Option<Output> {
|
||||
dynamic.downcast_ref::<Output>().cloned()
|
||||
}
|
||||
|
||||
/// Grab all of the values of a LEVELED input, which introspects as its
|
||||
/// whole legacy list rather than as one element. `T` is the introspected
|
||||
/// element type, which differs from the declared one where a conversion
|
||||
@@ -1003,6 +1012,18 @@ mod test {
|
||||
.filter_map(|dynamic| dynamic.downcast_ref::<List<T>>().cloned())
|
||||
}
|
||||
|
||||
/// Like [`Self::grab_all_input_level`], but downcasting each record to `Output` instead of to the marker's `Result`.
|
||||
/// Useful when a stored value's recorded form differs from the declared row types the marker's generic accepts.
|
||||
pub fn grab_all_input_as<'a, Input: NodeInputDecleration + 'a, Output: Send + Sync + Clone + 'static>(&'a self, runtime: &'a NodeRuntime) -> impl Iterator<Item = Output> + 'a {
|
||||
self.protonodes_by_name
|
||||
.get(&Input::identifier())
|
||||
.map_or([].as_slice(), |x| x.as_slice())
|
||||
.iter()
|
||||
.filter_map(|inputs| inputs.get(Input::INDEX))
|
||||
.filter_map(|input_monitor_node| runtime.executor.introspect(input_monitor_node).ok())
|
||||
.filter_map(Instrumented::downcast_record::<Output>)
|
||||
}
|
||||
|
||||
pub fn grab_protonode_input<Input: NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
@@ -1014,6 +1035,14 @@ mod test {
|
||||
Self::downcast::<Input>(dynamic)
|
||||
}
|
||||
|
||||
/// Grabs a ranked input's recorded value as its bare element; our monitor serves a rank-0 input as the element itself.
|
||||
pub fn grab_ranked_input<Input: NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
{
|
||||
self.grab_protonode_input::<Input>(path, runtime)
|
||||
}
|
||||
|
||||
pub fn grab_input_from_layer<Input: NodeInputDecleration>(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
|
||||
Reference in New Issue
Block a user