mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 16:08:11 +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
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -6,13 +6,24 @@ use crate::messages::prelude::*;
|
|||||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||||
use glam::{Affine2, DAffine2, Vec2};
|
use glam::{Affine2, DAffine2, Vec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::animation::RealTimeMode;
|
||||||
use graphene_std::blending::BlendMode;
|
use graphene_std::blending::BlendMode;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::gradient::Gradient;
|
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::raster_types::{CPU, GPU, Raster};
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::text::TextAlign;
|
||||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
|
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 graphene_std::{Artboard, Color, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -195,24 +206,103 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
List<Gradient>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
List<f64>,
|
List<f64>,
|
||||||
List<u8>,
|
List<f32>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<i32>,
|
||||||
|
List<i64>,
|
||||||
List<bool>,
|
List<bool>,
|
||||||
|
List<DVec2>,
|
||||||
List<DAffine2>,
|
List<DAffine2>,
|
||||||
List<BlendMode>,
|
List<BlendMode>,
|
||||||
List<GradientType>,
|
List<GradientType>,
|
||||||
List<GradientSpreadMethod>,
|
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,
|
Gradient,
|
||||||
|
String,
|
||||||
f64,
|
f64,
|
||||||
|
f32,
|
||||||
u32,
|
u32,
|
||||||
u64,
|
u64,
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
bool,
|
bool,
|
||||||
String,
|
|
||||||
Option<f64>,
|
|
||||||
DVec2,
|
DVec2,
|
||||||
DAffine2,
|
DAffine2,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
GradientType,
|
GradientType,
|
||||||
GradientSpreadMethod,
|
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> {
|
impl<T: TableItemLayout> TableItemLayout for List<T> {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"List"
|
"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<'_> {
|
impl TableItemLayout for Graphic<'_> {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"Graphic"
|
"Graphic"
|
||||||
@@ -520,7 +701,7 @@ impl TableItemLayout for Raster<GPU> {
|
|||||||
format!("Raster ({} x {})", self.data().width(), self.data().height())
|
format!("Raster ({} x {})", self.data().width(), self.data().height())
|
||||||
}
|
}
|
||||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
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)]
|
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 {
|
impl TableItemLayout for u32 {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"Number (u32)"
|
"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 {
|
impl TableItemLayout for u64 {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"Number (u64)"
|
"Number (u64)"
|
||||||
@@ -734,45 +961,73 @@ impl TableItemLayout for Affine2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableItemLayout for BlendMode {
|
// Choice enums all display as their variant's label, shown inline as a plain text widget
|
||||||
fn type_name() -> &'static str {
|
macro_rules! impl_table_item_layout_for_choice_enum {
|
||||||
"BlendMode"
|
($($ty:ty),* $(,)?) => {
|
||||||
}
|
$(
|
||||||
fn identifier(&self) -> String {
|
impl TableItemLayout for $ty {
|
||||||
self.to_string()
|
fn type_name() -> &'static str {
|
||||||
}
|
stringify!($ty)
|
||||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
}
|
||||||
TextLabel::new(self.to_string()).narrow(true).widget_instance()
|
fn identifier(&self) -> String {
|
||||||
}
|
self.to_string()
|
||||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
}
|
||||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
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 {
|
fn type_name() -> &'static str {
|
||||||
"GradientType"
|
"ReferencePoint"
|
||||||
}
|
}
|
||||||
fn identifier(&self) -> String {
|
fn identifier(&self) -> String {
|
||||||
self.to_string()
|
format!("{self:?}")
|
||||||
}
|
}
|
||||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
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)])]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
||||||
@@ -915,9 +1170,7 @@ macro_rules! known_item_types {
|
|||||||
List<Color>,
|
List<Color>,
|
||||||
List<Gradient>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
List<NodeId>,
|
|
||||||
List<f64>,
|
List<f64>,
|
||||||
List<u8>,
|
|
||||||
Gradient,
|
Gradient,
|
||||||
Color,
|
Color,
|
||||||
NodeId,
|
NodeId,
|
||||||
@@ -927,9 +1180,12 @@ macro_rules! known_item_types {
|
|||||||
Vec2,
|
Vec2,
|
||||||
Option<f64>,
|
Option<f64>,
|
||||||
f64,
|
f64,
|
||||||
|
f32,
|
||||||
u8,
|
u8,
|
||||||
u32,
|
u32,
|
||||||
u64,
|
u64,
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
bool,
|
bool,
|
||||||
String,
|
String,
|
||||||
Vector,
|
Vector,
|
||||||
@@ -937,6 +1193,42 @@ macro_rules! known_item_types {
|
|||||||
Raster<GPU>,
|
Raster<GPU>,
|
||||||
Graphic,
|
Graphic,
|
||||||
Artboard,
|
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
|
/// 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 stroke get just a `Solidify Stroke` added.
|
||||||
/// Layers with only a fill, or neither, are left untouched.
|
/// Layers with only a fill, or neither, are left untouched.
|
||||||
fn handle_expand_fill_stroke_on_selected_layers(&mut self, responses: &mut VecDeque<Message>) {
|
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)"
|
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)],
|
inputs: vec![NodeInput::import(generic!(T), 4)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// 1: Count Elements (number of subpaths)
|
// 1: List Length (number of subpaths)
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(vector::list_length::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(vector::list_length::IDENTIFIER),
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||||
@@ -578,7 +578,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
NodeInput::node(NodeId(14), 0),
|
NodeInput::node(NodeId(14), 0),
|
||||||
NodeInput::value(TaggedValue::Bool(false), false),
|
NodeInput::value(TaggedValue::Bool(false), false),
|
||||||
NodeInput::import(concrete!(vector::misc::InterpolationDistribution), 3),
|
NodeInput::import(concrete!(vector::misc::InterpolationDistribution), 3),
|
||||||
NodeInput::import(generic!(T), 4),
|
NodeInput::import(concrete!(Vector), 4),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -637,7 +637,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// 1: Count Elements
|
// 1: List Length
|
||||||
DocumentNodeMetadata {
|
DocumentNodeMetadata {
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
persistent_metadata: DocumentNodePersistentMetadata {
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(2, 2)),
|
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),
|
implementation: DocumentNodeImplementation::ProtoNode(text_nodes::regex::regex_find::IDENTIFIER),
|
||||||
..Default::default()
|
..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 {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER),
|
||||||
..Default::default()
|
..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 {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER),
|
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)],
|
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||||
nodes: vec![
|
nodes: vec![
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::import(concrete!(List<Vector>), 0)],
|
inputs: vec![NodeInput::import(generic!(T), 0)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||||
call_argument: generic!(T),
|
call_argument: generic!(T),
|
||||||
skip_deduplication: true,
|
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))
|
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,
|
data_type: self.wire_in_progress_type,
|
||||||
thick: false,
|
thick: false,
|
||||||
dashed: false,
|
dashed: false,
|
||||||
|
is_list: false,
|
||||||
|
center_path_string: String::new(),
|
||||||
};
|
};
|
||||||
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
|
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
|
||||||
}
|
}
|
||||||
@@ -1431,7 +1433,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
return None;
|
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 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);
|
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
|
||||||
@@ -1726,7 +1728,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
continue;
|
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);
|
nodes.push(*node_id);
|
||||||
}
|
}
|
||||||
for error in &network_interface.resolved_types.node_graph_errors {
|
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);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
}
|
}
|
||||||
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
|
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);
|
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 => {
|
NodeGraphMessage::UpdateActionButtons => {
|
||||||
if selection_network_path == breadcrumb_network_path {
|
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
|
// For all other types, use TypeId-based matching
|
||||||
_ => {
|
_ => {
|
||||||
use std::any::TypeId;
|
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 {
|
match concrete_type.id {
|
||||||
// ===============
|
// ===============
|
||||||
// PRIMITIVE TYPES
|
// PRIMITIVE TYPES
|
||||||
@@ -265,48 +271,48 @@ pub(crate) fn property_from_type(
|
|||||||
// ============
|
// ============
|
||||||
// STRUCT TYPES
|
// STRUCT TYPES
|
||||||
// ============
|
// ============
|
||||||
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
|
Some(x) if id_is::<Font>(x) => font_widget(default_info),
|
||||||
Some(x) if x == TypeId::of::<Footprint>() => footprint_widget(default_info, &mut extra_widgets),
|
Some(x) if id_is::<Footprint>(x) => footprint_widget(default_info, &mut extra_widgets),
|
||||||
Some(x) if x == TypeId::of::<Box<VectorModification>>() => vector_modification_widget(default_info).into(),
|
Some(x) if id_is::<Box<VectorModification>>(x) => 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::<Image<Color>>(x) => image_data_widget(default_info).into(),
|
||||||
// ===============================
|
// ===============================
|
||||||
// MANUALLY IMPLEMENTED ENUM TYPES
|
// MANUALLY IMPLEMENTED ENUM TYPES
|
||||||
// ===============================
|
// ===============================
|
||||||
Some(x) if x == TypeId::of::<ReferencePoint>() => reference_point_widget(default_info, false).into(),
|
Some(x) if id_is::<ReferencePoint>(x) => reference_point_widget(default_info, false).into(),
|
||||||
Some(x) if x == TypeId::of::<BlendMode>() => blend_mode_widget(default_info),
|
Some(x) if id_is::<BlendMode>(x) => blend_mode_widget(default_info),
|
||||||
// =========================
|
// =========================
|
||||||
// AUTO-GENERATED ENUM TYPES
|
// AUTO-GENERATED ENUM TYPES
|
||||||
// =========================
|
// =========================
|
||||||
Some(x) if x == TypeId::of::<GradientType>() => enum_choice::<GradientType>().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 x == TypeId::of::<GradientSpreadMethod>() => enum_choice::<GradientSpreadMethod>().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 x == TypeId::of::<RealTimeMode>() => enum_choice::<RealTimeMode>().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 x == TypeId::of::<RedGreenBlue>() => enum_choice::<RedGreenBlue>().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 x == TypeId::of::<RedGreenBlueAlpha>() => enum_choice::<RedGreenBlueAlpha>().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 x == TypeId::of::<XY>() => enum_choice::<XY>().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 x == TypeId::of::<StringCapitalization>() => enum_choice::<StringCapitalization>().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 x == TypeId::of::<NoiseType>() => enum_choice::<NoiseType>().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 x == TypeId::of::<FractalType>() => enum_choice::<FractalType>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if id_is::<FractalType>(x) => 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 id_is::<CellularDistanceFunction>(x) => 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 id_is::<CellularReturnType>(x) => 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 id_is::<DomainWarpType>(x) => 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 id_is::<RelativeAbsolute>(x) => 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 id_is::<GridType>(x) => 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 id_is::<StrokeCap>(x) => 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 id_is::<StrokeJoin>(x) => 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 id_is::<StrokeAlign>(x) => 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 id_is::<PaintOrder>(x) => 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 id_is::<ArcType>(x) => 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 id_is::<RowsOrColumns>(x) => 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 id_is::<TextAlign>(x) => 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 id_is::<MergeByDistanceAlgorithm>(x) => 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 id_is::<ExtrudeJoiningAlgorithm>(x) => 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 id_is::<PointSpacingType>(x) => 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 id_is::<BooleanOperation>(x) => 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 id_is::<CentroidType>(x) => 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 id_is::<LuminanceCalculation>(x) => 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 id_is::<QRCodeErrorCorrectionLevel>(x) => 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 id_is::<ScaleType>(x) => 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::<InterpolationDistribution>(x) => enum_choice::<InterpolationDistribution>().for_socket(default_info).property_row(),
|
||||||
// =====
|
// =====
|
||||||
// OTHER
|
// OTHER
|
||||||
// =====
|
// =====
|
||||||
@@ -2370,10 +2376,14 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
let mut unit_suffix = None;
|
let mut unit_suffix = None;
|
||||||
let input_type = match implementation {
|
let input_type = match implementation {
|
||||||
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
|
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
|
if let Some(field) = graphene_std::registry::NODE_METADATA
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(proto_node_identifier)
|
.get(&proto_node_identifier)
|
||||||
.and_then(|metadata| metadata.fields.get(input_index))
|
.and_then(|metadata| metadata.fields.get(input_index))
|
||||||
{
|
{
|
||||||
number_options = NumberOptions {
|
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;
|
display_decimal_places = field.number_display_decimal_places;
|
||||||
unit_suffix = field.unit;
|
unit_suffix = field.unit;
|
||||||
step = field.number_step;
|
step = field.number_step;
|
||||||
if let Some(ref default) = field.default_type {
|
default_type = field.default_type.clone();
|
||||||
break 'early_return default.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:?}");
|
log::error!("Could not get implementation for protonode {proto_node_identifier:?}");
|
||||||
return Vec::new();
|
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::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
|
||||||
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
|
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::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::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
|
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
|
||||||
use deserialization::deserialize_node_persistent_metadata;
|
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 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 thick = vertical_end && vertical_start;
|
||||||
let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style);
|
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 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 {
|
let wire_path_update = Some(WirePath {
|
||||||
path_string,
|
path_string,
|
||||||
data_type,
|
data_type,
|
||||||
thick,
|
thick,
|
||||||
dashed: false,
|
dashed: false,
|
||||||
|
is_list,
|
||||||
|
center_path_string,
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(WirePathUpdate {
|
Some(WirePathUpdate {
|
||||||
@@ -2515,15 +2521,15 @@ impl NodeNetworkInterface {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the vector subpath and a boolean of whether the wire should be thick.
|
/// 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, bool)> {
|
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 {
|
let Some(input_position) = self.get_input_center(input, network_path) else {
|
||||||
log::error!("Could not get dom rect for wire end: {input:?}");
|
log::error!("Could not get dom rect for wire end: {input:?}");
|
||||||
return None;
|
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
|
// 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 {
|
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 {
|
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);
|
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_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 vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path));
|
||||||
let thick = vertical_end && vertical_start;
|
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> {
|
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 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)
|
.upstream_output_connector(input, network_path)
|
||||||
.map(|output| self.output_type(&output, network_path).displayed_type())
|
.map(|output| {
|
||||||
.unwrap_or(FrontendGraphDataType::General);
|
let output_type = self.output_type(&output, network_path);
|
||||||
|
(output_type.displayed_type(), output_type.is_list())
|
||||||
|
})
|
||||||
|
.unwrap_or((FrontendGraphDataType::General, false));
|
||||||
Some(WirePath {
|
Some(WirePath {
|
||||||
path_string,
|
path_string,
|
||||||
data_type,
|
data_type,
|
||||||
thick,
|
thick,
|
||||||
dashed,
|
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
|
// Chain is empty: wire the node as the first (and only) entry in the chain
|
||||||
if matches!(current_input, NodeInput::Value { .. }) {
|
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]
|
// Wire: [parent] -> [new node]
|
||||||
if import {
|
if import {
|
||||||
self.set_input_for_import(&parent_input, NodeInput::node(*node_id, 0), network_path);
|
self.set_input_for_import(&parent_input, NodeInput::node(*node_id, 0), network_path);
|
||||||
|
|||||||
+32
-22
@@ -4,11 +4,7 @@ use graph_craft::document::value::TaggedValue;
|
|||||||
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
|
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
|
||||||
use graph_craft::proto::{GraphErrorType, GraphErrors};
|
use graph_craft::proto::{GraphErrorType, GraphErrors};
|
||||||
use graph_craft::{Type, concrete};
|
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::uuid::NodeId;
|
||||||
use graphene_std::vector::Vector;
|
|
||||||
use graphene_std::{Artboard, Graphic};
|
|
||||||
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
|
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
|
||||||
use interpreted_executor::node_registry::NODE_REGISTRY;
|
use interpreted_executor::node_registry::NODE_REGISTRY;
|
||||||
|
|
||||||
@@ -56,28 +52,33 @@ impl TypeSource {
|
|||||||
return FrontendGraphDataType::Invalid;
|
return FrontendGraphDataType::Invalid;
|
||||||
};
|
};
|
||||||
match self.compiled_nested_type() {
|
match self.compiled_nested_type() {
|
||||||
Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) {
|
Some(nested_type) => FrontendGraphDataType::from_type(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,
|
|
||||||
},
|
|
||||||
None => FrontendGraphDataType::General,
|
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> {
|
pub fn compiled_nested_type(&self) -> Option<&Type> {
|
||||||
match self {
|
match self {
|
||||||
TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()),
|
TypeSource::Compiled(compiled_type) => Some(compiled_type.nested_type()),
|
||||||
@@ -206,6 +207,8 @@ impl NodeNetworkInterface {
|
|||||||
concrete!(())
|
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)
|
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 {
|
pub fn output_type(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> TypeSource {
|
||||||
match output_connector {
|
match output_connector {
|
||||||
OutputConnector::Node { node_id, output_index } => {
|
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
|
// First try iterating upstream to the first protonode and try get its compiled type
|
||||||
let Some(implementation) = self.implementation(node_id, network_path) else {
|
let Some(implementation) = self.implementation(node_id, network_path) else {
|
||||||
return TypeSource::Error("Could not get implementation");
|
return TypeSource::Error("Could not get implementation");
|
||||||
};
|
};
|
||||||
match implementation {
|
match implementation {
|
||||||
DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()),
|
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()) {
|
DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) {
|
||||||
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
|
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
|
||||||
None => TypeSource::Unknown,
|
None => TypeSource::Unknown,
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ pub struct WirePath {
|
|||||||
pub data_type: FrontendGraphDataType,
|
pub data_type: FrontendGraphDataType,
|
||||||
pub thick: bool,
|
pub thick: bool,
|
||||||
pub dashed: 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))]
|
#[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 {
|
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.;
|
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 {
|
match graph_wire_style {
|
||||||
GraphWireStyle::Direct => {
|
GraphWireStyle::Direct => {
|
||||||
let horizontal_gap = (output_position.x - input_position.x).abs();
|
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> {
|
fn straight_wire_path(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
|
||||||
let grid_spacing = 24;
|
let grid_spacing = 24;
|
||||||
let line_width = 2;
|
let line_width = 2;
|
||||||
|
|||||||
@@ -739,8 +739,12 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
// vector
|
// vector
|
||||||
// ================================
|
// ================================
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::apply_transform::IDENTIFIER,
|
node: graphene_std::vector::bake_transform::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::ApplyTransformNode", "graphene_core::vector::vector_modification::ApplyTransformNode"],
|
aliases: &[
|
||||||
|
"graphene_core::vector::ApplyTransformNode",
|
||||||
|
"graphene_core::vector::vector_modification::ApplyTransformNode",
|
||||||
|
"vector_nodes::vector_modification_nodes::ApplyTransformNode",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::area::IDENTIFIER,
|
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);
|
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
|
// The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> converter pair, which reuses the same proto
|
||||||
// proto identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current
|
// identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current 12-input
|
||||||
// 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`.
|
// 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
|
let old_text_nodes: Vec<(NodeId, Vec<NodeId>)> = document
|
||||||
.network_interface
|
.network_interface
|
||||||
.document_network()
|
.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);
|
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.
|
// 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
|
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);
|
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.
|
// 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 Some(text_to_vector_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER)) else {
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
let text_to_vector_id = NodeId::new();
|
let converter_id = NodeId::new();
|
||||||
document
|
document.network_interface.insert_node(converter_id, converter_definition.default_node_template(), network_path);
|
||||||
.network_interface
|
|
||||||
.insert_node(text_to_vector_id, text_to_vector_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
|
// 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).
|
||||||
// dropping a node on a wire), then carry the old `separate_glyphs` value onto its second input.
|
|
||||||
if let Some((first_consumer, remaining_consumers)) = downstream_consumers.split_first() {
|
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 {
|
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 {
|
} else {
|
||||||
document
|
document.network_interface.set_input(&InputConnector::node(converter_id, 0), NodeInput::node(*node_id, 0), network_path);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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 `text_to_vector` beside it instead of
|
// 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.
|
// leaving it at the origin.
|
||||||
if text_was_in_chain {
|
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) {
|
} else if let Some(text_position) = document.network_interface.position(node_id, network_path) {
|
||||||
document
|
document.network_interface.shift_absolute_node_position(&converter_id, text_position + IVec2::new(7, 0), network_path);
|
||||||
.network_interface
|
|
||||||
.shift_absolute_node_position(&text_to_vector_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;
|
inputs_count = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the
|
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the value-model
|
||||||
// value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform).
|
// 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 {
|
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();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
document.network_interface.replace_implementation(node_id, network_path, &mut 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);
|
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")) {
|
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();
|
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);
|
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).
|
// 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 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.
|
// 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.
|
// 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 {
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::morph::IDENTIFIER) && inputs_count == 2 {
|
||||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
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.insert_node(divide_id, divide_template, network_path);
|
||||||
document.network_interface.shift_absolute_node_position(÷_id, morph_position + IVec2::new(-7, 1), 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);
|
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
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(list_length_id, 0), network_path);
|
.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
|
// 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.
|
/// 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.
|
/// Unticked means there is no Fill node. Returns `None` only when no layer is selected.
|
||||||
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
|
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
|
||||||
let selected_nodes = document.network_interface.selected_nodes();
|
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)
|
(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>) {
|
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
|
// Apply a zero-delta to one of the points to trigger reorganization
|
||||||
let dummy_modification = VectorModificationType::ApplyPointDelta {
|
let dummy_modification = VectorModificationType::ApplyPointDelta {
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ mod test_ellipse {
|
|||||||
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||||
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::IDENTIFIER)?;
|
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::IDENTIFIER)?;
|
||||||
Some(ResolvedEllipse {
|
Some(ResolvedEllipse {
|
||||||
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
radius_x: instrumented.grab_ranked_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&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),
|
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 {}
|
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 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), &[]);
|
let output_type = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
|
||||||
|
|||||||
@@ -465,7 +465,6 @@ impl NodeGraphExecutor {
|
|||||||
resolved_types: incomplete_delta,
|
resolved_types: incomplete_delta,
|
||||||
node_graph_errors,
|
node_graph_errors,
|
||||||
});
|
});
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
|
||||||
|
|
||||||
return Err(format!("Node graph evaluation failed:\n{e}"));
|
return Err(format!("Node graph evaluation failed:\n{e}"));
|
||||||
}
|
}
|
||||||
@@ -476,7 +475,6 @@ impl NodeGraphExecutor {
|
|||||||
resolved_types: type_delta,
|
resolved_types: type_delta,
|
||||||
node_graph_errors,
|
node_graph_errors,
|
||||||
});
|
});
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
|
||||||
}
|
}
|
||||||
NodeGraphUpdate::EyedropperPreview(raster) => {
|
NodeGraphUpdate::EyedropperPreview(raster) => {
|
||||||
let (data, width, height) = raster.to_flat_u8();
|
let (data, width, height) = raster.to_flat_u8();
|
||||||
@@ -836,8 +834,8 @@ impl NodeGraphExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// 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
|
/// Whether the fill node's `_has_transform` is still `false`, meaning its gradient placement has not yet been baked
|
||||||
/// behind, meaning its gradient placement has not yet been baked (or set by the user), so a measured bake may safely be written.
|
/// (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 {
|
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 {
|
let Some(network) = document.network_interface.document_network().nested_network(network_path) else {
|
||||||
return false;
|
return false;
|
||||||
@@ -946,11 +944,17 @@ mod test {
|
|||||||
let mut monitor_node_ids = Vec::with_capacity(node.inputs.len());
|
let mut monitor_node_ids = Vec::with_capacity(node.inputs.len());
|
||||||
for input in &mut node.inputs {
|
for input in &mut node.inputs {
|
||||||
let node_id = NodeId::new();
|
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);
|
path.push(node_id);
|
||||||
monitor_node_ids.push(path.clone());
|
monitor_node_ids.push(path.clone());
|
||||||
path.pop();
|
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 {
|
if let DocumentNodeImplementation::ProtoNode(identifier) = &mut node.implementation {
|
||||||
path.push(*id);
|
path.push(*id);
|
||||||
@@ -982,13 +986,18 @@ mod test {
|
|||||||
where
|
where
|
||||||
Input::Result: Send + Sync + Clone + 'static,
|
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() {
|
if element.is_none() {
|
||||||
warn!("cannot downcast type for introspection");
|
warn!("cannot downcast type for introspection");
|
||||||
}
|
}
|
||||||
element
|
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
|
/// 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
|
/// whole legacy list rather than as one element. `T` is the introspected
|
||||||
/// element type, which differs from the declared one where a conversion
|
/// 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())
|
.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>
|
pub fn grab_protonode_input<Input: NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||||
where
|
where
|
||||||
Input::Result: Send + Sync + Clone + 'static,
|
Input::Result: Send + Sync + Clone + 'static,
|
||||||
@@ -1014,6 +1035,14 @@ mod test {
|
|||||||
Self::downcast::<Input>(dynamic)
|
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>
|
pub fn grab_input_from_layer<Input: NodeInputDecleration>(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||||
where
|
where
|
||||||
Input::Result: Send + Sync + Clone + 'static,
|
Input::Result: Send + Sync + Clone + 'static,
|
||||||
|
|||||||
@@ -320,7 +320,7 @@
|
|||||||
<div class="wires" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
|
<div class="wires" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
|
||||||
<svg>
|
<svg>
|
||||||
{#each $nodeGraphWires.values() as map}
|
{#each $nodeGraphWires.values() as map}
|
||||||
{#each map.values() as { pathString, dataType, thick, dashed }}
|
{#each map.values() as { pathString, centerPathString, dataType, thick, dashed }}
|
||||||
{#if thick}
|
{#if thick}
|
||||||
<path
|
<path
|
||||||
d={pathString}
|
d={pathString}
|
||||||
@@ -329,6 +329,8 @@
|
|||||||
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
||||||
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
|
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
|
||||||
/>
|
/>
|
||||||
|
<!-- A thin inner line splits the layer-stack wire down the middle reaching past the ends into the cleaved connector slots -->
|
||||||
|
<path d={centerPathString} style:--data-line-width="2px" style:--data-color="#444444" style:--data-color-dim="#444444" style:--data-dasharray={`3,${dashed ? 2 : 0}`} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -561,7 +563,10 @@
|
|||||||
{#if node.primaryOutput.connectedTo.length > 0}
|
{#if node.primaryOutput.connectedTo.length > 0}
|
||||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
||||||
{#if node.primaryOutputConnectedToLayer}
|
{#if node.primaryOutputConnectedToLayer}
|
||||||
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
|
<path
|
||||||
|
d="M0,4.5 C0,4.5 0,-3.5 0,-3.5 C0,-3.5 3,-3.5 3,-3.5 C3,-3.5 3,2.565 3,2.565 C2.834,2.632 2.673,2.717 2.52,2.819 C2.52,2.819 0,4.5 0,4.5 ZM5,2.566 C5,2.566 5,-3.5 5,-3.5 C5,-3.5 8,-3.5 8,-3.5 C8,-3.5 8,4.5 8,4.5 C8,4.5 5.479,2.819 5.479,2.819 C5.326,2.717 5.166,2.633 5,2.566 Z"
|
||||||
|
fill="var(--data-color-dim)"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color-dim)" />
|
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color-dim)" />
|
||||||
@@ -583,7 +588,10 @@
|
|||||||
{#if node.primaryInput?.connectedTo !== "Connected to nothing."}
|
{#if node.primaryInput?.connectedTo !== "Connected to nothing."}
|
||||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
||||||
{#if node.primaryInputConnectedToLayer}
|
{#if node.primaryInputConnectedToLayer}
|
||||||
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
|
<path
|
||||||
|
d="M2.512,9.26 C2.673,9.157 2.834,9.072 3,9 C3,9 3,16 3,16 C3,16 0,16 0,16 C0,16 0,10.95 0,10.95 C0,10.95 2.512,9.26 2.512,9.26 ZM5,16 C5,16 5,9 5,9 C5.166,9.073 5.327,9.158 5.48,9.26 C5.48,9.26 8,10.95 8,10.95 C8,10.95 8,16 8,16 C8,16 5,16 5,16 Z"
|
||||||
|
fill="var(--data-color-dim)"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color-dim)" />
|
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color-dim)" />
|
||||||
@@ -679,15 +687,27 @@
|
|||||||
<div class="wires">
|
<div class="wires">
|
||||||
<svg>
|
<svg>
|
||||||
{#each $nodeGraphWires.values() as map}
|
{#each $nodeGraphWires.values() as map}
|
||||||
{#each map.values() as { pathString, dataType, thick, dashed }}
|
{#each map.values() as { pathString, dataType, thick, dashed, isList }}
|
||||||
{#if !thick}
|
{#if !thick}
|
||||||
<path
|
{#if isList}
|
||||||
d={pathString}
|
<!-- A rank-1 List wire reads as two parallel lines: a triple-width data line split down the middle by a 1x background-colored overlay -->
|
||||||
style:--data-line-width="2px"
|
<path
|
||||||
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
|
d={pathString}
|
||||||
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
style:--data-line-width="4px"
|
||||||
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
|
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
|
||||||
/>
|
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
||||||
|
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
|
||||||
|
/>
|
||||||
|
<path d={pathString} style:--data-line-width="2px" style:--data-color="#444444" style:--data-color-dim="#444444" style:--data-dasharray={`3,${dashed ? 2 : 0}`} />
|
||||||
|
{:else}
|
||||||
|
<path
|
||||||
|
d={pathString}
|
||||||
|
style:--data-line-width="2px"
|
||||||
|
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
|
||||||
|
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
||||||
|
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use brush_nodes::brush_stroke::BrushStroke;
|
|||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::context::Context;
|
use core_types::context::Context;
|
||||||
use core_types::gpoll::GPoll;
|
use core_types::gpoll::GPoll;
|
||||||
use core_types::list::List;
|
use core_types::list::{Item, List};
|
||||||
use core_types::registry::SourceHandle;
|
use core_types::registry::SourceHandle;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -125,7 +125,7 @@ macro_rules! tagged_value {
|
|||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
Self::NodeIdPath(path) => path.hash(state),
|
Self::NodeIdPath(path) => path.cache_hash(state),
|
||||||
Self::DocumentNode(node) => node.cache_hash(state),
|
Self::DocumentNode(node) => node.cache_hash(state),
|
||||||
Self::ContextModification(modification) => modification.cache_hash(state),
|
Self::ContextModification(modification) => modification.cache_hash(state),
|
||||||
Self::RenderOutput(x) => x.cache_hash(state),
|
Self::RenderOutput(x) => x.cache_hash(state),
|
||||||
@@ -169,7 +169,7 @@ macro_rules! tagged_value {
|
|||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( Self::$identifier(x) => Box::new(x), )*
|
$( Self::$identifier(x) => Box::new(Item::new_from_element(x)), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -178,7 +178,7 @@ macro_rules! tagged_value {
|
|||||||
Self::DocumentNode(node) => Box::new(node),
|
Self::DocumentNode(node) => Box::new(node),
|
||||||
Self::ContextModification(modification) => Box::new(modification),
|
Self::ContextModification(modification) => Box::new(modification),
|
||||||
Self::EditorApi(x) => Box::new(x),
|
Self::EditorApi(x) => Box::new(x),
|
||||||
Self::ResourceHash(x) => Box::new(x),
|
Self::ResourceHash(x) => Box::new(Item::new_from_element(x)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ macro_rules! tagged_value {
|
|||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( Self::$identifier(x) => Arc::new(x), )*
|
$( Self::$identifier(x) => Arc::new(Item::new_from_element(x)), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -222,7 +222,7 @@ macro_rules! tagged_value {
|
|||||||
Self::DocumentNode(node) => Arc::new(node),
|
Self::DocumentNode(node) => Arc::new(node),
|
||||||
Self::ContextModification(modification) => Arc::new(modification),
|
Self::ContextModification(modification) => Arc::new(modification),
|
||||||
Self::EditorApi(x) => Arc::new(x),
|
Self::EditorApi(x) => Arc::new(x),
|
||||||
Self::ResourceHash(x) => Arc::new(x),
|
Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,10 +397,11 @@ macro_rules! tagged_value {
|
|||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
|
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
|
||||||
|
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(downcast::<Item<$ty>>(input).unwrap().into_element())), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(downcast::<Item<RenderOutput>>(input).unwrap().into_element())),
|
||||||
|
|
||||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||||
}
|
}
|
||||||
@@ -419,10 +420,11 @@ macro_rules! tagged_value {
|
|||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
|
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
|
||||||
|
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(Item::<$ty>::clone(input.downcast_ref().unwrap()).into_element())), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))),
|
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(Item::<RenderOutput>::clone(input.downcast_ref().unwrap()).into_element())),
|
||||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))),
|
_ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,8 +548,6 @@ tagged_value! {
|
|||||||
LegacyOptionalDAffine2(Option<DAffine2>),
|
LegacyOptionalDAffine2(Option<DAffine2>),
|
||||||
#[serde(alias = "FillGradient")]
|
#[serde(alias = "FillGradient")]
|
||||||
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
||||||
#[serde(alias = "Fill")]
|
|
||||||
LegacyFill(graphic_types::migrations::legacy::LegacyFill),
|
|
||||||
// ==========
|
// ==========
|
||||||
// ENUM TYPES
|
// ENUM TYPES
|
||||||
// ==========
|
// ==========
|
||||||
@@ -589,6 +589,9 @@ tagged_value! {
|
|||||||
BooleanOperation(vector::misc::BooleanOperation),
|
BooleanOperation(vector::misc::BooleanOperation),
|
||||||
TextAlign(text_nodes::TextAlign),
|
TextAlign(text_nodes::TextAlign),
|
||||||
ScaleType(core_types::transform::ScaleType),
|
ScaleType(core_types::transform::ScaleType),
|
||||||
|
// Legacy
|
||||||
|
#[serde(alias = "Fill")]
|
||||||
|
LegacyFill(graphic_types::migrations::legacy::LegacyFill),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaggedValue {
|
impl TaggedValue {
|
||||||
@@ -729,6 +732,9 @@ impl TaggedValue {
|
|||||||
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
||||||
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(TaggedValue::Color)?,
|
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(TaggedValue::Color)?,
|
||||||
() if ty == TypeId::of::<List<Gradient>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
() if ty == TypeId::of::<List<Gradient>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||||
|
// A paint default also parses against the bare element forms, as a color or gradient literal
|
||||||
|
() if ty == TypeId::of::<Graphic>() => to_color(string).map(TaggedValue::Color)?,
|
||||||
|
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||||
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||||
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(DashPattern::from(string)),
|
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(DashPattern::from(string)),
|
||||||
() if ty == TypeId::of::<BoxCorners>() => TaggedValue::BoxCorners(BoxCorners::from(string)),
|
() if ty == TypeId::of::<BoxCorners>() => TaggedValue::BoxCorners(BoxCorners::from(string)),
|
||||||
@@ -1018,6 +1024,23 @@ mod record_defaults {
|
|||||||
mod paint_default_parsing {
|
mod paint_default_parsing {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// A Fill/Stroke paint wire carries `Graphic` elements, so its `Color::BLACK` default must parse
|
||||||
|
/// into a `Color` for a fresh Fill node's paint to resolve.
|
||||||
|
#[test]
|
||||||
|
fn paint_wire_parses_color_default_through_its_element() {
|
||||||
|
let black = Some(TaggedValue::Color(Color::BLACK));
|
||||||
|
assert_eq!(
|
||||||
|
TaggedValue::from_primitive_string("Color::BLACK", &concrete!(List<Graphic>)),
|
||||||
|
black,
|
||||||
|
"a `List<Graphic>` paint wire should resolve its color default"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TaggedValue::from_primitive_string("Color::BLACK", &concrete!(Graphic)),
|
||||||
|
black,
|
||||||
|
"a bare `Graphic` paint element should resolve its color default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Table-era documents stored the red-slash "no paint" fill as an empty color table, which must keep
|
/// Table-era documents stored the red-slash "no paint" fill as an empty color table, which must keep
|
||||||
/// deserializing to [`TaggedValue::no_paint`] rather than collapsing to a transparent color.
|
/// deserializing to [`TaggedValue::no_paint`] rather than collapsing to a transparent color.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ wgpu = ["dep:raster-types", "raster-types/wgpu"]
|
|||||||
# Local dependencies
|
# Local dependencies
|
||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
graphene-hash = { workspace = true }
|
graphene-hash = { workspace = true, features = ["derive"] }
|
||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
text-nodes = { workspace = true }
|
text-nodes = { workspace = true }
|
||||||
graphene-resource = { workspace = true }
|
graphene-resource = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
use graphene_hash::CacheHash;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ptr::addr_of;
|
use std::ptr::addr_of;
|
||||||
@@ -61,7 +62,7 @@ pub trait GetEditorPreferences {
|
|||||||
fn max_render_region_area(&self) -> u32;
|
fn max_render_region_area(&self) -> u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, CacheHash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum ExportFormat {
|
pub enum ExportFormat {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -69,14 +70,14 @@ pub enum ExportFormat {
|
|||||||
Raster,
|
Raster,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct TimingInformation {
|
pub struct TimingInformation {
|
||||||
pub time: f64,
|
pub time: f64,
|
||||||
pub animation_time: Duration,
|
pub animation_time: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, CacheHash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct RenderConfig {
|
pub struct RenderConfig {
|
||||||
pub viewport: Footprint,
|
pub viewport: Footprint,
|
||||||
@@ -136,7 +137,7 @@ impl<Io> Hash for EditorApi<Io> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Io> core_types::graphene_hash::CacheHash for EditorApi<Io> {
|
impl<Io> CacheHash for EditorApi<Io> {
|
||||||
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
core::hash::Hash::hash(self, state);
|
core::hash::Hash::hash(self, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,6 +601,11 @@ impl ItemAttributeValues {
|
|||||||
self.0.iter().map(|(key, value)| (key.as_str(), &**value))
|
self.0.iter().map(|(key, value)| (key.as_str(), &**value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a type-erased reference to the value of the attribute with the given key, if it exists.
|
||||||
|
pub fn get_any(&self, key: &str) -> Option<&dyn std::any::Any> {
|
||||||
|
self.0.iter().find_map(|(existing_key, value)| if existing_key == key { Some((**value).as_any()) } else { None })
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a debug-formatted string representation of the attribute value for the given key, if it exists.
|
/// Returns a debug-formatted string representation of the attribute value for the given key, if it exists.
|
||||||
/// The `overrides` function can provide custom formatting for specific type.
|
/// The `overrides` function can provide custom formatting for specific type.
|
||||||
pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
|
pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
|
||||||
@@ -1326,6 +1331,10 @@ impl<T> Item<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe impl<T: StaticTypeSized> StaticType for Item<T> {
|
||||||
|
type Static = Item<T::Static>;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// ItemIter<T>
|
// ItemIter<T>
|
||||||
// ===========
|
// ===========
|
||||||
|
|||||||
@@ -61,6 +61,27 @@ impl Clampable for DVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement for ranked wires (element-wise clamping across the frame)
|
||||||
|
use crate::list::{Item, List};
|
||||||
|
impl<T: Clampable> Clampable for Item<T> {
|
||||||
|
fn clamp_hard_min(self, min: f64) -> Self {
|
||||||
|
let (element, attributes) = self.into_parts();
|
||||||
|
Item::from_parts(element.clamp_hard_min(min), attributes)
|
||||||
|
}
|
||||||
|
fn clamp_hard_max(self, max: f64) -> Self {
|
||||||
|
let (element, attributes) = self.into_parts();
|
||||||
|
Item::from_parts(element.clamp_hard_max(max), attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Clampable> Clampable for List<T> {
|
||||||
|
fn clamp_hard_min(self, min: f64) -> Self {
|
||||||
|
self.into_iter().map(|item| item.clamp_hard_min(min)).collect()
|
||||||
|
}
|
||||||
|
fn clamp_hard_max(self, max: f64) -> Self {
|
||||||
|
self.into_iter().map(|item| item.clamp_hard_max(max)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct LegacyTable<T> {
|
struct LegacyTable<T> {
|
||||||
|
|||||||
@@ -77,8 +77,7 @@ impl Convert<DVec2, ()> for DVec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs `Self` from a single anchor point at the given position. Implemented by the vector crate's
|
/// Constructs `Self` from a single anchor point at the given position. Implemented by the vector crate's
|
||||||
/// path type so the `Convert` impl below can build a single-point path without core-types depending on
|
/// path type so a position wire can convert to a single-point path without core-types depending on that crate.
|
||||||
/// that crate (mirroring how [`ListConvert`] bridges per-item list conversions).
|
|
||||||
pub trait FromAnchorPosition {
|
pub trait FromAnchorPosition {
|
||||||
fn from_anchor_position(position: DVec2) -> Self;
|
fn from_anchor_position(position: DVec2) -> Self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,23 @@ impl From<()> for Footprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consumes an item's `transform` attribute by baking it into the underlying value itself.
|
||||||
|
pub trait BakeTransform {
|
||||||
|
fn bake_transform(&mut self, transform: &DAffine2);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BakeTransform for DAffine2 {
|
||||||
|
fn bake_transform(&mut self, transform: &DAffine2) {
|
||||||
|
*self = *transform * *self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BakeTransform for DVec2 {
|
||||||
|
fn bake_transform(&mut self, transform: &DAffine2) {
|
||||||
|
*self = transform.transform_point2(*self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ApplyTransform {
|
pub trait ApplyTransform {
|
||||||
fn apply_transform(&mut self, modification: &DAffine2);
|
fn apply_transform(&mut self, modification: &DAffine2);
|
||||||
fn left_apply_transform(&mut self, modification: &DAffine2);
|
fn left_apply_transform(&mut self, modification: &DAffine2);
|
||||||
|
|||||||
@@ -1767,7 +1767,7 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
||||||
// Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) produce hit areas for every glyph.
|
// Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph.
|
||||||
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
|
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
|
||||||
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
|
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
|
||||||
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
|
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ impl core_types::ops::FromAnchorPosition for Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lets a position wire feed a ranked vector connector through the input adapter's element conversion
|
||||||
|
impl From<DVec2> for Vector {
|
||||||
|
fn from(position: DVec2) -> Self {
|
||||||
|
<Self as core_types::ops::FromAnchorPosition>::from_anchor_position(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Identity item conversion so `List<Vector>` satisfies the blanket `Convert<List<U>, ()> for List<T>`, letting its
|
// Identity item conversion so `List<Vector>` satisfies the blanket `Convert<List<U>, ()> for List<T>`, letting its
|
||||||
// auto-inserted input wrapper be a `ConvertNode` (which also accepts a `DVec2` anchor position) rather than an `IntoNode`.
|
// auto-inserted input wrapper be a `ConvertNode` (which also accepts a `DVec2` anchor position) rather than an `IntoNode`.
|
||||||
impl core_types::ops::ListConvert<Vector> for Vector {
|
impl core_types::ops::ListConvert<Vector> for Vector {
|
||||||
@@ -71,6 +78,15 @@ impl core_types::ops::ListConvert<Vector> for Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl core_types::transform::BakeTransform for Vector {
|
||||||
|
fn bake_transform(&mut self, transform: &glam::DAffine2) {
|
||||||
|
for (_, point) in self.point_domain.positions_mut() {
|
||||||
|
*point = transform.transform_point2(*point);
|
||||||
|
}
|
||||||
|
self.segment_domain.transform(*transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Vector {
|
impl Vector {
|
||||||
/// Add a subpath to this vector path.
|
/// Add a subpath to this vector path.
|
||||||
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
|
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster<
|
|||||||
device.create_texture_with_data(
|
device.create_texture_with_data(
|
||||||
queue,
|
queue,
|
||||||
&TextureDescriptor {
|
&TextureDescriptor {
|
||||||
label: Some("upload_texture node texture"),
|
label: Some("upload_to_texture staging texture"),
|
||||||
size: Extent3d {
|
size: Extent3d {
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut backgroun
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::brush_stroke::BrushStroke;
|
||||||
use core_types::transform::Transform;
|
use core_types::transform::Transform;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use core_types::CacheHash;
|
use core_types::CacheHash;
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
|
use core_types::list::{Item, List};
|
||||||
use core_types::math::bbox::AxisAlignedBbox;
|
use core_types::math::bbox::AxisAlignedBbox;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
@@ -57,6 +58,22 @@ pub struct BrushStroke {
|
|||||||
pub trace: Vec<BrushInputSample>,
|
pub trace: Vec<BrushInputSample>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One Brush layer's full sequence of strokes, treated as a single rank-0 value rather than a frame of independent strokes.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, CacheHash, DynAny)]
|
||||||
|
pub struct BrushTrace(pub List<BrushStroke>);
|
||||||
|
|
||||||
|
impl From<List<BrushStroke>> for BrushTrace {
|
||||||
|
fn from(strokes: List<BrushStroke>) -> Self {
|
||||||
|
Self(strokes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<BrushStroke>> for BrushTrace {
|
||||||
|
fn from(strokes: Vec<BrushStroke>) -> Self {
|
||||||
|
Self(strokes.into_iter().map(Item::new_from_element).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BrushStroke {
|
impl BrushStroke {
|
||||||
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
||||||
let radius = self.style.diameter / 2.;
|
let radius = self.style.diameter / 2.;
|
||||||
|
|||||||
@@ -74,15 +74,23 @@ fn quantize_real_time<T>(
|
|||||||
Context -> DAffine2,
|
Context -> DAffine2,
|
||||||
Context -> Footprint,
|
Context -> Footprint,
|
||||||
Context -> DVec2,
|
Context -> DVec2,
|
||||||
|
Context -> Vector,
|
||||||
|
Context -> Graphic,
|
||||||
|
Context -> Raster<CPU>,
|
||||||
|
Context -> Raster<GPU>,
|
||||||
|
Context -> Color,
|
||||||
|
Context -> Gradient,
|
||||||
|
Context -> Artboard,
|
||||||
|
Context -> List<String>,
|
||||||
|
Context -> List<f64>,
|
||||||
|
Context -> List<DVec2>,
|
||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Graphic>,
|
Context -> List<Graphic>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
Context -> List<Artboard>,
|
|
||||||
Context -> List<Gradient>,
|
Context -> List<Gradient>,
|
||||||
Context -> List<String>,
|
Context -> List<Artboard>,
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
Context -> (),
|
||||||
)]
|
)]
|
||||||
value: impl Node<Context<'_>, Output = T>,
|
value: impl Node<Context<'_>, Output = T>,
|
||||||
@@ -114,15 +122,23 @@ fn quantize_animation_time<T>(
|
|||||||
Context -> DAffine2,
|
Context -> DAffine2,
|
||||||
Context -> Footprint,
|
Context -> Footprint,
|
||||||
Context -> DVec2,
|
Context -> DVec2,
|
||||||
|
Context -> Vector,
|
||||||
|
Context -> Graphic,
|
||||||
|
Context -> Raster<CPU>,
|
||||||
|
Context -> Raster<GPU>,
|
||||||
|
Context -> Color,
|
||||||
|
Context -> Gradient,
|
||||||
|
Context -> Artboard,
|
||||||
|
Context -> List<String>,
|
||||||
|
Context -> List<f64>,
|
||||||
|
Context -> List<DVec2>,
|
||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Graphic>,
|
Context -> List<Graphic>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
Context -> List<Artboard>,
|
|
||||||
Context -> List<Gradient>,
|
Context -> List<Gradient>,
|
||||||
Context -> List<String>,
|
Context -> List<Artboard>,
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
Context -> (),
|
||||||
)]
|
)]
|
||||||
value: impl Node<Context<'_>, Output = T>,
|
value: impl Node<Context<'_>, Output = T>,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ fn passthrough<T: Send>(_: impl Ctx, content: T) -> T {
|
|||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shifts a whole wire value onto a connector's type through the std `Into` trait, serving the whole-`List` erasure onto `ListDyn` under the input adapter identifier.
|
||||||
#[node_macro::node(category(""), skip_impl)]
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
fn into<T: Send + Into<O>, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData<O>) -> O {
|
fn into<T: Send + Into<O>, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData<O>) -> O {
|
||||||
value.into()
|
value.into()
|
||||||
|
|||||||
@@ -673,12 +673,16 @@ pub fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
|
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
|
||||||
#[node_macro::node(category("Color"))]
|
#[node_macro::node(category("Color"), name("Colors to Gradient"))]
|
||||||
fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
|
fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> Gradient {
|
||||||
|
let colors = colors.into_flattened_list::<Color>();
|
||||||
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
|
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
|
||||||
match colors.len() {
|
match colors.len() {
|
||||||
0 => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
|
0 => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
|
||||||
1 => Gradient::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]),
|
1 => Gradient::new(vec![
|
||||||
total => Gradient::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
|
stop(0., colors.element(0).copied().unwrap_or(Color::BLACK)),
|
||||||
|
stop(1., colors.element(0).copied().unwrap_or(Color::BLACK)),
|
||||||
|
]),
|
||||||
|
total => Gradient::new(colors.into_iter().enumerate().map(|(index, row)| stop(index as f64 / (total - 1) as f64, row.into_element()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ fn math<T: num_traits::float::Float>(
|
|||||||
#[implementations(f64, f32)]
|
#[implementations(f64, f32)]
|
||||||
operand_a: T,
|
operand_a: T,
|
||||||
/// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
|
/// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
|
||||||
#[default(A + B)]
|
#[default("A + B")]
|
||||||
expression: String,
|
expression: String,
|
||||||
/// The value of "B" when calculating the expression.
|
/// The value of "B" when calculating the expression.
|
||||||
#[implementations(f64, f32)]
|
#[implementations(f64, f32)]
|
||||||
@@ -517,10 +517,10 @@ fn absolute_value<T: AbsoluteValue>(
|
|||||||
fn min<T: std::cmp::PartialOrd>(
|
fn min<T: std::cmp::PartialOrd>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// One of the two numbers, of which the lesser is returned.
|
/// One of the two numbers, of which the lesser is returned.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
value: T,
|
value: T,
|
||||||
/// The other of the two numbers, of which the lesser is returned.
|
/// The other of the two numbers, of which the lesser is returned.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
other_value: T,
|
other_value: T,
|
||||||
) -> T {
|
) -> T {
|
||||||
if value < other_value { value } else { other_value }
|
if value < other_value { value } else { other_value }
|
||||||
@@ -531,10 +531,10 @@ fn min<T: std::cmp::PartialOrd>(
|
|||||||
fn max<T: std::cmp::PartialOrd>(
|
fn max<T: std::cmp::PartialOrd>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// One of the two numbers, of which the greater is returned.
|
/// One of the two numbers, of which the greater is returned.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
value: T,
|
value: T,
|
||||||
/// The other of the two numbers, of which the greater is returned.
|
/// The other of the two numbers, of which the greater is returned.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
other_value: T,
|
other_value: T,
|
||||||
) -> T {
|
) -> T {
|
||||||
if value > other_value { value } else { other_value }
|
if value > other_value { value } else { other_value }
|
||||||
@@ -545,13 +545,13 @@ fn max<T: std::cmp::PartialOrd>(
|
|||||||
fn clamp<T: std::cmp::PartialOrd>(
|
fn clamp<T: std::cmp::PartialOrd>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The number to be clamped, which is restricted to the range between the minimum and maximum values.
|
/// The number to be clamped, which is restricted to the range between the minimum and maximum values.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
value: T,
|
value: T,
|
||||||
/// The left (smaller) side of the range. The output is never less than this number.
|
/// The left (smaller) side of the range. The output is never less than this number.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
min: T,
|
min: T,
|
||||||
/// The right (greater) side of the range. The output is never greater than this number.
|
/// The right (greater) side of the range. The output is never greater than this number.
|
||||||
#[implementations(f64, f32, u32, &str)]
|
#[implementations(f64, f32, u32, String)]
|
||||||
#[default(1)]
|
#[default(1)]
|
||||||
max: T,
|
max: T,
|
||||||
) -> T {
|
) -> T {
|
||||||
@@ -678,10 +678,10 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
|
|||||||
fn equals<T: std::cmp::PartialEq<T>>(
|
fn equals<T: std::cmp::PartialEq<T>>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// One of the two values to compare for equality.
|
/// One of the two values to compare for equality.
|
||||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
#[implementations(f64, f32, u32, DVec2, bool, String)]
|
||||||
value: T,
|
value: T,
|
||||||
/// The other of the two values to compare for equality.
|
/// The other of the two values to compare for equality.
|
||||||
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
|
#[implementations(f64, f32, u32, DVec2, bool, String)]
|
||||||
other_value: T,
|
other_value: T,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
other_value == value
|
other_value == value
|
||||||
@@ -692,10 +692,10 @@ fn equals<T: std::cmp::PartialEq<T>>(
|
|||||||
fn not_equals<T: std::cmp::PartialEq<T>>(
|
fn not_equals<T: std::cmp::PartialEq<T>>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// One of the two values to compare for inequality.
|
/// One of the two values to compare for inequality.
|
||||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
#[implementations(f64, f32, u32, DVec2, bool, String)]
|
||||||
value: T,
|
value: T,
|
||||||
/// The other of the two values to compare for inequality.
|
/// The other of the two values to compare for inequality.
|
||||||
#[implementations(f64, f32, u32, DVec2, bool, &str)]
|
#[implementations(f64, f32, u32, DVec2, bool, String)]
|
||||||
other_value: T,
|
other_value: T,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
other_value != value
|
other_value != value
|
||||||
@@ -767,7 +767,7 @@ fn vec2_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
|
|||||||
DVec2::new(x, y)
|
DVec2::new(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a color value which may be set to any color, or no color.
|
/// Constructs a color value which may be set to any color.
|
||||||
#[node_macro::node(category("Value"))]
|
#[node_macro::node(category("Value"))]
|
||||||
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Color) -> Color {
|
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Color) -> Color {
|
||||||
color
|
color
|
||||||
|
|||||||
@@ -14,37 +14,44 @@ fn format_json(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The JSON string to reformat.
|
/// The JSON string to reformat.
|
||||||
#[name("JSON")]
|
#[name("JSON")]
|
||||||
json: String,
|
json: Item<String>,
|
||||||
/// Removes optional spaces within curly brackets and after colons and commas.
|
/// Removes optional spaces within curly brackets and after colons and commas.
|
||||||
compact: bool,
|
compact: Item<bool>,
|
||||||
/// Break arrays and objects across multiple lines when they exceed the line break length.
|
/// Break arrays and objects across multiple lines when they exceed the line break length.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
#[name("Multi-Line")]
|
#[name("Multi-Line")]
|
||||||
multi_line: bool,
|
multi_line: Item<bool>,
|
||||||
/// The indentation string used for each nesting level. Escape sequences like `\t` (the tab character) are supported. Two or four spaces are also common choices.
|
/// The indentation string used for each nesting level. Escape sequences like `\t` (the tab character) are supported. Two or four spaces are also common choices.
|
||||||
#[default("\\t")]
|
#[default("\\t")]
|
||||||
indent: String,
|
indent: Item<String>,
|
||||||
/// The maximum line length before a container (array or object) is broken across lines. Set this to 0 to always break containers. (Requires *Multi-Line* to take effect.)
|
/// The maximum line length before a container (array or object) is broken across lines. Set this to 0 to always break containers. (Requires *Multi-Line* to take effect.)
|
||||||
///
|
///
|
||||||
/// This is not a maximum line length guarantee. Deep nesting and long keys or values may exceed this length.
|
/// This is not a maximum line length guarantee. Deep nesting and long keys or values may exceed this length.
|
||||||
#[default(120)]
|
#[default(120)]
|
||||||
break_length: u32,
|
break_length: Item<u32>,
|
||||||
/// Always break a container (array or object) across lines if it holds another container, even if it would fit within the break length. (Requires *Multi-Line* to take effect.)
|
/// Always break a container (array or object) across lines if it holds another container, even if it would fit within the break length. (Requires *Multi-Line* to take effect.)
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
break_nested: bool,
|
break_nested: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
let cleaned = strip_trailing_commas(&json);
|
let mut json = json;
|
||||||
|
let (compact, multi_line, break_length, break_nested) = (*compact.element(), *multi_line.element(), *break_length.element(), *break_nested.element());
|
||||||
|
let indent = indent.element().clone();
|
||||||
|
|
||||||
|
let cleaned = strip_trailing_commas(json.element());
|
||||||
let Ok(value) = serde_json::from_str::<serde_json::Value>(&cleaned) else { return json };
|
let Ok(value) = serde_json::from_str::<serde_json::Value>(&cleaned) else { return json };
|
||||||
let indent = unescape_string(indent);
|
let indent = unescape_string(indent);
|
||||||
let colon = if compact { ":" } else { ": " };
|
let colon = if compact { ":" } else { ": " };
|
||||||
let comma_space = if compact { "," } else { ", " };
|
let comma_space = if compact { "," } else { ", " };
|
||||||
let line_width = break_length as usize;
|
let line_width = break_length as usize;
|
||||||
|
|
||||||
if multi_line {
|
let result = if multi_line {
|
||||||
format_value(&value, 0, &indent, colon, comma_space, compact, break_nested, line_width)
|
format_value(&value, 0, &indent, colon, comma_space, compact, break_nested, line_width)
|
||||||
} else {
|
} else {
|
||||||
format_inline(&value, colon, comma_space, compact)
|
format_inline(&value, colon, comma_space, compact)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
*json.element_mut() = result;
|
||||||
|
json
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Strips trailing commas before `]` and `}` to accept JSON-with-trailing-commas input.
|
/// Strips trailing commas before `]` and `}` to accept JSON-with-trailing-commas input.
|
||||||
@@ -188,7 +195,7 @@ fn query_json(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The JSON string to extract a value from.
|
/// The JSON string to extract a value from.
|
||||||
#[name("JSON")]
|
#[name("JSON")]
|
||||||
json: String,
|
json: Item<String>,
|
||||||
/// Determines which contained value to extract from within the JSON.
|
/// Determines which contained value to extract from within the JSON.
|
||||||
///
|
///
|
||||||
/// The path syntax is like JavaScript's accessor syntax that follows an array/object value. It also supports negative indexing to count backwards from the end. Additionally, `[]` accesses all array and object values instead of just one.
|
/// The path syntax is like JavaScript's accessor syntax that follows an array/object value. It also supports negative indexing to count backwards from the end. Additionally, `[]` accesses all array and object values instead of just one.
|
||||||
@@ -198,19 +205,29 @@ fn query_json(
|
|||||||
/// Use `.size` or `["size"]` to get the `size` property of `{ "size": 10 }`. The latter form is required if the key contains spaces or special characters like `["this key with spaces!"]`.
|
/// Use `.size` or `["size"]` to get the `size` property of `{ "size": 10 }`. The latter form is required if the key contains spaces or special characters like `["this key with spaces!"]`.
|
||||||
/// Use chained accessors like `.fonts[0].name` to query deeper.
|
/// Use chained accessors like `.fonts[0].name` to query deeper.
|
||||||
/// Use the `[]` accessor to query all elements, like `.fonts[].weights[]` to get every weight of every font.
|
/// Use the `[]` accessor to query all elements, like `.fonts[].weights[]` to get every weight of every font.
|
||||||
path: String,
|
path: Item<String>,
|
||||||
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
|
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
unquote_strings: bool,
|
unquote_strings: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
let cleaned = strip_trailing_commas(&json);
|
let mut json = json;
|
||||||
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return String::new() };
|
let path = path.element().clone();
|
||||||
let Some(segments) = parse_json_path(path.trim()) else { return String::new() };
|
let unquote_strings = *unquote_strings.element();
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let cleaned = strip_trailing_commas(json.element());
|
||||||
resolve_all(&value, &segments, !unquote_strings, &mut results);
|
|
||||||
|
|
||||||
results.into_iter().next().map(|(text, _ty)| text).unwrap_or_default()
|
let result = match (serde_json::from_str::<Value>(&cleaned), parse_json_path(path.trim())) {
|
||||||
|
(Ok(value), Some(segments)) => {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
resolve_all(&value, &segments, !unquote_strings, &mut results);
|
||||||
|
|
||||||
|
results.into_iter().next().map(|(text, _ty)| text).unwrap_or_default()
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
*json.element_mut() = result;
|
||||||
|
json
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts every matched value from a JSON string using a path expression (see that parameter's description for its syntax). A list of zero or more resultant strings is produced. The `[]` path accessor is used to read more than one value.
|
/// Extracts every matched value from a JSON string using a path expression (see that parameter's description for its syntax). A list of zero or more resultant strings is produced. The `[]` path accessor is used to read more than one value.
|
||||||
@@ -226,7 +243,7 @@ fn query_json_all(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The JSON string to extract values from.
|
/// The JSON string to extract values from.
|
||||||
#[name("JSON")]
|
#[name("JSON")]
|
||||||
json: String,
|
json: Item<String>,
|
||||||
/// Determines which contained values to extract from within the JSON.
|
/// Determines which contained values to extract from within the JSON.
|
||||||
///
|
///
|
||||||
/// The path syntax is like JavaScript's accessor syntax that follows an array/object value. It also supports negative indexing to count backwards from the end. Additionally, `[]` accesses all array and object values instead of just one.
|
/// The path syntax is like JavaScript's accessor syntax that follows an array/object value. It also supports negative indexing to count backwards from the end. Additionally, `[]` accesses all array and object values instead of just one.
|
||||||
@@ -236,17 +253,17 @@ fn query_json_all(
|
|||||||
/// Use `.size` or `["size"]` to get the `size` property of `{ "size": 10 }`. The latter form is required if the key contains spaces or special characters like `["this key with spaces!"]`.
|
/// Use `.size` or `["size"]` to get the `size` property of `{ "size": 10 }`. The latter form is required if the key contains spaces or special characters like `["this key with spaces!"]`.
|
||||||
/// Use chained accessors like `.fonts[0].name` to query deeper.
|
/// Use chained accessors like `.fonts[0].name` to query deeper.
|
||||||
/// Use the `[]` accessor to query all elements, like `.fonts[].weights[]` to get every weight of every font.
|
/// Use the `[]` accessor to query all elements, like `.fonts[].weights[]` to get every weight of every font.
|
||||||
path: String,
|
path: Item<String>,
|
||||||
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
|
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
unquote_strings: bool,
|
unquote_strings: Item<bool>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
let cleaned = strip_trailing_commas(&json);
|
let cleaned = strip_trailing_commas(json.element());
|
||||||
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return List::new() };
|
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return List::new() };
|
||||||
let Some(segments) = parse_json_path(path.trim()) else { return List::new() };
|
let Some(segments) = parse_json_path(path.element().trim()) else { return List::new() };
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
resolve_all(&value, &segments, !unquote_strings, &mut results);
|
resolve_all(&value, &segments, !*unquote_strings.element(), &mut results);
|
||||||
|
|
||||||
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect()
|
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
+236
-153
@@ -187,34 +187,43 @@ pub enum StringCapitalization {
|
|||||||
|
|
||||||
/// Constructs a string value which may be set to any plain text.
|
/// Constructs a string value which may be set to any plain text.
|
||||||
#[node_macro::node(category("Value"))]
|
#[node_macro::node(category("Value"))]
|
||||||
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
|
fn string_value(_: impl Ctx, _primary: (), string: Item<TextArea>) -> Item<String> {
|
||||||
string
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type-asserts a value to be a string.
|
/// Type-asserts a value to be a string.
|
||||||
#[node_macro::node(category("Debug"))]
|
#[node_macro::node(category("Debug"))]
|
||||||
fn as_string(_: impl Ctx, value: String) -> String {
|
fn as_string(_: impl Ctx, value: Item<String>) -> Item<String> {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Joins two strings together.
|
/// Joins two strings together.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
|
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: Item<String>, second: Item<TextArea>) -> Item<String> {
|
||||||
first + &second
|
let mut first = first;
|
||||||
|
first.element_mut().push_str(second.element());
|
||||||
|
first
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces all occurrences of "From" with "To" in the input string.
|
/// Replaces all occurrences of "From" with "To" in the input string.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_replace(_: impl Ctx, string: String, from: TextArea, to: TextArea) -> String {
|
fn string_replace(_: impl Ctx, string: Item<String>, from: Item<TextArea>, to: Item<TextArea>) -> Item<String> {
|
||||||
string.replace(&from, &to)
|
let mut string = string;
|
||||||
|
let result = string.element().replace(from.element().as_str(), to.element());
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts a substring from the input string, starting at "Start" and ending before "End".
|
/// Extracts a substring from the input string, starting at "Start" and ending before "End".
|
||||||
///
|
///
|
||||||
/// Negative indices count from the end of the string. If the index of "Start" equals or exceeds "End", the result is an empty string.
|
/// Negative indices count from the end of the string. If the index of "Start" equals or exceeds "End", the result is an empty string.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_slice(_: impl Ctx, string: String, start: SignedInteger, end: SignedInteger) -> String {
|
fn string_slice(_: impl Ctx, string: Item<String>, start: Item<SignedInteger>, end: Item<SignedInteger>) -> Item<String> {
|
||||||
let total_graphemes = string.graphemes(true).count();
|
let mut string = string;
|
||||||
|
let (start, end) = (*start.element(), *end.element());
|
||||||
|
|
||||||
|
let total_graphemes = string.element().graphemes(true).count();
|
||||||
|
|
||||||
let start = if start < 0. {
|
let start = if start < 0. {
|
||||||
total_graphemes.saturating_sub(start.abs() as usize)
|
total_graphemes.saturating_sub(start.abs() as usize)
|
||||||
@@ -227,11 +236,14 @@ fn string_slice(_: impl Ctx, string: String, start: SignedInteger, end: SignedIn
|
|||||||
(end as usize).min(total_graphemes)
|
(end as usize).min(total_graphemes)
|
||||||
};
|
};
|
||||||
|
|
||||||
if start >= end {
|
let result = if start >= end {
|
||||||
return String::new();
|
String::new()
|
||||||
}
|
} else {
|
||||||
|
string.element().graphemes(true).skip(start).take(end - start).collect()
|
||||||
|
};
|
||||||
|
|
||||||
string.graphemes(true).skip(start).take(end - start).collect()
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clips the string to a maximum character length, optionally appending a suffix (like "…") when truncation occurs. Strings already within the limit are not modified.
|
/// Clips the string to a maximum character length, optionally appending a suffix (like "…") when truncation occurs. Strings already within the limit are not modified.
|
||||||
@@ -239,27 +251,30 @@ fn string_slice(_: impl Ctx, string: String, start: SignedInteger, end: SignedIn
|
|||||||
fn string_truncate(
|
fn string_truncate(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to truncate.
|
/// The string to truncate.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The maximum number of characters allowed, including the suffix if one is appended.
|
/// The maximum number of characters allowed, including the suffix if one is appended.
|
||||||
#[default(80)]
|
#[default(80)]
|
||||||
length: u32,
|
length: Item<u32>,
|
||||||
/// A suffix appended to indicate truncation occurred, unless empty. Its length counts towards the character budget.
|
/// A suffix appended to indicate truncation occurred, unless empty. Its length counts towards the character budget.
|
||||||
#[default("…")]
|
#[default("…")]
|
||||||
suffix: String,
|
suffix: Item<String>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
let max_length = length as usize;
|
let mut string = string;
|
||||||
let grapheme_count = string.graphemes(true).count();
|
let max_length = *length.element() as usize;
|
||||||
|
let grapheme_count = string.element().graphemes(true).count();
|
||||||
|
|
||||||
if grapheme_count <= max_length {
|
if grapheme_count <= max_length {
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let suffix: String = suffix.graphemes(true).take(max_length).collect();
|
let suffix: String = suffix.element().graphemes(true).take(max_length).collect();
|
||||||
let keep = max_length - suffix.graphemes(true).count();
|
let keep = max_length - suffix.graphemes(true).count();
|
||||||
|
|
||||||
let mut truncated: String = string.graphemes(true).take(keep).collect();
|
let mut truncated: String = string.element().graphemes(true).take(keep).collect();
|
||||||
truncated.push_str(&suffix);
|
truncated.push_str(&suffix);
|
||||||
truncated
|
|
||||||
|
*string.element_mut() = truncated;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a number as a string with control over decimal places, decimal separator, and thousands grouping.
|
/// Formats a number as a string with control over decimal places, decimal separator, and thousands grouping.
|
||||||
@@ -267,25 +282,31 @@ fn string_truncate(
|
|||||||
fn format_number(
|
fn format_number(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The number to format as a string.
|
/// The number to format as a string.
|
||||||
number: f64,
|
number: Item<f64>,
|
||||||
/// The amount of digits after the decimal point. The value is rounded to fit. Set to 0 to show only whole numbers.
|
/// The amount of digits after the decimal point. The value is rounded to fit. Set to 0 to show only whole numbers.
|
||||||
#[default(2)]
|
#[default(2)]
|
||||||
decimal_places: u32,
|
decimal_places: Item<u32>,
|
||||||
/// The character(s) used as the decimal point.
|
/// The character(s) used as the decimal point.
|
||||||
#[default(".")]
|
#[default(".")]
|
||||||
decimal_separator: String,
|
decimal_separator: Item<String>,
|
||||||
/// Always show the exact number of decimal places, even if they are trailing zeros.
|
/// Always show the exact number of decimal places, even if they are trailing zeros.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
fixed_decimals: bool,
|
fixed_decimals: Item<bool>,
|
||||||
/// Whether to group digits with a thousands separator.
|
/// Whether to group digits with a thousands separator.
|
||||||
use_thousands_separator: bool,
|
use_thousands_separator: Item<bool>,
|
||||||
/// The character(s) inserted between digit groups.
|
/// The character(s) inserted between digit groups.
|
||||||
#[default(",")]
|
#[default(",")]
|
||||||
thousands_separator: String,
|
thousands_separator: Item<String>,
|
||||||
/// Don't group 4-digit numbers with a thousands separator (only start grouping at 10,000 and above).
|
/// Don't group 4-digit numbers with a thousands separator (only start grouping at 10,000 and above).
|
||||||
#[name("Start at 10,000")]
|
#[name("Start at 10,000")]
|
||||||
start_at_10000: bool,
|
start_at_10000: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let (number, attributes) = number.into_parts();
|
||||||
|
let (decimal_places, fixed_decimals, use_thousands_separator, start_at_10000) =
|
||||||
|
(*decimal_places.element(), *fixed_decimals.element(), *use_thousands_separator.element(), *start_at_10000.element());
|
||||||
|
let decimal_separator = decimal_separator.element().clone();
|
||||||
|
let thousands_separator = thousands_separator.element().clone();
|
||||||
|
|
||||||
// Find the maximum meaningful decimal precision by detecting where float noise begins.
|
// Find the maximum meaningful decimal precision by detecting where float noise begins.
|
||||||
// This works correctly whether the value originated as f32 or f64, since we find the
|
// This works correctly whether the value originated as f32 or f64, since we find the
|
||||||
// shortest decimal representation that round-trips back to the same f64 value.
|
// shortest decimal representation that round-trips back to the same f64 value.
|
||||||
@@ -340,36 +361,38 @@ fn format_number(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build the final string
|
// Build the final string
|
||||||
let Some(decimal_string) = decimal_string else {
|
let result = match decimal_string {
|
||||||
if fixed_decimals && requested_places > 0 {
|
None if fixed_decimals && requested_places > 0 => {
|
||||||
let zeros = "0".repeat(requested_places);
|
let zeros = "0".repeat(requested_places);
|
||||||
return format!("{sign}{grouped_whole}{decimal_separator}{zeros}");
|
format!("{sign}{grouped_whole}{decimal_separator}{zeros}")
|
||||||
|
}
|
||||||
|
None => format!("{sign}{grouped_whole}"),
|
||||||
|
Some(decimal_string) if fixed_decimals => format!("{sign}{grouped_whole}{decimal_separator}{decimal_string}"),
|
||||||
|
Some(decimal_string) => {
|
||||||
|
let trimmed = decimal_string.trim_end_matches('0');
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
format!("{sign}{grouped_whole}")
|
||||||
|
} else {
|
||||||
|
format!("{sign}{grouped_whole}{decimal_separator}{trimmed}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return format!("{sign}{grouped_whole}");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if fixed_decimals {
|
Item::from_parts(result, attributes)
|
||||||
format!("{sign}{grouped_whole}{decimal_separator}{decimal_string}")
|
|
||||||
} else {
|
|
||||||
let trimmed = decimal_string.trim_end_matches('0');
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
format!("{sign}{grouped_whole}")
|
|
||||||
} else {
|
|
||||||
format!("{sign}{grouped_whole}{decimal_separator}{trimmed}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a string into a number. Falls back to the chosen value if the string is not a valid number.
|
/// Parses a string into a number. Falls back to the chosen value if the string is not a valid number.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"), name("String to Number"))]
|
||||||
fn string_to_number(
|
fn string_to_number(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string containing a number. Surrounding whitespace is ignored, a decimal point (.) may be included, sign prefixes (+/-) are respected, and scientific notation (e.g. "1e-3") is supported.
|
/// The string containing a number. Surrounding whitespace is ignored, a decimal point (.) may be included, sign prefixes (+/-) are respected, and scientific notation (e.g. "1e-3") is supported.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The value of the result if the string cannot be parsed as a valid number.
|
/// The value of the result if the string cannot be parsed as a valid number.
|
||||||
fallback: f64,
|
fallback: Item<f64>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
string.trim().parse::<f64>().unwrap_or(fallback)
|
let (string, attributes) = string.into_parts();
|
||||||
|
|
||||||
|
Item::from_parts(string.trim().parse::<f64>().unwrap_or(*fallback.element()), attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes leading and/or trailing whitespace from a string. Common whitespace characters include spaces, tabs, and newlines.
|
/// Removes leading and/or trailing whitespace from a string. Common whitespace characters include spaces, tabs, and newlines.
|
||||||
@@ -377,20 +400,26 @@ fn string_to_number(
|
|||||||
fn string_trim(
|
fn string_trim(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string that may contain leading and trailing whitespace that should be removed.
|
/// The string that may contain leading and trailing whitespace that should be removed.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// Whether the start of the string should have its whitespace removed.
|
/// Whether the start of the string should have its whitespace removed.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
start: bool,
|
start: Item<bool>,
|
||||||
/// Whether the end of the string should have its whitespace removed.
|
/// Whether the end of the string should have its whitespace removed.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
end: bool,
|
end: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
match (start, end) {
|
let mut string = string;
|
||||||
(true, true) => string.trim().to_string(),
|
let (start, end) = (*start.element(), *end.element());
|
||||||
(true, false) => string.trim_start().to_string(),
|
|
||||||
(false, true) => string.trim_end().to_string(),
|
let result = match (start, end) {
|
||||||
(false, false) => string,
|
(true, true) => string.element().trim().to_string(),
|
||||||
}
|
(true, false) => string.element().trim_start().to_string(),
|
||||||
|
(false, true) => string.element().trim_end().to_string(),
|
||||||
|
(false, false) => return string,
|
||||||
|
};
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts between literal escape sequences and their corresponding control characters within a string.
|
/// Converts between literal escape sequences and their corresponding control characters within a string.
|
||||||
@@ -401,12 +430,18 @@ fn string_trim(
|
|||||||
fn string_escape(
|
fn string_escape(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string that contains either literal escape sequences or control characters to be converted to the opposite representation.
|
/// The string that contains either literal escape sequences or control characters to be converted to the opposite representation.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// Convert the control characters back into their escape sequence representations.
|
/// Convert the control characters back into their escape sequence representations.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
unescape: bool,
|
unescape: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
if unescape { unescape_string(string) } else { escape_string(string) }
|
let mut string = string;
|
||||||
|
let input = std::mem::take(string.element_mut());
|
||||||
|
|
||||||
|
let result = if *unescape.element() { unescape_string(input) } else { escape_string(input) };
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverses the sequence of characters making up the string so it reads back-to-front. ("Backwards text" becomes "txet sdrawkcaB".)
|
/// Reverses the sequence of characters making up the string so it reads back-to-front. ("Backwards text" becomes "txet sdrawkcaB".)
|
||||||
@@ -414,9 +449,13 @@ fn string_escape(
|
|||||||
fn string_reverse(
|
fn string_reverse(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to be reversed.
|
/// The string to be reversed.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
string.graphemes(true).rev().collect()
|
let mut string = string;
|
||||||
|
let result: String = string.element().graphemes(true).rev().collect();
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Repeats the string a given number of times, optionally with a separator between each repetition.
|
/// Repeats the string a given number of times, optionally with a separator between each repetition.
|
||||||
@@ -424,31 +463,35 @@ fn string_reverse(
|
|||||||
fn string_repeat(
|
fn string_repeat(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to be repeated.
|
/// The string to be repeated.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The number of times the string should appear in the output.
|
/// The number of times the string should appear in the output.
|
||||||
#[default(2)]
|
#[default(2)]
|
||||||
#[hard(1..)]
|
#[hard(1..)]
|
||||||
count: u32,
|
count: Item<u32>,
|
||||||
/// The string placed between each repetition.
|
/// The string placed between each repetition.
|
||||||
#[default("\\n")]
|
#[default("\\n")]
|
||||||
separator: String,
|
separator: Item<String>,
|
||||||
/// Whether to convert escape sequences found in the separator into their corresponding characters:
|
/// Whether to convert escape sequences found in the separator into their corresponding characters:
|
||||||
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
separator_escaping: bool,
|
separator_escaping: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
let separator = if separator_escaping { unescape_string(separator) } else { separator };
|
let mut string = string;
|
||||||
|
let separator = separator.element().clone();
|
||||||
|
let separator = if *separator_escaping.element() { unescape_string(separator) } else { separator };
|
||||||
|
|
||||||
let count = count as usize;
|
let count = *count.element() as usize;
|
||||||
|
|
||||||
let mut result = String::with_capacity((string.len() + separator.len()) * count);
|
let mut result = String::with_capacity((string.element().len() + separator.len()) * count);
|
||||||
for i in 0..count {
|
for i in 0..count {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
result.push_str(&separator);
|
result.push_str(&separator);
|
||||||
}
|
}
|
||||||
result.push_str(&string);
|
result.push_str(string.element());
|
||||||
}
|
}
|
||||||
result
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pads the string to a target length by filling with the given repeated substring. If the string already meets or exceeds the target length, it is returned unchanged.
|
/// Pads the string to a target length by filling with the given repeated substring. If the string already meets or exceeds the target length, it is returned unchanged.
|
||||||
@@ -456,21 +499,25 @@ fn string_repeat(
|
|||||||
fn string_pad(
|
fn string_pad(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to be padded to a target length.
|
/// The string to be padded to a target length.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The target character length after padding. When "Up To" is set, this length concerns only the portion before (or after) that substring.
|
/// The target character length after padding. When "Up To" is set, this length concerns only the portion before (or after) that substring.
|
||||||
#[default(10)]
|
#[default(10)]
|
||||||
length: u32,
|
length: Item<u32>,
|
||||||
/// The repeated substring used to fill the remaining space. A multi-charcter substring may end partway through its final repetition.
|
/// The repeated substring used to fill the remaining space. A multi-charcter substring may end partway through its final repetition.
|
||||||
#[default("#")]
|
#[default("#")]
|
||||||
padding: String,
|
padding: Item<String>,
|
||||||
/// Pad only the length of the string encountered before the start of the first (or after the end of the last) occurrence of this substring, if given and present (otherwise the full string is considered).
|
/// Pad only the length of the string encountered before the start of the first (or after the end of the last) occurrence of this substring, if given and present (otherwise the full string is considered).
|
||||||
///
|
///
|
||||||
/// For example, this can pad numbers with leading zeros to align them before the decimal point.
|
/// For example, this can pad numbers with leading zeros to align them before the decimal point.
|
||||||
up_to: String,
|
up_to: Item<String>,
|
||||||
/// Pad at the end of the string instead of the start.
|
/// Pad at the end of the string instead of the start.
|
||||||
from_end: bool,
|
from_end: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
let target_length = length as usize;
|
let mut string = string;
|
||||||
|
let target_length = *length.element() as usize;
|
||||||
|
let padding = padding.element().clone();
|
||||||
|
let up_to = up_to.element().clone();
|
||||||
|
let from_end = *from_end.element();
|
||||||
|
|
||||||
if padding.is_empty() {
|
if padding.is_empty() {
|
||||||
return string;
|
return string;
|
||||||
@@ -478,9 +525,9 @@ fn string_pad(
|
|||||||
|
|
||||||
// Split the string at the "up to" substring if provided, and only pad that portion
|
// Split the string at the "up to" substring if provided, and only pad that portion
|
||||||
if !up_to.is_empty()
|
if !up_to.is_empty()
|
||||||
&& let Some(position) = if from_end { string.rfind(&*up_to) } else { string.find(&*up_to) }
|
&& let Some(position) = if from_end { string.element().rfind(&*up_to) } else { string.element().find(&*up_to) }
|
||||||
{
|
{
|
||||||
let (before, after) = string.split_at(position);
|
let (before, after) = string.element().split_at(position);
|
||||||
|
|
||||||
if from_end {
|
if from_end {
|
||||||
// Pad the portion after the substring
|
// Pad the portion after the substring
|
||||||
@@ -491,7 +538,10 @@ fn string_pad(
|
|||||||
}
|
}
|
||||||
let pad_length = target_length - current_length;
|
let pad_length = target_length - current_length;
|
||||||
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
||||||
return format!("{before}{up_to}{after_substring}{padding}");
|
let result = format!("{before}{up_to}{after_substring}{padding}");
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
return string;
|
||||||
} else {
|
} else {
|
||||||
// Pad the portion before the substring
|
// Pad the portion before the substring
|
||||||
let current_length = before.graphemes(true).count();
|
let current_length = before.graphemes(true).count();
|
||||||
@@ -500,11 +550,14 @@ fn string_pad(
|
|||||||
}
|
}
|
||||||
let pad_length = target_length - current_length;
|
let pad_length = target_length - current_length;
|
||||||
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
||||||
return format!("{padding}{before}{after}");
|
let result = format!("{padding}{before}{after}");
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
return string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_length = string.graphemes(true).count();
|
let current_length = string.element().graphemes(true).count();
|
||||||
if current_length >= target_length {
|
if current_length >= target_length {
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
@@ -512,7 +565,10 @@ fn string_pad(
|
|||||||
let pad_length = target_length - current_length;
|
let pad_length = target_length - current_length;
|
||||||
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
let padding: String = padding.graphemes(true).cycle().take(pad_length).collect();
|
||||||
|
|
||||||
if from_end { string + &padding } else { padding + &string }
|
let result = if from_end { string.element().clone() + &padding } else { padding + string.element() };
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the string contains the given substring. Optionally restricts the match to only the start and/or end of the string.
|
/// Checks whether the string contains the given substring. Optionally restricts the match to only the start and/or end of the string.
|
||||||
@@ -520,20 +576,26 @@ fn string_pad(
|
|||||||
fn string_contains(
|
fn string_contains(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The substring to search for.
|
/// The substring to search for.
|
||||||
substring: String,
|
substring: Item<String>,
|
||||||
/// Only match if the substring appears at the start of the string.
|
/// Only match if the substring appears at the start of the string.
|
||||||
at_start: bool,
|
at_start: Item<bool>,
|
||||||
/// Only match if the substring appears at the end of the string.
|
/// Only match if the substring appears at the end of the string.
|
||||||
at_end: bool,
|
at_end: Item<bool>,
|
||||||
) -> bool {
|
) -> Item<bool> {
|
||||||
match (at_start, at_end) {
|
let (string, attributes) = string.into_parts();
|
||||||
(true, true) => string.starts_with(&*substring) && string.ends_with(&*substring),
|
let substring = substring.element().as_str();
|
||||||
(true, false) => string.starts_with(&*substring),
|
let (at_start, at_end) = (*at_start.element(), *at_end.element());
|
||||||
(false, true) => string.ends_with(&*substring),
|
|
||||||
(false, false) => string.contains(&*substring),
|
let result = match (at_start, at_end) {
|
||||||
}
|
(true, true) => string.starts_with(substring) && string.ends_with(substring),
|
||||||
|
(true, false) => string.starts_with(substring),
|
||||||
|
(false, true) => string.ends_with(substring),
|
||||||
|
(false, false) => string.contains(substring),
|
||||||
|
};
|
||||||
|
|
||||||
|
Item::from_parts(result, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to the **String Contains** node, this searches within the input string for the first (or last) occurrence of a substring and returns the index of where that begins, or -1 if not found.
|
/// Similar to the **String Contains** node, this searches within the input string for the first (or last) occurrence of a substring and returns the index of where that begins, or -1 if not found.
|
||||||
@@ -541,28 +603,35 @@ fn string_contains(
|
|||||||
fn string_find_index(
|
fn string_find_index(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The substring to search for.
|
/// The substring to search for.
|
||||||
substring: String,
|
substring: Item<String>,
|
||||||
/// Find the start index of the last occurrence instead of the first.
|
/// Find the start index of the last occurrence instead of the first.
|
||||||
from_end: bool,
|
from_end: Item<bool>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
|
let (string, attributes) = string.into_parts();
|
||||||
|
let substring = substring.element().as_str();
|
||||||
|
let from_end = *from_end.element();
|
||||||
|
|
||||||
if substring.is_empty() {
|
if substring.is_empty() {
|
||||||
return if from_end { string.graphemes(true).count() as f64 } else { 0. };
|
let result = if from_end { string.graphemes(true).count() as f64 } else { 0. };
|
||||||
|
return Item::from_parts(result, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if from_end {
|
let result = if from_end {
|
||||||
// Search backwards by finding all byte-level matches and taking the last one
|
// Search backwards by finding all byte-level matches and taking the last one
|
||||||
string
|
string
|
||||||
.rmatch_indices(&*substring)
|
.rmatch_indices(substring)
|
||||||
.next()
|
.next()
|
||||||
.map_or(-1., |(byte_index, _)| string[..byte_index].graphemes(true).count() as f64)
|
.map_or(-1., |(byte_index, _)| string[..byte_index].graphemes(true).count() as f64)
|
||||||
} else {
|
} else {
|
||||||
string
|
string
|
||||||
.match_indices(&*substring)
|
.match_indices(substring)
|
||||||
.next()
|
.next()
|
||||||
.map_or(-1., |(byte_index, _)| string[..byte_index].graphemes(true).count() as f64)
|
.map_or(-1., |(byte_index, _)| string[..byte_index].graphemes(true).count() as f64)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Item::from_parts(result, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Counts the number of occurrences of a substring within the string.
|
/// Counts the number of occurrences of a substring within the string.
|
||||||
@@ -570,22 +639,25 @@ fn string_find_index(
|
|||||||
fn string_occurrences(
|
fn string_occurrences(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The substring to count occurrences of.
|
/// The substring to count occurrences of.
|
||||||
substring: String,
|
substring: Item<String>,
|
||||||
/// Whether to count overlapping occurrences, using the substring as a sliding window.
|
/// Whether to count overlapping occurrences, using the substring as a sliding window.
|
||||||
///
|
///
|
||||||
/// For example, "aa" occurs twice in "aaaa" without overlapping but three times with overlapping.
|
/// For example, "aa" occurs twice in "aaaa" without overlapping but three times with overlapping.
|
||||||
overlapping: bool,
|
overlapping: Item<bool>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
|
let (string, attributes) = string.into_parts();
|
||||||
|
let substring = substring.element().as_str();
|
||||||
|
|
||||||
if substring.is_empty() {
|
if substring.is_empty() {
|
||||||
return 0.;
|
return Item::from_parts(0., attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NON-OVERLAPPING: Simple linear scan.
|
// NON-OVERLAPPING: Simple linear scan.
|
||||||
// O(n), where n = string length
|
// O(n), where n = string length
|
||||||
if !overlapping {
|
if !*overlapping.element() {
|
||||||
return string.matches(&*substring).count() as f64;
|
return Item::from_parts(string.matches(substring).count() as f64, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OVERLAPPING: KMP (Knuth-Morris-Pratt) algorithm.
|
// OVERLAPPING: KMP (Knuth-Morris-Pratt) algorithm.
|
||||||
@@ -631,7 +703,7 @@ fn string_occurrences(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
count as f64
|
Item::from_parts(count as f64, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a string's capitalization style to another of the common upper and lower case patterns, optionally joining words with a chosen separator.
|
/// Converts a string's capitalization style to another of the common upper and lower case patterns, optionally joining words with a chosen separator.
|
||||||
@@ -639,47 +711,49 @@ fn string_occurrences(
|
|||||||
fn string_capitalization(
|
fn string_capitalization(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to have its letter capitalization converted.
|
/// The string to have its letter capitalization converted.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The capitalization style to apply.
|
/// The capitalization style to apply.
|
||||||
capitalization: StringCapitalization,
|
capitalization: Item<StringCapitalization>,
|
||||||
/// Whether to split the string into words and reconnect with the chosen joiner. When disabled, the existing word structure separators are preserved.
|
/// Whether to split the string into words and reconnect with the chosen joiner. When disabled, the existing word structure separators are preserved.
|
||||||
use_joiner: bool,
|
use_joiner: Item<bool>,
|
||||||
/// The string placed between each word.
|
/// The string placed between each word.
|
||||||
joiner: String,
|
joiner: Item<String>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let mut string = string;
|
||||||
|
let capitalization = *capitalization.element();
|
||||||
|
let use_joiner = *use_joiner.element();
|
||||||
|
let joiner = joiner.element().clone();
|
||||||
|
let input = std::mem::take(string.element_mut());
|
||||||
|
|
||||||
// When the joiner is enabled, apply word-level casing and optionally reconnect words with the selected joiner
|
// When the joiner is enabled, apply word-level casing and optionally reconnect words with the selected joiner
|
||||||
if use_joiner {
|
let result = if use_joiner {
|
||||||
match capitalization {
|
match capitalization {
|
||||||
// Simple case mappings that preserve the string's existing structure
|
// Simple case mappings that preserve the string's existing structure
|
||||||
StringCapitalization::LowerCase => string.to_lowercase(),
|
StringCapitalization::LowerCase => input.to_lowercase(),
|
||||||
StringCapitalization::UpperCase => string.to_uppercase(),
|
StringCapitalization::UpperCase => input.to_uppercase(),
|
||||||
|
|
||||||
// Word-aware capitalizations that split on word boundaries and rejoin with the joiner
|
// Word-aware capitalizations that split on word boundaries and rejoin with the joiner
|
||||||
StringCapitalization::CapitalCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(&joiner).convert(&string),
|
StringCapitalization::CapitalCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(&joiner).convert(&input),
|
||||||
StringCapitalization::HeadlineCase => {
|
StringCapitalization::HeadlineCase => {
|
||||||
// First split into words with convert_case so word boundaries like "AlphaNumeric" are detected consistently with other modes,
|
// First split into words with convert_case so word boundaries like "AlphaNumeric" are detected consistently with other modes,
|
||||||
// then apply the titlecase crate for smart capitalization (lowercasing short words like "of", "the", etc.),
|
// then apply the titlecase crate for smart capitalization (lowercasing short words like "of", "the", etc.),
|
||||||
// then rejoin with the custom joiner without mangling the capitalization
|
// then rejoin with the custom joiner without mangling the capitalization
|
||||||
let spaced = Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(" ").convert(&string);
|
let spaced = Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(" ").convert(&input);
|
||||||
let headline = titlecase::titlecase(&spaced);
|
let headline = titlecase::titlecase(&spaced);
|
||||||
Converter::new().set_boundaries(&[Boundary::SPACE]).set_pattern(pattern::noop).set_delim(&joiner).convert(&headline)
|
Converter::new().set_boundaries(&[Boundary::SPACE]).set_pattern(pattern::noop).set_delim(&joiner).convert(&headline)
|
||||||
}
|
}
|
||||||
StringCapitalization::SentenceCase => Converter::new()
|
StringCapitalization::SentenceCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::sentence).set_delim(&joiner).convert(&input),
|
||||||
.set_boundaries(&Boundary::defaults())
|
StringCapitalization::CamelCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::camel).set_delim(&joiner).convert(&input),
|
||||||
.set_pattern(pattern::sentence)
|
|
||||||
.set_delim(&joiner)
|
|
||||||
.convert(&string),
|
|
||||||
StringCapitalization::CamelCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::camel).set_delim(&joiner).convert(&string),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// When the joiner is disabled, apply only character-level casing while preserving the string's existing structure
|
// When the joiner is disabled, apply only character-level casing while preserving the string's existing structure
|
||||||
else {
|
else {
|
||||||
match capitalization {
|
match capitalization {
|
||||||
StringCapitalization::LowerCase => string.to_lowercase(),
|
StringCapitalization::LowerCase => input.to_lowercase(),
|
||||||
StringCapitalization::UpperCase => string.to_uppercase(),
|
StringCapitalization::UpperCase => input.to_uppercase(),
|
||||||
StringCapitalization::CapitalCase => {
|
StringCapitalization::CapitalCase => {
|
||||||
let mut capitalize_next = true;
|
let mut capitalize_next = true;
|
||||||
string.chars().fold(String::with_capacity(string.len()), |mut result, c| {
|
input.chars().fold(String::with_capacity(input.len()), |mut result, c| {
|
||||||
if c.is_whitespace() || c == '_' || c == '-' {
|
if c.is_whitespace() || c == '_' || c == '-' {
|
||||||
capitalize_next = true;
|
capitalize_next = true;
|
||||||
result.push(c);
|
result.push(c);
|
||||||
@@ -692,9 +766,9 @@ fn string_capitalization(
|
|||||||
result
|
result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
StringCapitalization::HeadlineCase => titlecase::titlecase(&string),
|
StringCapitalization::HeadlineCase => titlecase::titlecase(&input),
|
||||||
StringCapitalization::SentenceCase => {
|
StringCapitalization::SentenceCase => {
|
||||||
let mut chars = string.chars();
|
let mut chars = input.chars();
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
Some(first) => first.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
|
Some(first) => first.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
@@ -702,7 +776,7 @@ fn string_capitalization(
|
|||||||
}
|
}
|
||||||
StringCapitalization::CamelCase => {
|
StringCapitalization::CamelCase => {
|
||||||
let mut capitalize_next = false;
|
let mut capitalize_next = false;
|
||||||
string.chars().fold(String::with_capacity(string.len()), |mut result, c| {
|
input.chars().fold(String::with_capacity(input.len()), |mut result, c| {
|
||||||
if c.is_whitespace() || c == '_' || c == '-' {
|
if c.is_whitespace() || c == '_' || c == '-' {
|
||||||
capitalize_next = true;
|
capitalize_next = true;
|
||||||
result.push(c);
|
result.push(c);
|
||||||
@@ -716,15 +790,20 @@ fn string_capitalization(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
||||||
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
|
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
|
||||||
/// Counts the number of characters in a string.
|
/// Counts the number of characters in a string.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_length(_: impl Ctx, string: String) -> f64 {
|
fn string_length(_: impl Ctx, string: Item<String>) -> Item<f64> {
|
||||||
string.graphemes(true).count() as f64
|
let (string, attributes) = string.into_parts();
|
||||||
|
|
||||||
|
Item::from_parts(string.graphemes(true).count() as f64, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a string into a list of substrings based on the specified delimiter. This is the inverse of the **String Join** node.
|
/// Splits a string into a list of substrings based on the specified delimiter. This is the inverse of the **String Join** node.
|
||||||
@@ -734,18 +813,19 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
|
|||||||
fn string_split(
|
fn string_split(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to split into substrings.
|
/// The string to split into substrings.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The character(s) that separate the substrings. These are not included in the outputs.
|
/// The character(s) that separate the substrings. These are not included in the outputs.
|
||||||
#[default("\\n")]
|
#[default("\\n")]
|
||||||
delimiter: String,
|
delimiter: Item<String>,
|
||||||
/// Whether to convert escape sequences found in the delimiter into their corresponding characters:
|
/// Whether to convert escape sequences found in the delimiter into their corresponding characters:
|
||||||
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
delimiter_escaping: bool,
|
delimiter_escaping: Item<bool>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
let delimiter = if delimiter_escaping { unescape_string(delimiter) } else { delimiter };
|
let delimiter = delimiter.element().clone();
|
||||||
|
let delimiter = if *delimiter_escaping.element() { unescape_string(delimiter) } else { delimiter };
|
||||||
|
|
||||||
string.split(&delimiter).map(str::to_string).map(Item::new_from_element).collect()
|
string.element().split(&delimiter).map(str::to_string).map(Item::new_from_element).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Joins a list of strings together with a separator between each pair. This is the inverse of the **String Split** node.
|
/// Joins a list of strings together with a separator between each pair. This is the inverse of the **String Split** node.
|
||||||
@@ -758,15 +838,18 @@ fn string_join(
|
|||||||
strings: List<String>,
|
strings: List<String>,
|
||||||
/// The text placed between each pair of strings.
|
/// The text placed between each pair of strings.
|
||||||
#[default(", ")]
|
#[default(", ")]
|
||||||
separator: String,
|
separator: Item<String>,
|
||||||
/// Whether to convert escape sequences found in the separator into their corresponding characters:
|
/// Whether to convert escape sequences found in the separator into their corresponding characters:
|
||||||
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
separator_escaping: bool,
|
separator_escaping: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let (separator, separator_escaping) = (separator.into_element(), separator_escaping.into_element());
|
||||||
let separator = if separator_escaping { unescape_string(separator) } else { separator };
|
let separator = if separator_escaping { unescape_string(separator) } else { separator };
|
||||||
|
|
||||||
strings.iter_element_values().map(|s| s.as_str()).collect::<Vec<_>>().join(&separator)
|
let joined = strings.iter_element_values().map(|s| s.as_str()).collect::<Vec<_>>().join(&separator);
|
||||||
|
|
||||||
|
Item::new_from_element(joined)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop.
|
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop.
|
||||||
@@ -794,11 +877,11 @@ fn map_string(
|
|||||||
|
|
||||||
/// Reads the current string from within a **Map String** node's loop.
|
/// Reads the current string from within a **Map String** node's loop.
|
||||||
#[node_macro::node(category("Context"))]
|
#[node_macro::node(category("Context"))]
|
||||||
fn read_string(ctx: impl Ctx + ExtractVarArgs) -> String {
|
fn read_string(ctx: impl Ctx + ExtractVarArgs) -> Item<String> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return String::new() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Item::new_from_element(String::new()) };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
var_arg.downcast_ref::<String>().cloned().unwrap_or_default()
|
var_arg.downcast_ref::<Item<String>>().cloned().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a value to a JSON string representation.
|
/// Converts a value to a JSON string representation.
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ impl PathBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Separate Glyphs" off: widen the accumulated AABBs and bundle as one override `Vector`
|
// Glyph separation off: widen the accumulated AABBs and bundle as one override `Vector`
|
||||||
if !self.merged_click_target_bboxes.is_empty() {
|
if !self.merged_click_target_bboxes.is_empty() {
|
||||||
let mut bboxes = self.merged_click_target_bboxes;
|
let mut bboxes = self.merged_click_target_bboxes;
|
||||||
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);
|
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ use core_types::{ATTR_END, ATTR_NAME, ATTR_START, Ctx};
|
|||||||
fn regex_contains(
|
fn regex_contains(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: Item<String>,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: Item<bool>,
|
||||||
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
||||||
multiline: bool,
|
multiline: Item<bool>,
|
||||||
/// Only match if the pattern appears at the start of the string.
|
/// Only match if the pattern appears at the start of the string.
|
||||||
at_start: bool,
|
at_start: Item<bool>,
|
||||||
/// Only match if the pattern appears at the end of the string.
|
/// Only match if the pattern appears at the end of the string.
|
||||||
at_end: bool,
|
at_end: Item<bool>,
|
||||||
) -> bool {
|
) -> Item<bool> {
|
||||||
|
let (string, attributes) = string.into_parts();
|
||||||
|
let pattern = pattern.element();
|
||||||
|
let (case_insensitive, multiline, at_start, at_end) = (*case_insensitive.element(), *multiline.element(), *at_start.element(), *at_end.element());
|
||||||
|
|
||||||
let flags = match (case_insensitive, multiline) {
|
let flags = match (case_insensitive, multiline) {
|
||||||
(false, false) => "",
|
(false, false) => "",
|
||||||
(true, false) => "(?i)",
|
(true, false) => "(?i)",
|
||||||
@@ -34,29 +38,34 @@ fn regex_contains(
|
|||||||
|
|
||||||
let Ok(regex) = fancy_regex::Regex::new(&anchored_pattern) else {
|
let Ok(regex) = fancy_regex::Regex::new(&anchored_pattern) else {
|
||||||
log::error!("Invalid regex pattern: {pattern}");
|
log::error!("Invalid regex pattern: {pattern}");
|
||||||
return false;
|
return Item::from_parts(false, attributes);
|
||||||
};
|
};
|
||||||
|
|
||||||
regex.is_match(&string).unwrap_or(false)
|
Item::from_parts(regex.is_match(&string).unwrap_or(false), attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces matches of a regular expression pattern in the string. The replacement string can reference captures: `$0` for the whole match and `$1`, `$2`, etc. for capture groups.
|
/// Replaces matches of a regular expression pattern in the string. The replacement string can reference captures: `$0` for the whole match and `$1`, `$2`, etc. for capture groups.
|
||||||
#[node_macro::node(category("Text: Regex"))]
|
#[node_macro::node(category("Text: Regex"))]
|
||||||
fn regex_replace(
|
fn regex_replace(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: Item<String>,
|
||||||
/// The replacement string. Use `$0` for the whole match and `$1`, `$2`, etc. for capture groups.
|
/// The replacement string. Use `$0` for the whole match and `$1`, `$2`, etc. for capture groups.
|
||||||
replacement: String,
|
replacement: Item<String>,
|
||||||
/// Replace all matches. When disabled, only the first match is replaced.
|
/// Replace all matches. When disabled, only the first match is replaced.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
replace_all: bool,
|
replace_all: Item<bool>,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: Item<bool>,
|
||||||
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
||||||
multiline: bool,
|
multiline: Item<bool>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let mut string = string;
|
||||||
|
let pattern = pattern.element().clone();
|
||||||
|
let replacement = replacement.element().clone();
|
||||||
|
let (replace_all, case_insensitive, multiline) = (*replace_all.element(), *case_insensitive.element(), *multiline.element());
|
||||||
|
|
||||||
let flags = match (case_insensitive, multiline) {
|
let flags = match (case_insensitive, multiline) {
|
||||||
(false, false) => "",
|
(false, false) => "",
|
||||||
(true, false) => "(?i)",
|
(true, false) => "(?i)",
|
||||||
@@ -70,11 +79,14 @@ fn regex_replace(
|
|||||||
return string;
|
return string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if replace_all {
|
let result = if replace_all {
|
||||||
regex.replace_all(&string, replacement.as_str()).into_owned()
|
regex.replace_all(string.element(), replacement.as_str()).into_owned()
|
||||||
} else {
|
} else {
|
||||||
regex.replace(&string, replacement.as_str()).into_owned()
|
regex.replace(string.element(), replacement.as_str()).into_owned()
|
||||||
}
|
};
|
||||||
|
|
||||||
|
*string.element_mut() = result;
|
||||||
|
string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds a regex match in the string and returns its components. The result is a list where the first item is the whole match (`$0`) and subsequent items are the capture groups (`$1`, `$2`, etc., if any).
|
/// Finds a regex match in the string and returns its components. The result is a list where the first item is the whole match (`$0`) and subsequent items are the capture groups (`$1`, `$2`, etc., if any).
|
||||||
@@ -87,16 +99,20 @@ fn regex_replace(
|
|||||||
fn regex_find(
|
fn regex_find(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: Item<String>,
|
||||||
/// Which non-overlapping occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.
|
/// Which non-overlapping occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.
|
||||||
match_index: SignedInteger,
|
match_index: Item<SignedInteger>,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: Item<bool>,
|
||||||
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
||||||
multiline: bool,
|
multiline: Item<bool>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
|
let string = string.element();
|
||||||
|
let pattern = pattern.element();
|
||||||
|
let (match_index, case_insensitive, multiline) = (*match_index.element(), *case_insensitive.element(), *multiline.element());
|
||||||
|
|
||||||
if pattern.is_empty() {
|
if pattern.is_empty() {
|
||||||
return List::new();
|
return List::new();
|
||||||
}
|
}
|
||||||
@@ -118,7 +134,7 @@ fn regex_find(
|
|||||||
let capture_names: Vec<Option<String>> = regex.capture_names().map(|name| name.map(str::to_string)).collect();
|
let capture_names: Vec<Option<String>> = regex.capture_names().map(|name| name.map(str::to_string)).collect();
|
||||||
|
|
||||||
// Collect all matches since we need to support negative indexing
|
// Collect all matches since we need to support negative indexing
|
||||||
let matches: Vec<_> = regex.captures_iter(&string).filter_map(|c| c.ok()).collect();
|
let matches: Vec<_> = regex.captures_iter(string).filter_map(|c| c.ok()).collect();
|
||||||
|
|
||||||
let match_index = match_index as i32;
|
let match_index = match_index as i32;
|
||||||
let resolved_index = if match_index < 0 {
|
let resolved_index = if match_index < 0 {
|
||||||
@@ -158,14 +174,18 @@ fn regex_find(
|
|||||||
fn regex_find_all(
|
fn regex_find_all(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to search within.
|
/// The string to search within.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: Item<String>,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: Item<bool>,
|
||||||
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
||||||
multiline: bool,
|
multiline: Item<bool>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
|
let string = string.element();
|
||||||
|
let pattern = pattern.element();
|
||||||
|
let (case_insensitive, multiline) = (*case_insensitive.element(), *multiline.element());
|
||||||
|
|
||||||
if pattern.is_empty() {
|
if pattern.is_empty() {
|
||||||
return List::new();
|
return List::new();
|
||||||
}
|
}
|
||||||
@@ -184,7 +204,7 @@ fn regex_find_all(
|
|||||||
};
|
};
|
||||||
|
|
||||||
regex
|
regex
|
||||||
.find_iter(&string)
|
.find_iter(string)
|
||||||
.filter_map(|m| m.ok())
|
.filter_map(|m| m.ok())
|
||||||
.map(|m| {
|
.map(|m| {
|
||||||
Item::new_from_element(m.as_str().to_string())
|
Item::new_from_element(m.as_str().to_string())
|
||||||
@@ -201,16 +221,19 @@ fn regex_find_all(
|
|||||||
fn regex_split(
|
fn regex_split(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The string to split into substrings.
|
/// The string to split into substrings.
|
||||||
string: String,
|
string: Item<String>,
|
||||||
/// The regular expression pattern to split on. Matches are consumed and not included in the output.
|
/// The regular expression pattern to split on. Matches are consumed and not included in the output.
|
||||||
pattern: String,
|
pattern: Item<String>,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: Item<bool>,
|
||||||
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
/// Make `^` and `$` match the start and end of each line, not just the whole string.
|
||||||
multiline: bool,
|
multiline: Item<bool>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
|
let pattern = pattern.element().clone();
|
||||||
|
let (case_insensitive, multiline) = (*case_insensitive.element(), *multiline.element());
|
||||||
|
|
||||||
if pattern.is_empty() {
|
if pattern.is_empty() {
|
||||||
return List::new_from_element(string);
|
return List::new_from_item(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
let flags = match (case_insensitive, multiline) {
|
let flags = match (case_insensitive, multiline) {
|
||||||
@@ -223,8 +246,8 @@ fn regex_split(
|
|||||||
|
|
||||||
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
|
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
|
||||||
log::error!("Invalid regex pattern: {pattern}");
|
log::error!("Invalid regex pattern: {pattern}");
|
||||||
return List::new_from_element(string);
|
return List::new_from_item(string);
|
||||||
};
|
};
|
||||||
|
|
||||||
regex.split(&string).filter_map(|s| s.ok()).map(|s| s.to_string()).map(Item::new_from_element).collect()
|
regex.split(string.element()).filter_map(|s| s.ok()).map(|s| s.to_string()).map(Item::new_from_element).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ use core_types::gpoll::{Extent, GPoll, Interrupt};
|
|||||||
use core_types::transform::{ApplyTransform, ScaleType, Transform};
|
use core_types::transform::{ApplyTransform, ScaleType, Transform};
|
||||||
use core_types::{CacheHash, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint};
|
use core_types::{CacheHash, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint};
|
||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphic_types::Graphic;
|
|
||||||
use graphic_types::Vector;
|
|
||||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
|
use graphic_types::{Artboard, Graphic, Vector};
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
|
|
||||||
/// Applies the specified transform to each lane of the input, composing onto the lane's transform attribute.
|
/// Applies the specified transform to each lane of the input, composing onto the lane's transform attribute.
|
||||||
@@ -97,7 +96,10 @@ fn replace_transform<T>(_: impl Ctx + InjectFootprint, (element, _content_transf
|
|||||||
// TODO: Figure out how this node should behave once #2982 is implemented.
|
// TODO: Figure out how this node should behave once #2982 is implemented.
|
||||||
/// Obtains the transform of the first lane of the input, if present.
|
/// Obtains the transform of the first lane of the input, if present.
|
||||||
#[node_macro::node(category("Math: Transform"), path(core_types::vector))]
|
#[node_macro::node(category("Math: Transform"), path(core_types::vector))]
|
||||||
fn extract_transform<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient)] content: IList<T>) -> DAffine2 {
|
fn extract_transform<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||||
|
_: impl Ctx,
|
||||||
|
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String, Artboard)] content: IList<T>,
|
||||||
|
) -> DAffine2 {
|
||||||
match content.len() {
|
match content.len() {
|
||||||
0 => DAffine2::default(),
|
0 => DAffine2::default(),
|
||||||
_ => content.lane(0).attr::<TransformAttr>(),
|
_ => content.lane(0).attr::<TransformAttr>(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use core_types::attribute::{Attr, EditorLayerPath, RemoveAttr, Transform as TransformAttr};
|
use core_types::attribute::{Attr, EditorLayerPath, RemoveAttr, Transform as TransformAttr};
|
||||||
use core_types::gpoll::{GraphError, Interrupt};
|
use core_types::gpoll::{GraphError, Interrupt};
|
||||||
|
use core_types::transform::BakeTransform;
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
use core_types::{Ctx, ExtractIndex, InjectIndex};
|
use core_types::{Ctx, ExtractIndex, InjectIndex};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
@@ -38,14 +39,12 @@ fn path_modify<'e>(
|
|||||||
Ok((element, Attr(parked.as_slice()), RemoveAttr::new()))
|
Ok((element, Attr(parked.as_slice()), RemoveAttr::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
|
/// Bakes the content's transform attribute into its underlying value, resetting the attribute to the identity.
|
||||||
#[node_macro::node(category("Vector"))]
|
#[node_macro::node(category("Vector"))]
|
||||||
fn apply_transform(_ctx: impl Ctx, (mut vector, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
|
// Monomorphic on Vector: our macro cannot yet read a record element through an open generic, so master's DAffine2 and DVec2 rows have no node here.
|
||||||
|
fn bake_transform(_ctx: impl Ctx, (mut content, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
|
||||||
let transform: DAffine2 = *transform;
|
let transform: DAffine2 = *transform;
|
||||||
for (_, point) in vector.point_domain.positions_mut() {
|
content.bake_transform(&transform);
|
||||||
*point = transform.transform_point2(*point);
|
|
||||||
}
|
|
||||||
vector.segment_domain.transform(transform);
|
|
||||||
|
|
||||||
(vector, Attr(DAffine2::IDENTITY))
|
(content, Attr(DAffine2::IDENTITY))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user