mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 13:08:12 +08:00
Convert the node catalog to rank-polymorphic kernels, materialize stored values as ranked wires, and display wire rank in the graph
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
co-authored by
Dennis Kobert
parent
83cfd0225a
commit
04d6c0d5cf
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,41 @@ 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();
|
||||||
|
|
||||||
|
// An empty stack base is never served: `Extend` maps no lane onto it, so the monitor records nothing rather than an empty list.
|
||||||
|
// A base that wrongly carried a phantom element would therefore show up as a recorded row, which this catches.
|
||||||
|
// The `news` guard below is what keeps both assertions honest, since a wrong `Output` type empties every record.
|
||||||
|
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.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;
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
// ================================
|
// ================================
|
||||||
// brush
|
// brush
|
||||||
// ================================
|
// ================================
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::brush::brush::blit::IDENTIFIER,
|
|
||||||
aliases: &["graphene_brush::BlitNode", "graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::brush::brush::brush::IDENTIFIER,
|
node: graphene_std::brush::brush::brush::IDENTIFIER,
|
||||||
aliases: &["graphene_brush::BrushNode", "graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"],
|
aliases: &["graphene_brush::BrushNode", "graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"],
|
||||||
@@ -172,8 +168,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
||||||
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
||||||
},
|
},
|
||||||
|
// The legacy layer extend no longer exists as a node; the aliases still land on its identifier so the
|
||||||
|
// subgraph rebuild below recognizes and replaces the networks that carried it.
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER,
|
node: ProtoNodeIdentifier::new("graphic_nodes::graphic::LegacyLayerExtendNode"),
|
||||||
aliases: &[
|
aliases: &[
|
||||||
"graphene_core::graphic_element::LayerNode",
|
"graphene_core::graphic_element::LayerNode",
|
||||||
"graphene_core::graphic_types::LayerNode",
|
"graphene_core::graphic_types::LayerNode",
|
||||||
@@ -739,8 +737,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,
|
||||||
@@ -846,9 +848,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::context::read_index::IDENTIFIER,
|
node: graphene_std::context::read_index::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceIndexNode", "core_types::vector::InstanceIndexNode"],
|
aliases: &["graphene_core::vector::InstanceIndexNode", "core_types::vector::InstanceIndexNode"],
|
||||||
},
|
},
|
||||||
|
// The string map folded into the general Map, and its reader into the vararg readers.
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::graphic::map::IDENTIFIER,
|
node: graphene_std::graphic::map::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceMapNode"],
|
aliases: &["graphene_core::vector::InstanceMapNode", "text_nodes::MapStringNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::context::read_position::IDENTIFIER,
|
node: graphene_std::context::read_position::IDENTIFIER,
|
||||||
@@ -858,6 +861,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::context::read_vector::IDENTIFIER,
|
node: graphene_std::context::read_vector::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceVectorNode"],
|
aliases: &["graphene_core::vector::InstanceVectorNode"],
|
||||||
},
|
},
|
||||||
|
NodeReplacement {
|
||||||
|
node: graphene_std::context::read_string::IDENTIFIER,
|
||||||
|
aliases: &["text_nodes::ReadStringNode"],
|
||||||
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::repeat::repeat::IDENTIFIER,
|
node: graphene_std::repeat::repeat::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceRepeatNode", "core_types::vector::InstanceRepeatNode"],
|
aliases: &["graphene_core::vector::InstanceRepeatNode", "core_types::vector::InstanceRepeatNode"],
|
||||||
@@ -1289,9 +1296,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 +1332,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 +1345,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 +1642,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 +2195,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 +2418,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 +2473,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 +2687,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}
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -546,8 +546,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 +587,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 +730,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 +1022,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]
|
||||||
|
|||||||
@@ -945,33 +945,6 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn the_clone_node_clones_the_element_out_of_its_record_wire() {
|
|
||||||
let network = ProtoNetwork {
|
|
||||||
stack_need: 0,
|
|
||||||
inputs: vec![],
|
|
||||||
output: NodeId(1),
|
|
||||||
nodes: vec![
|
|
||||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![7.]).into()), vec![])),
|
|
||||||
(NodeId(1), proto_node("graphene_core::debug::CloneNode", vec![NodeId(0)])),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let executor = build_executor(network);
|
|
||||||
let arena = Arena::new(1 << 20).unwrap();
|
|
||||||
let generations = [];
|
|
||||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
|
||||||
let handle = executor.tree().get(NodeId(1)).unwrap();
|
|
||||||
let layout = handle.layout().clone();
|
|
||||||
let edge = handle.duplicate().downcast_record::<f64>().unwrap();
|
|
||||||
let frames = core_types::record::test_frames(executor.tree().stack_need());
|
|
||||||
let GPoll::Final(value) = core_types::record::serve_input(&edge, &ctx, &frames) else {
|
|
||||||
panic!("the flipped clone must evaluate over record wires, got a non-final poll");
|
|
||||||
};
|
|
||||||
assert_eq!(unsafe { core_types::record::read_element::<f64>(layout.rec(&value)) }, 7.);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_palette_folds_its_record_wire_to_a_color_level() {
|
fn the_palette_folds_its_record_wire_to_a_color_level() {
|
||||||
let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>)).unwrap();
|
let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>)).unwrap();
|
||||||
|
|||||||
@@ -1,59 +1,20 @@
|
|||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
|
|
||||||
use graphene_std::list::List;
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
use graphene_std::raster::GPU;
|
|
||||||
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
use graphene_std::SourceId;
|
|
||||||
use graphene_std::raster::{CPU, Raster};
|
|
||||||
use graphene_std::registry::{ConstructionError, NodeIOTypes, RegistryEntry, SourceHandle};
|
use graphene_std::registry::{ConstructionError, NodeIOTypes, RegistryEntry, SourceHandle};
|
||||||
#[cfg(feature = "gpu")]
|
use graphene_std::{Context, ProtoNodeIdentifier, concrete};
|
||||||
use graphene_std::runtime::RuntimeHandle;
|
use node_registry_macros::convert_node;
|
||||||
|
|
||||||
use graphene_std::vector::Vector;
|
|
||||||
use graphene_std::{Context, Graphic, ProtoNodeIdentifier, concrete};
|
|
||||||
use node_registry_macros::{convert_node, into_node};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
use wgpu_executor::WgpuExecutorHandle;
|
|
||||||
|
|
||||||
fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
||||||
let mut node_types: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
|
let mut node_types: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
|
||||||
// ==========
|
// =============
|
||||||
// INTO NODES
|
// CONVERT NODES
|
||||||
// ==========
|
// =============
|
||||||
into_node!(from: List<Graphic>, to: List<Graphic>),
|
|
||||||
into_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
into_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>),
|
|
||||||
convert_node!(from: List<Vector>, to: List<Graphic>),
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: List<Graphic>),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: List<Graphic>),
|
|
||||||
// into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>),
|
|
||||||
convert_node!(from: DVec2, to: DVec2),
|
convert_node!(from: DVec2, to: DVec2),
|
||||||
convert_node!(from: List<Vector>, to: List<Vector>),
|
|
||||||
convert_node!(from: DVec2, to: List<Vector>),
|
|
||||||
convert_node!(from: String, to: String),
|
convert_node!(from: String, to: String),
|
||||||
convert_node!(from: bool, to: String),
|
convert_node!(from: bool, to: String),
|
||||||
convert_node!(from: DVec2, to: String),
|
convert_node!(from: DVec2, to: String),
|
||||||
convert_node!(from: IVec2, to: String),
|
convert_node!(from: IVec2, to: String),
|
||||||
convert_node!(from: DAffine2, to: String),
|
convert_node!(from: DAffine2, to: String),
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>, converter: WgpuExecutorHandle),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: List<Raster<GPU>>, converter: WgpuExecutorHandle),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>, converter: WgpuExecutorHandle),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: List<Raster<CPU>>, converter: WgpuExecutorHandle, async),
|
|
||||||
// =============
|
|
||||||
// MONITOR NODES
|
|
||||||
// =============
|
|
||||||
// ==========
|
|
||||||
// MEMO NODES
|
|
||||||
// ==========
|
|
||||||
];
|
];
|
||||||
// The transform's value-typed rows, served by `transform_value` under the
|
// The transform's value-typed rows, served by `transform_value` under the
|
||||||
// leveled transform's identifier.
|
// leveled transform's identifier.
|
||||||
@@ -103,6 +64,12 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|entry| (graphene_std::graphic::mirror::IDENTIFIER.clone(), entry)),
|
.map(|entry| (graphene_std::graphic::mirror::IDENTIFIER.clone(), entry)),
|
||||||
);
|
);
|
||||||
|
// The gradient over a graphic level's color leaves, served under the colors to gradient identifier.
|
||||||
|
node_types.extend(
|
||||||
|
graphene_std::graphic::colors_to_gradient_graphic_entries()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| (graphene_std::graphic::colors_to_gradient::IDENTIFIER.clone(), entry)),
|
||||||
|
);
|
||||||
// The morph's plain vector rows, served under its identifier.
|
// The morph's plain vector rows, served under its identifier.
|
||||||
node_types.extend(
|
node_types.extend(
|
||||||
graphene_std::vector::morph_vector_entries()
|
graphene_std::vector::morph_vector_entries()
|
||||||
@@ -128,23 +95,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|entry| (graphene_std::graphic::to_graphic::IDENTIFIER.clone(), entry)),
|
.map(|entry| (graphene_std::graphic::to_graphic::IDENTIFIER.clone(), entry)),
|
||||||
);
|
);
|
||||||
// The transitional level bridge: a leveled input materializes into the legacy
|
|
||||||
// list an unconverted consumer expects. The rows are keyed under the legacy
|
|
||||||
// convert identifiers and die with the last legacy consumer.
|
|
||||||
node_types.extend(
|
|
||||||
graphene_std::graphic::level_to_list_entries()
|
|
||||||
.into_iter()
|
|
||||||
.zip([
|
|
||||||
"List<Graphic>",
|
|
||||||
"List<Vector>",
|
|
||||||
"List<Raster<CPU>>",
|
|
||||||
"List<Raster<GPU>>",
|
|
||||||
"List<Color>",
|
|
||||||
"List<Gradient>",
|
|
||||||
"List<String>",
|
|
||||||
])
|
|
||||||
.map(|(entry, target)| (ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{target}>")), entry)),
|
|
||||||
);
|
|
||||||
// =============
|
// =============
|
||||||
// CONVERT NODES
|
// CONVERT NODES
|
||||||
// =============
|
// =============
|
||||||
@@ -208,33 +158,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
|||||||
pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>> = once_cell::sync::Lazy::new(node_registry);
|
pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>> = once_cell::sync::Lazy::new(node_registry);
|
||||||
|
|
||||||
mod node_registry_macros {
|
mod node_registry_macros {
|
||||||
macro_rules! into_node {
|
|
||||||
(from: $from:ty, to: $to:ty) => {
|
|
||||||
(
|
|
||||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
|
|
||||||
RegistryEntry {
|
|
||||||
layout_meta: Some(core_types::record::LayoutMeta::retype(core_types::record::element_write::<$to>())),
|
|
||||||
io: NodeIOTypes::new(
|
|
||||||
concrete!(Context),
|
|
||||||
core_types::registry::record_type::<$to>(),
|
|
||||||
vec![core_types::registry::record_source_type::<$from>()],
|
|
||||||
),
|
|
||||||
constructor: |inputs| {
|
|
||||||
if inputs.len() != 1 {
|
|
||||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
|
||||||
}
|
|
||||||
let mut inputs = inputs.into_iter();
|
|
||||||
let handle = inputs.next().unwrap();
|
|
||||||
let layout = handle.layout().clone();
|
|
||||||
let node = graphene_std::ops::IntoNode::<$to, _, $from>::new(handle.downcast_record::<$from>()?, &layout);
|
|
||||||
Ok(SourceHandle::new_record::<$to>(
|
|
||||||
std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>
|
|
||||||
))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
macro_rules! convert_node {
|
macro_rules! convert_node {
|
||||||
(from: $from:ty, to: numbers) => {{
|
(from: $from:ty, to: numbers) => {{
|
||||||
let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
|
let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
|
||||||
@@ -354,7 +277,6 @@ mod node_registry_macros {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use convert_node;
|
pub(crate) use convert_node;
|
||||||
pub(crate) use into_node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
|
|||||||
DocumentNode {
|
DocumentNode {
|
||||||
call_argument: concrete!(Context),
|
call_argument: concrete!(Context),
|
||||||
inputs: vec![NodeInput::import(core_types::Type::Fn(Box::new(concrete!(Context)), Box::new(generic!(T))), 0)],
|
inputs: vec![NodeInput::import(core_types::Type::Fn(Box::new(concrete!(Context)), Box::new(generic!(T))), 0)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render_intermediate_leveled::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render_intermediate::IDENTIFIER),
|
||||||
context_features: graphene_std::ContextDependencies::new(ContextFeatures::VARARGS, ContextFeatures::INDEX),
|
context_features: graphene_std::ContextDependencies::new(ContextFeatures::VARARGS, ContextFeatures::INDEX),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
use dyn_any::DynAny;
|
||||||
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,
|
||||||
@@ -138,7 +139,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> {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -228,36 +228,33 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
|
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
|
||||||
pub trait TryFromGraphic: Clone + Sized {
|
pub trait TryFromGraphic: Clone + Sized {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>>;
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>>;
|
||||||
|
|
||||||
|
/// The leaf's element, borrowed, where `graphic` is this type's variant.
|
||||||
|
fn leaf_of<'a>(graphic: &'a Graphic<'_>) -> Option<&'a Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for Vector {
|
macro_rules! try_from_graphic {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
($($variant:ident: $element:ty;)*) => {
|
||||||
if let Graphic::Vector(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
$(
|
||||||
}
|
impl TryFromGraphic for $element {
|
||||||
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
|
if let Graphic::$variant(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf_of<'a>(graphic: &'a Graphic<'_>) -> Option<&'a Self> {
|
||||||
|
if let Graphic::$variant(t) = graphic { Some(t) } else { None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for Raster<CPU> {
|
try_from_graphic! {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
Vector: Vector;
|
||||||
if let Graphic::RasterCPU(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
RasterCPU: Raster<CPU>;
|
||||||
}
|
Color: Color;
|
||||||
}
|
Gradient: Gradient;
|
||||||
|
Text: String;
|
||||||
impl TryFromGraphic for Color {
|
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
|
||||||
if let Graphic::Color(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFromGraphic for Gradient {
|
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
|
||||||
if let Graphic::Gradient(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFromGraphic for String {
|
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
|
||||||
if let Graphic::Text(t) = graphic { Some(List::new_from_element(t)) } else { None }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -59,6 +59,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 {
|
||||||
@@ -67,6 +74,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,
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling.
|
/// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling.
|
||||||
#[node_macro::node(category(""), skip_impl)]
|
fn blit<BlendFn: Fn(Color, Color) -> Color>(mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>> {
|
||||||
fn blit<BlendFn: Fn(Color, Color) -> Color>(_: impl Ctx, mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>> {
|
|
||||||
if positions.is_empty() {
|
if positions.is_empty() {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
@@ -340,7 +339,7 @@ fn brush_core(list_item: Item<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: &B
|
|||||||
List::new_from_item(item)
|
List::new_from_item(item)
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
|
let list = blit(blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
|
||||||
assert_eq!(list.len(), 1);
|
assert_eq!(list.len(), 1);
|
||||||
list.into_iter().next().unwrap_or_default()
|
list.into_iter().next().unwrap_or_default()
|
||||||
};
|
};
|
||||||
@@ -376,12 +375,10 @@ fn brush_core(list_item: Item<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: &B
|
|||||||
_ => BlendMode::Restore,
|
_ => BlendMode::Restore,
|
||||||
};
|
};
|
||||||
|
|
||||||
erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| {
|
erase_restore_mask = blit(List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| blend_colors(a, b, mask_blend_mode, 1.))
|
||||||
blend_colors(a, b, mask_blend_mode, 1.)
|
.into_iter()
|
||||||
})
|
.next()
|
||||||
.into_iter()
|
.unwrap_or_default();
|
||||||
.next()
|
|
||||||
.unwrap_or_default();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
|
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
|
||||||
@@ -458,6 +455,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,5 +1,4 @@
|
|||||||
use core_types::gpoll::GPoll;
|
use core_types::gpoll::GPoll;
|
||||||
use core_types::list::List;
|
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
|
use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -74,15 +73,13 @@ fn quantize_real_time<T>(
|
|||||||
Context -> DAffine2,
|
Context -> DAffine2,
|
||||||
Context -> Footprint,
|
Context -> Footprint,
|
||||||
Context -> DVec2,
|
Context -> DVec2,
|
||||||
Context -> List<Vector>,
|
Context -> Vector,
|
||||||
Context -> List<Graphic>,
|
Context -> Graphic,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> Raster<CPU>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> Raster<GPU>,
|
||||||
Context -> List<Color>,
|
Context -> Color,
|
||||||
Context -> List<Artboard>,
|
Context -> Gradient,
|
||||||
Context -> List<Gradient>,
|
Context -> Artboard,
|
||||||
Context -> List<String>,
|
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
Context -> (),
|
||||||
)]
|
)]
|
||||||
value: impl Node<Context<'_>, Output = T>,
|
value: impl Node<Context<'_>, Output = T>,
|
||||||
@@ -114,15 +111,13 @@ fn quantize_animation_time<T>(
|
|||||||
Context -> DAffine2,
|
Context -> DAffine2,
|
||||||
Context -> Footprint,
|
Context -> Footprint,
|
||||||
Context -> DVec2,
|
Context -> DVec2,
|
||||||
Context -> List<Vector>,
|
Context -> Vector,
|
||||||
Context -> List<Graphic>,
|
Context -> Graphic,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> Raster<CPU>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> Raster<GPU>,
|
||||||
Context -> List<Color>,
|
Context -> Color,
|
||||||
Context -> List<Artboard>,
|
Context -> Gradient,
|
||||||
Context -> List<Gradient>,
|
Context -> Artboard,
|
||||||
Context -> List<String>,
|
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
Context -> (),
|
||||||
)]
|
)]
|
||||||
value: impl Node<Context<'_>, Output = T>,
|
value: impl Node<Context<'_>, Output = T>,
|
||||||
|
|||||||
@@ -7,46 +7,6 @@ use graphic_types::vector_types::Gradient;
|
|||||||
use graphic_types::{Graphic, Vector};
|
use graphic_types::{Graphic, Vector};
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
|
||||||
fn read_graphic(ctx: impl Ctx + ExtractVarArgs) -> List<Graphic<'static>> {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
|
||||||
fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> List<Vector> {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
|
||||||
fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> List<Raster<CPU>> {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
|
||||||
fn read_color(ctx: impl Ctx + ExtractVarArgs) -> List<Color> {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
|
||||||
fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<Gradient> {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The mapped row riding as vararg 0, in the production single-item shape.
|
/// The mapped row riding as vararg 0, in the production single-item shape.
|
||||||
fn vararg_list<T: 'static>(ctx: &impl ExtractVarArgs) -> Option<&List<T>> {
|
fn vararg_list<T: 'static>(ctx: &impl ExtractVarArgs) -> Option<&List<T>> {
|
||||||
let arg = ctx.vararg(0).ok()?;
|
let arg = ctx.vararg(0).ok()?;
|
||||||
@@ -69,54 +29,30 @@ fn vararg_element<T: Clone + 'static>(ctx: &(impl ExtractVarArgs + ExtractIndex)
|
|||||||
.ok_or_else(|| GraphError::new("vararg row addressed past its items").into())
|
.ok_or_else(|| GraphError::new("vararg row addressed past its items").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
|
// The vararg readers: the mapped row's items as the lanes of a level, elements
|
||||||
#[node_macro::node(category("Test"), extent_raw(read_graphic_row_extent))]
|
// only, one node per element type since a reader names its type.
|
||||||
pub fn read_graphic_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Graphic<'static>>, Interrupt> {
|
macro_rules! vararg_readers {
|
||||||
vararg_element(ctx)
|
($($node:ident / $extent:ident / $node_type:ident: $element:ty;)*) => {
|
||||||
|
$(
|
||||||
|
#[node_macro::node(category("Context"), path(graphene_core::vector), extent_raw($extent))]
|
||||||
|
pub fn $node(ctx: impl Ctx + ExtractVarArgs + ExtractIndex, _primary: ()) -> Result<IList<$element>, Interrupt> {
|
||||||
|
vararg_element(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $extent<C: Ctx + ExtractVarArgs, N>(_: &$node_type<N>, ctx: &C, level: u8) -> GPoll<Extent> {
|
||||||
|
vararg_lanes::<$element>(ctx, level)
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_graphic_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadGraphicRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
|
vararg_readers! {
|
||||||
vararg_lanes::<Graphic>(ctx, level)
|
read_graphic / read_graphic_extent / ReadGraphicNode: Graphic<'static>;
|
||||||
}
|
read_vector / read_vector_extent / ReadVectorNode: Vector;
|
||||||
|
read_raster / read_raster_extent / ReadRasterNode: Raster<CPU>;
|
||||||
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
|
read_color / read_color_extent / ReadColorNode: Color;
|
||||||
#[node_macro::node(category("Test"), extent_raw(read_vector_row_extent))]
|
read_gradient / read_gradient_extent / ReadGradientNode: Gradient;
|
||||||
pub fn read_vector_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Vector>, Interrupt> {
|
read_string / read_string_extent / ReadStringNode: String;
|
||||||
vararg_element(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_vector_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadVectorRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
|
|
||||||
vararg_lanes::<Vector>(ctx, level)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
|
|
||||||
#[node_macro::node(category("Test"), extent_raw(read_raster_row_extent))]
|
|
||||||
pub fn read_raster_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Raster<CPU>>, Interrupt> {
|
|
||||||
vararg_element(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_raster_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadRasterRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
|
|
||||||
vararg_lanes::<Raster<CPU>>(ctx, level)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
|
|
||||||
#[node_macro::node(category("Test"), extent_raw(read_color_row_extent))]
|
|
||||||
pub fn read_color_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Color>, Interrupt> {
|
|
||||||
vararg_element(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_color_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadColorRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
|
|
||||||
vararg_lanes::<Color>(ctx, level)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
|
|
||||||
#[node_macro::node(category("Test"), extent_raw(read_gradient_row_extent))]
|
|
||||||
pub fn read_gradient_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Gradient>, Interrupt> {
|
|
||||||
vararg_element(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_gradient_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadGradientRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
|
|
||||||
vararg_lanes::<Gradient>(ctx, level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(core_types::vector))]
|
#[node_macro::node(category("Context"), path(core_types::vector))]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1944,7 +1944,12 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let node = install(crate::context::ReadColorRowNode::new(), crate::context::read_color_row_layout_meta(), &[]);
|
let unit = Layout::default().with_writes(0, core_types::record::element_write::<()>(), &[]);
|
||||||
|
let node = install(
|
||||||
|
crate::context::ReadColorNode::new(ValueSource::new(()), &unit),
|
||||||
|
crate::context::read_color_layout_meta(),
|
||||||
|
&[Some(&unit)],
|
||||||
|
);
|
||||||
let out = Node::<ContextImpl>::layout(&node).clone();
|
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||||
assert_eq!(out.depth, 1);
|
assert_eq!(out.depth, 1);
|
||||||
// No row pushed: an empty level, matching the legacy empty-list return.
|
// No row pushed: an empty level, matching the legacy empty-list return.
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Transform as TransformAttr, WireValue};
|
use crate::record::Inherited;
|
||||||
|
use core_types::arena::Arena;
|
||||||
|
use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Opacity, OpacityFill, Transform as TransformAttr, WireValue};
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
||||||
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt, Level};
|
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::node::Lane;
|
use core_types::node::Lane;
|
||||||
use core_types::registry::types::{Angle, SignedInteger};
|
use core_types::registry::types::{Angle, SignedInteger};
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
|
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{Graphic, IntoGraphicList};
|
use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, walk_vector_rows};
|
||||||
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Artboard, Vector};
|
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
|
||||||
|
use graphic_types::{ATTR_FILL, ATTR_STROKE, Artboard, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
|
|
||||||
use vector_types::gradient::{GradientSpreadMethod, GradientType as GradientTypeValue};
|
use vector_types::gradient::{GradientSpreadMethod, GradientType as GradientTypeValue};
|
||||||
use vector_types::{Gradient, GradientStop, ReferencePoint};
|
use vector_types::{Gradient, GradientStop, ReferencePoint};
|
||||||
|
|
||||||
|
fn arena_exhausted() -> Interrupt {
|
||||||
|
GraphError {
|
||||||
|
kind: ErrorKind::ArenaExhausted,
|
||||||
|
trace: Vec::new(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolves a signed index over `total` lanes: negatives count from the end,
|
/// Resolves a signed index over `total` lanes: negatives count from the end,
|
||||||
/// out of range resolves to nothing.
|
/// out of range resolves to nothing.
|
||||||
fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
||||||
@@ -79,7 +89,7 @@ pub fn item_at_index<T: Clone + Default + Send + Sync + CacheHash + 'static>(
|
|||||||
/// the subgraph's lanes concatenated into one flat level. The level reports a
|
/// the subgraph's lanes concatenated into one flat level. The level reports a
|
||||||
/// lower bound; consumers drain to the past-end signal.
|
/// lower bound; consumers drain to the past-end signal.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
|
pub fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
|
||||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||||
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
||||||
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
||||||
@@ -181,7 +191,7 @@ where
|
|||||||
|
|
||||||
#[node_macro::node(category("General"), extent(mirror_extent))]
|
#[node_macro::node(category("General"), extent(mirror_extent))]
|
||||||
fn mirror<'e>(
|
fn mirror<'e>(
|
||||||
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
content: IList<Graphic<'static>>,
|
content: IList<Graphic<'static>>,
|
||||||
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
||||||
#[unit(" px")] offset: f64,
|
#[unit(" px")] offset: f64,
|
||||||
@@ -219,7 +229,7 @@ fn mirror_extent(
|
|||||||
/// mirror identifier.
|
/// mirror identifier.
|
||||||
#[node_macro::node(category(""), extent(mirror_vector_extent))]
|
#[node_macro::node(category(""), extent(mirror_vector_extent))]
|
||||||
fn mirror_vector<'e>(
|
fn mirror_vector<'e>(
|
||||||
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
content: IList<Vector>,
|
content: IList<Vector>,
|
||||||
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
||||||
#[unit(" px")] offset: f64,
|
#[unit(" px")] offset: f64,
|
||||||
@@ -251,6 +261,7 @@ fn mirror_vector_extent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use _map_mod::map_entries;
|
||||||
pub use _mirror_vector_mod::mirror_vector_entries;
|
pub use _mirror_vector_mod::mirror_vector_entries;
|
||||||
|
|
||||||
/// `node_path` with its trailing entry dropped: the containing network's path, which is also a unique
|
/// `node_path` with its trailing entry dropped: the containing network's path, which is also a unique
|
||||||
@@ -265,10 +276,7 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: Vec<NodeId>) -> Vec<NodeId> {
|
|||||||
/// each lane, which lets editor tools trace data back to its layer.
|
/// each lane, which lets editor tools trace data back to its layer.
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub fn stamp_layer_path<'e, T>(ctx: impl Ctx + ExtractArena<'e>, element: T, path: Vec<NodeId>) -> Result<(T, Attr<'e, EditorLayerPath>), Interrupt> {
|
pub fn stamp_layer_path<'e, T>(ctx: impl Ctx + ExtractArena<'e>, element: T, path: Vec<NodeId>) -> Result<(T, Attr<'e, EditorLayerPath>), Interrupt> {
|
||||||
let (parked, _) = ctx.arena().alloc(path).ok_or(GraphError {
|
let (parked, _) = ctx.arena().alloc(path).ok_or_else(arena_exhausted)?;
|
||||||
kind: core_types::gpoll::ErrorKind::ArenaExhausted,
|
|
||||||
trace: Vec::new(),
|
|
||||||
})?;
|
|
||||||
Ok((element, Attr(parked.as_slice())))
|
Ok((element, Attr(parked.as_slice())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,10 +292,7 @@ pub fn write_attribute<'e, T, V: WireValue>(
|
|||||||
name: Named<Name0>,
|
name: Named<Name0>,
|
||||||
#[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec<NodeId>, String)] value: V,
|
#[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec<NodeId>, String)] value: V,
|
||||||
) -> Result<(T, Attr<'e, Named<Name0, V::Row>>), Interrupt> {
|
) -> Result<(T, Attr<'e, Named<Name0, V::Row>>), Interrupt> {
|
||||||
let parked = value.park(ctx.arena()).ok_or(GraphError {
|
let parked = value.park(ctx.arena()).ok_or_else(arena_exhausted)?;
|
||||||
kind: core_types::gpoll::ErrorKind::ArenaExhausted,
|
|
||||||
trace: Vec::new(),
|
|
||||||
})?;
|
|
||||||
Ok((content, Attr(parked)))
|
Ok((content, Attr(parked)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,34 +395,6 @@ fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// Performs an obsolete function as part of a migration from an older document format.
|
|
||||||
/// Users are advised to delete this node and replace it with a new one.
|
|
||||||
#[node_macro::node(category(""))]
|
|
||||||
pub fn legacy_layer_extend<T: Send + Clone>(
|
|
||||||
_: impl Ctx,
|
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)] base: List<T>,
|
|
||||||
#[expose]
|
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)]
|
|
||||||
new: List<T>,
|
|
||||||
nested_node_path: List<NodeId>,
|
|
||||||
) -> List<T> {
|
|
||||||
// Get the penultimate element of the node path, or None if the path is too short
|
|
||||||
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
|
|
||||||
let layer = {
|
|
||||||
let index = nested_node_path.len().wrapping_sub(2);
|
|
||||||
nested_node_path.element(index).copied()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut base = base;
|
|
||||||
for mut row in new.into_iter() {
|
|
||||||
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
|
||||||
base.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
base
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
|
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
|
||||||
/// The wrapped run keeps the level's element type, so the legacy boundary can
|
/// The wrapped run keeps the level's element type, so the legacy boundary can
|
||||||
/// lower a wrapped vector level to the bare typed graphic the pre-flip wrap made.
|
/// lower a wrapped vector level to the bare typed graphic the pre-flip wrap made.
|
||||||
@@ -438,23 +415,9 @@ fn wrap_graphic_extent<T>(_content: ListIn<'_, T>, _level: LevelIn) -> GPoll<Ext
|
|||||||
|
|
||||||
/// Converts graphical content into a `Graphic` level. A `Graphic` level passes through
|
/// Converts graphical content into a `Graphic` level. A `Graphic` level passes through
|
||||||
/// unchanged; a typed level nests as one graphic lane, keeping the pre-flip list
|
/// unchanged; a typed level nests as one graphic lane, keeping the pre-flip list
|
||||||
/// collapse (`to_graphic_typed` serves those rows). The legacy list rows accept an
|
/// collapse (`to_graphic_typed` serves those rows).
|
||||||
/// unconverted producer's list value as one element, built as a native group.
|
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(
|
pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(ctx: impl Ctx + ExtractArena<'e>, #[implementations(Graphic)] content: T) -> Result<Graphic<'e>, Interrupt> {
|
||||||
ctx: impl Ctx + core_types::context::ExtractArena<'e>,
|
|
||||||
#[implementations(
|
|
||||||
Graphic,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Raster<GPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<Gradient>,
|
|
||||||
List<String>,
|
|
||||||
)]
|
|
||||||
content: T,
|
|
||||||
) -> Result<Graphic<'e>, Interrupt> {
|
|
||||||
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
|
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,24 +426,8 @@ pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(
|
|||||||
/// without changing the level's shape. Registered under the convert identifier.
|
/// without changing the level's shape. Registered under the convert identifier.
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub fn to_graphic_element<'e, T: graphic_types::graphic::IntoGraphicElement>(
|
pub fn to_graphic_element<'e, T: graphic_types::graphic::IntoGraphicElement>(
|
||||||
ctx: impl Ctx + core_types::context::ExtractArena<'e>,
|
ctx: impl Ctx + ExtractArena<'e>,
|
||||||
#[implementations(
|
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)] content: T,
|
||||||
Graphic,
|
|
||||||
Vector,
|
|
||||||
Raster<CPU>,
|
|
||||||
Raster<GPU>,
|
|
||||||
Color,
|
|
||||||
Gradient,
|
|
||||||
String,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Raster<GPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<Gradient>,
|
|
||||||
List<String>,
|
|
||||||
)]
|
|
||||||
content: T,
|
|
||||||
) -> Result<Graphic<'e>, Interrupt> {
|
) -> Result<Graphic<'e>, Interrupt> {
|
||||||
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
|
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
|
||||||
}
|
}
|
||||||
@@ -508,21 +455,6 @@ fn to_graphic_unit_extent(_content: core_types::extent::ValueIn<'_, ()>, _level:
|
|||||||
GPoll::Final(Extent::Exactly(0))
|
GPoll::Final(Extent::Exactly(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The transitional level bridge: the input's records as the legacy list an
|
|
||||||
/// unconverted consumer expects, attributes copied through their erased
|
|
||||||
/// reads and content kept in its native form. Registered under the legacy
|
|
||||||
/// convert identifiers.
|
|
||||||
#[node_macro::node(category(""))]
|
|
||||||
pub fn level_to_list<T: Clone + Send + Sync + CacheHash + dyn_any::StaticTypeSized>(
|
|
||||||
_: impl Ctx,
|
|
||||||
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)] value: IList<T>,
|
|
||||||
_converter: (),
|
|
||||||
) -> List<T> {
|
|
||||||
let item = value.as_group_item();
|
|
||||||
graphic_types::graphic::run_to_list::<T>(&item).expect("the run holds the row's element type")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use _level_to_list_mod::level_to_list_entries;
|
|
||||||
pub use _to_graphic_element_mod::to_graphic_element_entries;
|
pub use _to_graphic_element_mod::to_graphic_element_entries;
|
||||||
pub use _to_graphic_typed_mod::to_graphic_typed_entries;
|
pub use _to_graphic_typed_mod::to_graphic_typed_entries;
|
||||||
pub use _to_graphic_unit_mod::to_graphic_unit_entries;
|
pub use _to_graphic_unit_mod::to_graphic_unit_entries;
|
||||||
@@ -539,7 +471,7 @@ pub use _to_graphic_unit_mod::to_graphic_unit_entries;
|
|||||||
/// not declare are truncated.
|
/// not declare are truncated.
|
||||||
#[node_macro::node(category("General"), extent(flatten_graphic_extent))]
|
#[node_macro::node(category("General"), extent(flatten_graphic_extent))]
|
||||||
pub fn flatten_graphic<'e>(
|
pub fn flatten_graphic<'e>(
|
||||||
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
content: IList<Graphic<'static>>,
|
content: IList<Graphic<'static>>,
|
||||||
fully_flatten: bool,
|
fully_flatten: bool,
|
||||||
) -> Result<IList<(Lane<Graphic<'static>>, Attr<'e, TransformAttr>)>, Interrupt> {
|
) -> Result<IList<(Lane<Graphic<'static>>, Attr<'e, TransformAttr>)>, Interrupt> {
|
||||||
@@ -572,64 +504,222 @@ fn flatten_graphic_extent(content: ListIn<'_, Graphic>, fully_flatten: ValueIn<'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
|
/// The `lane`-th flattened vector row of `level` as a one-item list, with the
|
||||||
#[node_macro::node(category("Vector"))]
|
/// top-level lane it descends from.
|
||||||
pub fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
fn locate_vector_row(level: GraphicLevel<'_>, lane: usize) -> Option<(List<Vector>, usize)> {
|
||||||
let graphic_list = content.into_graphic_list();
|
let mut remaining = lane;
|
||||||
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
|
let mut located = None;
|
||||||
|
walk_vector_rows(level, &mut |row| {
|
||||||
// TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node.
|
if remaining > 0 {
|
||||||
// TODO: Flattening here erases the upstream `List<Graphic>` hierarchy that editor metadata collection walks
|
remaining -= 1;
|
||||||
// TODO: to populate `upstream_footprints` / `local_transforms` / `click_targets` per child layer. As a workaround
|
return RowStep::Continue;
|
||||||
// TODO: we stash the pre-flattened list on the output so `List<Vector>::collect_metadata` can recurse into it,
|
|
||||||
// TODO: which conflates render output with editor metadata and forces the pre-compensation dance below.
|
|
||||||
// TODO: The cleaner fix is to drive each layer's metadata from its own Monitor's captured `(Context, List<Graphic>)`,
|
|
||||||
// TODO: at which point this attribute (and the equivalents in Boolean Operation, Solidify Stroke, Flatten Path,
|
|
||||||
// TODO: Morph, Rasterize) become unnecessary.
|
|
||||||
if !output.is_empty() {
|
|
||||||
// Item 0 carries a composed transform inherited from the flattened input, but the merged_layers
|
|
||||||
// already holds the original transforms; pre-compensate by item 0's inverse so the renderer's
|
|
||||||
// `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact.
|
|
||||||
let mut graphic_list = graphic_list;
|
|
||||||
let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
|
||||||
if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
|
|
||||||
let inverse = item_0_transform.inverse();
|
|
||||||
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
|
||||||
*transform = inverse * *transform;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let mut one = List::new();
|
||||||
|
row.build_into(&mut one);
|
||||||
|
located = Some((one, row.top_lane()));
|
||||||
|
RowStep::Stop
|
||||||
|
});
|
||||||
|
located
|
||||||
|
}
|
||||||
|
|
||||||
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
|
fn vector_row_count(level: GraphicLevel<'_>) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
walk_vector_rows(level, &mut |_| {
|
||||||
|
count += 1;
|
||||||
|
RowStep::Continue
|
||||||
|
});
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node.
|
||||||
|
// TODO: Flattening erases the upstream `Graphic` hierarchy that editor metadata collection walks to populate
|
||||||
|
// TODO: `upstream_footprints` / `local_transforms` / `click_targets` per child layer, so the pre-flattened list
|
||||||
|
// TODO: is stashed on row 0 for `collect_metadata` to recurse into (as Boolean Operation, Solidify Stroke,
|
||||||
|
// TODO: Flatten Path, Morph and Rasterize do). Driving each layer's metadata from its own Monitor's captured
|
||||||
|
// TODO: `(Context, List<Graphic>)` would make this attribute unnecessary.
|
||||||
|
/// The parked merged-layers snapshot for row 0. Row 0 carries a composed
|
||||||
|
/// transform the snapshot's own transforms already include, so the snapshot is
|
||||||
|
/// pre-compensated by its inverse to cancel the renderer's
|
||||||
|
/// `upstream_footprint *= row_0_transform` recursion.
|
||||||
|
fn merged_layers_snapshot<'e>(arena: &'e Arena, mut snapshot: List<Graphic<'static>>, row_0_transform: DAffine2) -> Result<&'e List<Graphic<'static>>, Interrupt> {
|
||||||
|
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
|
||||||
|
let inverse = row_0_transform.inverse();
|
||||||
|
for transform in snapshot.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
|
*transform = inverse * *transform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
arena.alloc_sized_keyed(snapshot, 0).map(|(parked, _)| parked).ok_or_else(arena_exhausted)
|
||||||
|
}
|
||||||
|
|
||||||
output
|
type FlattenedVectorRow<'a, 'e> = (
|
||||||
|
Lane<'a, Vector>,
|
||||||
|
Attr<'e, TransformAttr>,
|
||||||
|
Attr<'e, Fill>,
|
||||||
|
Attr<'e, StrokeAttr>,
|
||||||
|
Attr<'e, Opacity>,
|
||||||
|
Attr<'e, OpacityFill>,
|
||||||
|
Attr<'e, EditorLayerPath>,
|
||||||
|
Attr<'e, EditorMergedLayers>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A built vector row as the flatten's output: `carrier`'s columns with the
|
||||||
|
/// walk's composition, paint and layer path overriding, and `snapshot` parked
|
||||||
|
/// as the merged layers where given.
|
||||||
|
fn emit_vector_row<'a, 'e>(arena: &'e Arena, carrier: Lane<'a, Graphic<'static>>, row: List<Vector>, snapshot: Option<List<Graphic<'static>>>) -> Result<FlattenedVectorRow<'a, 'e>, Interrupt> {
|
||||||
|
let park_paint = |paint: Option<&Option<List<Graphic<'static>>>>| {
|
||||||
|
paint
|
||||||
|
.and_then(|paint| paint.as_ref())
|
||||||
|
.map(|paint| arena.alloc_sized_keyed(paint.clone(), 0).map(|(parked, _)| parked).ok_or_else(arena_exhausted))
|
||||||
|
.transpose()
|
||||||
|
};
|
||||||
|
let fill = park_paint(row.attribute(ATTR_FILL, 0))?;
|
||||||
|
let stroke = park_paint(row.attribute(ATTR_STROKE, 0))?;
|
||||||
|
let layer_path: Vec<NodeId> = row.attribute(ATTR_EDITOR_LAYER_PATH, 0).cloned().unwrap_or_default();
|
||||||
|
let (layer_path, _) = arena.alloc(layer_path).ok_or_else(arena_exhausted)?;
|
||||||
|
|
||||||
|
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
|
let merged_layers = snapshot.map(|snapshot| merged_layers_snapshot(arena, snapshot, transform)).transpose()?;
|
||||||
|
let element = row.element(0).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
carrier.map_element(element),
|
||||||
|
Attr(transform),
|
||||||
|
Attr(fill),
|
||||||
|
Attr(stroke),
|
||||||
|
Attr(row.attribute_cloned_or(ATTR_OPACITY, 0, 1.)),
|
||||||
|
Attr(row.attribute_cloned_or(ATTR_OPACITY_FILL, 0, 1.)),
|
||||||
|
Attr(layer_path.as_slice()),
|
||||||
|
Attr(merged_layers),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
|
||||||
|
/// Each row carries the columns of the top-level row it descends from, with the
|
||||||
|
/// path's composed transform and opacities, the reaching paint and the layer path overriding.
|
||||||
|
#[node_macro::node(category("Vector"), extent(flatten_vector_extent))]
|
||||||
|
pub fn flatten_vector<'e>(
|
||||||
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
content: IList<Graphic<'static>>,
|
||||||
|
) -> Result<
|
||||||
|
IList<(
|
||||||
|
Lane<Vector>,
|
||||||
|
Attr<'e, TransformAttr>,
|
||||||
|
Attr<'e, Fill>,
|
||||||
|
Attr<'e, StrokeAttr>,
|
||||||
|
Attr<'e, Opacity>,
|
||||||
|
Attr<'e, OpacityFill>,
|
||||||
|
Attr<'e, EditorLayerPath>,
|
||||||
|
Attr<'e, EditorMergedLayers>,
|
||||||
|
)>,
|
||||||
|
Interrupt,
|
||||||
|
> {
|
||||||
|
let lane = ctx.index() as usize;
|
||||||
|
let item = content.as_group_item();
|
||||||
|
let Some((row, top)) = locate_vector_row(GraphicLevel::Run(&item), lane) else {
|
||||||
|
return Err(GraphError::past_end().into());
|
||||||
|
};
|
||||||
|
let snapshot = (lane == 0).then(|| legacy_render_list_of(content));
|
||||||
|
emit_vector_row(ctx.arena(), content.lane(top), row, snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level holds one row per vector leaf of the walk.
|
||||||
|
fn flatten_vector_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => content.get().map(|content| Extent::Exactly(vector_row_count(GraphicLevel::Run(&content.as_group_item())))),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `lane`-th `T` leaf under the content, carrying the columns of the
|
||||||
|
/// top-level row it descends from with the path's composition overriding.
|
||||||
|
type FlattenedLeafRow<'a, 'e, T> = (Lane<'a, T>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>);
|
||||||
|
|
||||||
|
fn flatten_leaf_lane<'a, 'e, T: TryFromGraphic + dyn_any::StaticTypeSized>(content: core_types::node::List<'a, Graphic<'static>>, lane: usize) -> Result<FlattenedLeafRow<'a, 'e, T>, Interrupt> {
|
||||||
|
let mut remaining = lane;
|
||||||
|
for row in 0..content.len() {
|
||||||
|
let carrier = content.lane(row);
|
||||||
|
let mut located = None;
|
||||||
|
crate::record::walk_typed_leaves(content.element_ref(row), Inherited::of(&carrier), &mut |leaf: &T, inherited| {
|
||||||
|
if remaining > 0 {
|
||||||
|
remaining -= 1;
|
||||||
|
return RowStep::Continue;
|
||||||
|
}
|
||||||
|
located = Some((leaf.clone(), inherited));
|
||||||
|
RowStep::Stop
|
||||||
|
});
|
||||||
|
if let Some((leaf, inherited)) = located {
|
||||||
|
return Ok((carrier.map_element(leaf), Attr(inherited.transform), Attr(inherited.opacity), Attr(inherited.fill_opacity)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(GraphError::past_end().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level holds one row per `T` leaf under the content.
|
||||||
|
fn flatten_leaves_extent<T: TryFromGraphic + dyn_any::StaticTypeSized>(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => content
|
||||||
|
.get()
|
||||||
|
.map(|content| Extent::Exactly((0..content.len()).map(|row| crate::record::typed_leaf_count::<T>(content.element_ref(row))).sum())),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
|
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
|
||||||
#[node_macro::node(category("Raster"))]
|
#[node_macro::node(category("Raster"), extent(flatten_leaves_extent::<Raster<CPU>>))]
|
||||||
pub fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
|
pub fn flatten_raster<'e>(
|
||||||
content.into_flattened_list()
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
content: IList<Graphic<'static>>,
|
||||||
|
) -> Result<IList<(Lane<Raster<CPU>>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
|
||||||
|
flatten_leaf_lane(content, ctx.index() as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
|
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"), extent(flatten_leaves_extent::<Color>))]
|
||||||
pub fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
|
pub fn flatten_color<'e>(
|
||||||
content.into_flattened_list()
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
content: IList<Graphic<'static>>,
|
||||||
|
) -> Result<IList<(Lane<Color>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
|
||||||
|
flatten_leaf_lane(content, ctx.index() as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
|
/// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"), extent(flatten_leaves_extent::<Gradient>))]
|
||||||
pub fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Gradient>)] content: T) -> List<Gradient> {
|
pub fn flatten_gradient<'e>(
|
||||||
content.into_flattened_list()
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
content: IList<Graphic<'static>>,
|
||||||
|
) -> Result<IList<(Lane<Gradient>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
|
||||||
|
flatten_leaf_lane(content, ctx.index() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A gradient with `colors` as evenly spaced stops from 0 to 1; none makes a
|
||||||
|
/// black gradient and one repeats at both ends.
|
||||||
|
fn evenly_spaced_gradient(colors: &[Color]) -> Gradient {
|
||||||
|
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
|
||||||
|
match colors {
|
||||||
|
[] => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
|
||||||
|
[color] => Gradient::new(vec![stop(0., *color), stop(1., *color)]),
|
||||||
|
colors => Gradient::new(colors.iter().enumerate().map(|(index, color)| stop(index as f64 / (colors.len() - 1) as f64, *color))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
pub fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
|
||||||
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
|
evenly_spaced_gradient(&colors.iter().collect::<Vec<_>>())
|
||||||
match colors.len() {
|
|
||||||
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))]),
|
|
||||||
total => Gradient::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The gradient over a graphic level's color leaves, as [`colors_to_gradient`].
|
||||||
|
/// Registered under the colors to gradient identifier.
|
||||||
|
#[node_macro::node(category(""))]
|
||||||
|
pub fn colors_to_gradient_graphic(_: impl Ctx, colors: IList<Graphic<'static>>) -> Gradient {
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
for row in 0..colors.len() {
|
||||||
|
crate::record::walk_typed_leaves::<Color>(colors.element_ref(row), Inherited::IDENTITY, &mut |color, _| {
|
||||||
|
leaves.push(*color);
|
||||||
|
RowStep::Continue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
evenly_spaced_gradient(&leaves)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use _colors_to_gradient_graphic_mod::colors_to_gradient_graphic_entries;
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
//! Pilot record nodes over the production graphic types: element-space
|
//! The element-space walks the production flatten nodes share, plus the
|
||||||
//! expanders whose ragged nesting lives inside `Graphic` values, ahead of the
|
//! level-nesting pilots (`nested_map`, `flatten_levels`) that have no
|
||||||
//! flip. Wiring is by hand until the compiler pass constructs layouts.
|
//! production counterpart yet. The tests here drive the production nodes in
|
||||||
|
//! `graphic.rs` with hand-wired layouts.
|
||||||
|
|
||||||
use core_types::attribute::{Attr, Transform};
|
use core_types::attribute::{Opacity, OpacityFill, Transform};
|
||||||
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
|
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
|
||||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
use core_types::extent::{ExtentIn, LevelIn};
|
||||||
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||||
|
use core_types::lane::LaneSource;
|
||||||
|
use core_types::node::RecordLane;
|
||||||
|
use core_types::record::RunView;
|
||||||
use core_types::{ATTR_TRANSFORM, Color, Ctx};
|
use core_types::{ATTR_TRANSFORM, Color, Ctx};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use graphic_types::graphic::Graphic;
|
use graphic_types::graphic::{Graphic, RowStep, TryFromGraphic};
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
use vector_types::{Gradient, GradientStop};
|
use vector_types::Gradient;
|
||||||
|
|
||||||
/// Whether the walk can descend into a group: the run holds `Graphic`
|
/// Whether the walk can descend into a group: the run holds `Graphic`
|
||||||
/// elements.
|
/// elements.
|
||||||
@@ -65,63 +69,90 @@ pub(crate) fn locate<'e>(graphic: &Graphic<'e>, transform: DAffine2, fully_flatt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rank-model Flatten: one flat level holding the content's leaves, each with
|
/// The ancestor composition a typed flatten's leaf inherits: transform,
|
||||||
/// the transforms along its path composed; a group beyond the walk's depth
|
/// opacity and fill opacity multiply down the path, as the legacy flatten did.
|
||||||
/// rides as a leaf with its embedded transforms untouched.
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[node_macro::node(category("Test"), extent(flatten_extent))]
|
pub(crate) struct Inherited {
|
||||||
fn flatten(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Graphic<'static>>, fully_flatten: bool) -> Result<IList<(Graphic<'static>, Attr<Transform>)>, Interrupt> {
|
pub transform: DAffine2,
|
||||||
let mut remaining = ctx.index() as usize;
|
pub opacity: f64,
|
||||||
for row in 0..content.len() {
|
pub fill_opacity: f64,
|
||||||
let graphic = content.element_ref(row);
|
}
|
||||||
let count = leaf_count(graphic, fully_flatten, 0);
|
|
||||||
if remaining >= count {
|
impl Inherited {
|
||||||
remaining -= count;
|
pub(crate) const IDENTITY: Self = Self {
|
||||||
continue;
|
transform: DAffine2::IDENTITY,
|
||||||
}
|
opacity: 1.,
|
||||||
let transform: DAffine2 = content.lane(row).attr::<Transform>();
|
fill_opacity: 1.,
|
||||||
if let Some((leaf, composed)) = locate(graphic, transform, fully_flatten, 0, &mut remaining) {
|
};
|
||||||
return Ok((leaf, Attr(composed)));
|
|
||||||
|
/// The composition a top-level row starts from: the row's own columns.
|
||||||
|
pub(crate) fn of(lane: &RecordLane<'_>) -> Self {
|
||||||
|
Self {
|
||||||
|
transform: lane.attr::<Transform>(),
|
||||||
|
opacity: lane.attr::<Opacity>(),
|
||||||
|
fill_opacity: lane.attr::<OpacityFill>(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(GraphError::new("flatten addressed past its leaf count").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The level holds one row per leaf of the walk.
|
fn composed<S: LaneSource>(self, source: &S, lane: usize) -> Self {
|
||||||
fn flatten_extent(content: ListIn<'_, Graphic>, fully_flatten: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
Self {
|
||||||
match level.top() {
|
transform: self.transform * source.attr::<Transform>(lane),
|
||||||
true => fully_flatten
|
opacity: self.opacity * source.attr::<Opacity>(lane),
|
||||||
.get()
|
fill_opacity: self.fill_opacity * source.attr::<OpacityFill>(lane),
|
||||||
.zip(content.get())
|
}
|
||||||
.map(|(fully_flatten, content)| Extent::Exactly((0..content.len()).map(|row| leaf_count(content.element_ref(row), fully_flatten, 0)).sum())),
|
|
||||||
false => GPoll::Final(Extent::Exactly(1)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rank-model Wrap: the content level as one group element on a one-lane
|
/// Visits every `T` leaf under `graphic`, at any depth, with the composition
|
||||||
/// level, the inverse of flatten's one-level descent.
|
/// along its path. A group run typed `T` contributes its lanes directly; runs
|
||||||
#[node_macro::node(category("Test"), extent(wrap_extent))]
|
/// of other element types contribute nothing.
|
||||||
fn wrap<'e>(_: impl Ctx, content: IList<Graphic<'e>>) -> Result<IList<Graphic<'e>>, Interrupt> {
|
pub(crate) fn walk_typed_leaves<T: TryFromGraphic + dyn_any::StaticTypeSized>(graphic: &Graphic, inherited: Inherited, visit: &mut dyn FnMut(&T, Inherited) -> RowStep) -> RowStep {
|
||||||
let item = content.as_group_item();
|
match graphic {
|
||||||
Ok(Graphic::Group(core_types::record::Group { row: None, content: item }))
|
Graphic::Graphic(children) => {
|
||||||
}
|
for index in 0..children.len() {
|
||||||
|
let Some(child) = children.element(index) else { continue };
|
||||||
/// The collected group is the level's single lane.
|
if let RowStep::Stop = walk_typed_leaves(child, inherited.composed(children, index), visit) {
|
||||||
fn wrap_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll<Extent> {
|
return RowStep::Stop;
|
||||||
GPoll::Final(Extent::Exactly(1))
|
}
|
||||||
}
|
}
|
||||||
|
RowStep::Continue
|
||||||
/// Rank-model colors-to-gradient: the color level folds into one gradient
|
}
|
||||||
/// with evenly spaced stops.
|
Graphic::Group(group) => {
|
||||||
#[node_macro::node(category("Test"))]
|
let item = &group.content;
|
||||||
fn to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
|
if let Some(run) = RunView::<Graphic>::new(item) {
|
||||||
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
|
for lane in 0..item.len() {
|
||||||
match colors.len() {
|
let Some(child) = run.element(lane) else { continue };
|
||||||
0 => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
|
if let RowStep::Stop = walk_typed_leaves(child, inherited.composed(&run, lane), visit) {
|
||||||
1 => Gradient::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]),
|
return RowStep::Stop;
|
||||||
total => Gradient::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(run) = RunView::<T>::new(item) {
|
||||||
|
for lane in 0..item.len() {
|
||||||
|
let Some(leaf) = run.element(lane) else { continue };
|
||||||
|
if let RowStep::Stop = visit(leaf, inherited.composed(&run, lane)) {
|
||||||
|
return RowStep::Stop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowStep::Continue
|
||||||
|
}
|
||||||
|
leaf => match T::leaf_of(leaf) {
|
||||||
|
Some(leaf) => visit(leaf, inherited),
|
||||||
|
None => RowStep::Continue,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `T` leaves under `graphic`, at any depth.
|
||||||
|
pub(crate) fn typed_leaf_count<T: TryFromGraphic + dyn_any::StaticTypeSized>(graphic: &Graphic) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
walk_typed_leaves::<T>(graphic, Inherited::IDENTITY, &mut |_, _| {
|
||||||
|
count += 1;
|
||||||
|
RowStep::Continue
|
||||||
|
});
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
/// One content row as the production vararg shape: a single-item legacy list
|
/// One content row as the production vararg shape: a single-item legacy list
|
||||||
/// carrying the row's element only, so the list's dyn-hash is a complete
|
/// carrying the row's element only, so the list's dyn-hash is a complete
|
||||||
/// cache key over the observables.
|
/// cache key over the observables.
|
||||||
@@ -129,11 +160,12 @@ pub(crate) fn vararg_row<Row: Clone + Send + Sync + 'static>(content: core_types
|
|||||||
core_types::list::List::new_from_element(content.element_ref(row).clone())
|
core_types::list::List::new_from_element(content.element_ref(row).clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rank-model Map: one subgraph invocation per content row, the row riding as
|
/// Rank-model nested Map: one subgraph invocation per content row, the row
|
||||||
/// a vararg; the subgraph's own level nests under the content level. The
|
/// riding as a vararg; the subgraph's own level nests under the content level.
|
||||||
/// levels report a lower bound; consumers drain to the past-end signal.
|
/// The levels report a lower bound; consumers drain to the past-end signal.
|
||||||
|
/// The production `Map` is this walk with the levels concatenated.
|
||||||
#[node_macro::node(category("Test"))]
|
#[node_macro::node(category("Test"))]
|
||||||
fn map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
|
fn nested_map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
|
||||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||||
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
||||||
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
||||||
@@ -153,30 +185,6 @@ fn map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
|
|||||||
Err(GraphError::past_end().into())
|
Err(GraphError::past_end().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rank-model flat-map (the production Map): map's walk with the subgraph's
|
|
||||||
/// lanes concatenated into one flat level. The level reports a lower bound;
|
|
||||||
/// consumers drain to the past-end signal.
|
|
||||||
#[node_macro::node(category("Test"))]
|
|
||||||
fn flat_map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
|
|
||||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
|
||||||
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
|
||||||
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
|
||||||
) -> Result<IList<T>, Interrupt> {
|
|
||||||
let mut remaining = ctx.index();
|
|
||||||
for row in 0..content.len() {
|
|
||||||
let item = vararg_row(content, row);
|
|
||||||
let scoped = ctx.push_vararg(&item);
|
|
||||||
let lanes = mapped.inner_extent_at(&scoped.ctx(), row as u64)?;
|
|
||||||
if remaining >= lanes {
|
|
||||||
remaining -= lanes;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut frame = IndexLink { index: 0, outer: None };
|
|
||||||
return mapped.eval(&scoped.ctx().push_level(&mut frame, row as u64, remaining));
|
|
||||||
}
|
|
||||||
Err(GraphError::past_end().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rank-model level collapse: two nested levels become one flat level. The
|
/// Rank-model level collapse: two nested levels become one flat level. The
|
||||||
/// flat index already spans the input's depth, so the eval forwards it.
|
/// flat index already spans the input's depth, so the eval forwards it.
|
||||||
#[node_macro::node(category("Test"), extent(flatten_levels_extent))]
|
#[node_macro::node(category("Test"), extent(flatten_levels_extent))]
|
||||||
@@ -215,6 +223,7 @@ fn flatten_levels_extent(content: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent>
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::graphic::{ColorsToGradientNode, FlattenColorNode, FlattenGraphicNode, MapNode, WrapGraphicNode, flatten_color_layout_meta, flatten_graphic_layout_meta, wrap_graphic_layout_meta};
|
||||||
use core_types::SourceId;
|
use core_types::SourceId;
|
||||||
use core_types::arena::Arena;
|
use core_types::arena::Arena;
|
||||||
use core_types::attribute::Attribute as AttributeMarker;
|
use core_types::attribute::Attribute as AttributeMarker;
|
||||||
@@ -352,7 +361,7 @@ mod tests {
|
|||||||
macro_rules! build {
|
macro_rules! build {
|
||||||
($layout:ident, $rows:expr, $fully:expr) => {
|
($layout:ident, $rows:expr, $fully:expr) => {
|
||||||
install(
|
install(
|
||||||
FlattenNode::new(
|
FlattenGraphicNode::new(
|
||||||
RecordSource::new(
|
RecordSource::new(
|
||||||
GraphicSource {
|
GraphicSource {
|
||||||
layout: $layout.clone(),
|
layout: $layout.clone(),
|
||||||
@@ -363,7 +372,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
ValueSource::new($fully),
|
ValueSource::new($fully),
|
||||||
),
|
),
|
||||||
flatten_layout_meta(),
|
flatten_graphic_layout_meta(),
|
||||||
&[Some(&$layout)],
|
&[Some(&$layout)],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -453,7 +462,7 @@ mod tests {
|
|||||||
|
|
||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let node = install(
|
let node = install(
|
||||||
MapNode::<_, _, Graphic>::new(
|
NestedMapNode::<_, _, Graphic>::new(
|
||||||
RecordSource::new(
|
RecordSource::new(
|
||||||
GraphicSource {
|
GraphicSource {
|
||||||
layout: layout.clone(),
|
layout: layout.clone(),
|
||||||
@@ -496,7 +505,7 @@ mod tests {
|
|||||||
|
|
||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let flat = install(
|
let flat = install(
|
||||||
FlatMapNode::<_, _, Graphic>::new(
|
MapNode::<_, _, Graphic>::new(
|
||||||
RecordSource::new(
|
RecordSource::new(
|
||||||
GraphicSource {
|
GraphicSource {
|
||||||
layout: layout.clone(),
|
layout: layout.clone(),
|
||||||
@@ -512,7 +521,7 @@ mod tests {
|
|||||||
&[Some(&layout), Some(&layout)],
|
&[Some(&layout), Some(&layout)],
|
||||||
);
|
);
|
||||||
let mapped = install(
|
let mapped = install(
|
||||||
MapNode::<_, _, Graphic>::new(
|
NestedMapNode::<_, _, Graphic>::new(
|
||||||
RecordSource::new(
|
RecordSource::new(
|
||||||
GraphicSource {
|
GraphicSource {
|
||||||
layout: layout.clone(),
|
layout: layout.clone(),
|
||||||
@@ -559,7 +568,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flat_map_registers_one_row_per_content_type() {
|
fn flat_map_registers_one_row_per_content_type() {
|
||||||
let entries = _flat_map_mod::flat_map_entries();
|
let entries = crate::graphic::map_entries();
|
||||||
assert_eq!(entries.len(), 6, "one registry row per content implementation");
|
assert_eq!(entries.len(), 6, "one registry row per content implementation");
|
||||||
let content_types: Vec<core_types::Type> = entries.iter().map(|entry| entry.io.inputs[0].clone()).collect();
|
let content_types: Vec<core_types::Type> = entries.iter().map(|entry| entry.io.inputs[0].clone()).collect();
|
||||||
assert_eq!(content_types[0], core_types::registry::record_source_type::<Graphic>());
|
assert_eq!(content_types[0], core_types::registry::record_source_type::<Graphic>());
|
||||||
@@ -580,7 +589,7 @@ mod tests {
|
|||||||
|
|
||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let node = install(
|
let node = install(
|
||||||
FlatMapNode::<_, _, Graphic>::new(
|
MapNode::<_, _, Graphic>::new(
|
||||||
RecordSource::new(
|
RecordSource::new(
|
||||||
GraphicSource {
|
GraphicSource {
|
||||||
layout: layout.clone(),
|
layout: layout.clone(),
|
||||||
@@ -710,8 +719,8 @@ mod tests {
|
|||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
||||||
let node = install(
|
let node = install(
|
||||||
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
||||||
wrap_layout_meta(),
|
wrap_graphic_layout_meta(),
|
||||||
&[Some(&layout)],
|
&[Some(&layout)],
|
||||||
);
|
);
|
||||||
let out = Node::<ContextImpl>::layout(&node).clone();
|
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||||
@@ -748,8 +757,8 @@ mod tests {
|
|||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
||||||
let node = install(
|
let node = install(
|
||||||
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
||||||
wrap_layout_meta(),
|
wrap_graphic_layout_meta(),
|
||||||
&[Some(&layout)],
|
&[Some(&layout)],
|
||||||
);
|
);
|
||||||
let out = Node::<ContextImpl>::layout(&node).clone();
|
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||||
@@ -821,7 +830,12 @@ mod tests {
|
|||||||
|
|
||||||
let layout = Layout::default().with_writes(1, record::element_write_hashed::<Color>(), &[]);
|
let layout = Layout::default().with_writes(1, record::element_write_hashed::<Color>(), &[]);
|
||||||
let out = Layout::default().with_writes(0, record::element_write_hashed::<Gradient>(), &[]);
|
let out = Layout::default().with_writes(0, record::element_write_hashed::<Gradient>(), &[]);
|
||||||
let build = |colors: Vec<Color>| install_flip(ToGradientNode::new(RecordSource::new(ColorSource { layout: layout.clone(), colors }, &layout, &layout), &layout), &out);
|
let build = |colors: Vec<Color>| {
|
||||||
|
install_flip(
|
||||||
|
ColorsToGradientNode::new(RecordSource::new(ColorSource { layout: layout.clone(), colors }, &layout, &layout), &layout),
|
||||||
|
&out,
|
||||||
|
)
|
||||||
|
};
|
||||||
let stops_of = |colors: Vec<Color>| {
|
let stops_of = |colors: Vec<Color>| {
|
||||||
let node = build(colors);
|
let node = build(colors);
|
||||||
let GPoll::Final(record) = record::capture(&node, &ctx, &frames) else {
|
let GPoll::Final(record) = record::capture(&node, &ctx, &frames) else {
|
||||||
@@ -852,8 +866,8 @@ mod tests {
|
|||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
||||||
let node = install(
|
let node = install(
|
||||||
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
||||||
wrap_layout_meta(),
|
wrap_graphic_layout_meta(),
|
||||||
&[Some(&layout)],
|
&[Some(&layout)],
|
||||||
);
|
);
|
||||||
let out = Node::<ContextImpl>::layout(&node).clone();
|
let out = Node::<ContextImpl>::layout(&node).clone();
|
||||||
@@ -884,8 +898,8 @@ mod tests {
|
|||||||
let layout = graphic_layout();
|
let layout = graphic_layout();
|
||||||
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
|
||||||
let wrapped = install(
|
let wrapped = install(
|
||||||
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
|
||||||
wrap_layout_meta(),
|
wrap_graphic_layout_meta(),
|
||||||
&[Some(&layout)],
|
&[Some(&layout)],
|
||||||
);
|
);
|
||||||
let wrap_out = Node::<ContextImpl>::layout(&wrapped).clone();
|
let wrap_out = Node::<ContextImpl>::layout(&wrapped).clone();
|
||||||
@@ -920,6 +934,45 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [Color a, G[Color b (opacity 0.5), Text], Text]: two color leaves, the
|
||||||
|
/// nested one composing G's transform and its own opacity; the texts drop.
|
||||||
|
#[test]
|
||||||
|
fn flatten_color_keeps_only_color_leaves_and_composes_the_path() {
|
||||||
|
let frames = core_types::record::test_frames(1 << 16);
|
||||||
|
let arena = Arena::new(1 << 16).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let nested = {
|
||||||
|
let Graphic::Graphic(mut children) = group(vec![(Graphic::Color(Color::WHITE), translation(20.)), (text("x"), translation(300.))]) else {
|
||||||
|
unreachable!("group builds a legacy graphic list");
|
||||||
|
};
|
||||||
|
children.set_attribute(core_types::ATTR_OPACITY, 0, 0.5);
|
||||||
|
Graphic::Graphic(children)
|
||||||
|
};
|
||||||
|
let rows = vec![(Graphic::Color(Color::BLACK), translation(1.)), (nested, translation(0.5)), (text("y"), translation(9.))];
|
||||||
|
let layout = graphic_layout();
|
||||||
|
let node = install(
|
||||||
|
FlattenColorNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout)),
|
||||||
|
flatten_color_layout_meta(),
|
||||||
|
&[Some(&layout)],
|
||||||
|
);
|
||||||
|
assert_eq!(node.extent_at(&ctx, 0, &frames.reborrow()), GPoll::Final(Extent::Exactly(2)));
|
||||||
|
|
||||||
|
let head = ctx.index_head();
|
||||||
|
let expected = [(Color::BLACK, 1., 1.), (Color::WHITE, 20.5, 0.5)];
|
||||||
|
for (lane, &(color, x, opacity)) in expected.iter().enumerate() {
|
||||||
|
let GPoll::Final(record) = record::capture(&node, &ctx.promoted(&head, lane as u64), &frames) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
assert_eq!(record.element::<Color>(), color, "lane {lane}");
|
||||||
|
let transform: DAffine2 = record.attr::<Transform>();
|
||||||
|
assert_eq!(transform.translation.x, x, "lane {lane}");
|
||||||
|
assert_eq!(record.attr::<Opacity>(), opacity, "lane {lane}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flatten_fully_composes_the_path() {
|
fn flatten_fully_composes_the_path() {
|
||||||
let frames = core_types::record::test_frames(1 << 16);
|
let frames = core_types::record::test_frames(1 << 16);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use canvas_utils::{Canvas, CanvasHandle};
|
|||||||
use core_types::attribute::{Attr, OwnedAttr, Transform};
|
use core_types::attribute::{Attr, OwnedAttr, Transform};
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::gpoll::GPoll;
|
use core_types::gpoll::GPoll;
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
@@ -218,7 +219,7 @@ async fn rasterize<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(
|
|||||||
mut canvas: CanvasHandle,
|
mut canvas: CanvasHandle,
|
||||||
) -> (Raster<CPU>, Attr<Transform>, OwnedAttr<EditorMergedLayers>)
|
) -> (Raster<CPU>, Attr<Transform>, OwnedAttr<EditorMergedLayers>)
|
||||||
where
|
where
|
||||||
List<T>: Render + Clone + graphic_types::IntoGraphicList,
|
List<T>: Render + Clone + IntoGraphicList,
|
||||||
{
|
{
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
@@ -310,10 +311,6 @@ pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_ap
|
|||||||
|
|
||||||
/// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it.
|
/// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it.
|
||||||
#[node_macro::node(category("Debug"), memoize)]
|
#[node_macro::node(category("Debug"), memoize)]
|
||||||
pub fn upload_texture<T: Convert<List<Raster<GPU>>, ::wgpu_executor::WgpuExecutorHandle>>(
|
pub fn upload_texture(_: impl Ctx, content: Raster<CPU>, #[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle) -> Raster<GPU> {
|
||||||
_: impl Ctx,
|
|
||||||
#[implementations(List<Raster<CPU>>)] content: T,
|
|
||||||
#[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle,
|
|
||||||
) -> List<Raster<GPU>> {
|
|
||||||
content.convert(Footprint::DEFAULT, executor)
|
content.convert(Footprint::DEFAULT, executor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use core_types::gpoll::Interrupt;
|
use core_types::gpoll::Interrupt;
|
||||||
use core_types::list::List;
|
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractIndex, ExtractVarArgs, InjectIndex, VarArgLink, VarArgSlots, WasmNotSend};
|
use core_types::{Color, Ctx, DeriveCtx, ExtractFootprint, ExtractIndex, ExtractVarArgs, InjectIndex, VarArgLink, VarArgSlots};
|
||||||
use graph_craft::document::value::{RenderOutput, RenderOutputType};
|
use graph_craft::document::value::{RenderOutput, RenderOutputType};
|
||||||
use graphene_application_io::{ExportFormat, RenderConfig};
|
use graphene_application_io::{ExportFormat, RenderConfig};
|
||||||
use graphic_types::raster_types::{CPU, Raster};
|
use graphic_types::raster_types::{CPU, Raster};
|
||||||
@@ -51,34 +50,9 @@ fn intermediate_of<R: Render>(data: &R, render_params: &RenderParams) -> RenderI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The input's records materialize into a run, which renders directly.
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
fn render_intermediate<T: dyn_any::StaticTypeSized + 'static + Render + WasmNotSend + Send + Sync>(
|
fn render_intermediate<T: Clone + Send + Sync + core_types::CacheHash + dyn_any::StaticTypeSized + 'static>(
|
||||||
ctx: impl Ctx + ExtractVarArgs + DeriveCtx,
|
|
||||||
#[implementations(
|
|
||||||
Context -> List<Artboard>,
|
|
||||||
Context -> List<Graphic>,
|
|
||||||
Context -> List<Vector>,
|
|
||||||
Context -> List<Raster<CPU>>,
|
|
||||||
Context -> List<Color>,
|
|
||||||
Context -> List<Gradient>,
|
|
||||||
Context -> List<String>,
|
|
||||||
)]
|
|
||||||
data: impl Node<Context<'_>, Output = T>,
|
|
||||||
) -> Result<RenderIntermediate, Interrupt> {
|
|
||||||
let data = data.eval(&ctx.derived())?;
|
|
||||||
let render_params = ctx
|
|
||||||
.vararg(0)
|
|
||||||
.expect("Did not find var args")
|
|
||||||
.downcast_ref::<RenderParams>()
|
|
||||||
.expect("Downcasting render params yielded invalid type");
|
|
||||||
|
|
||||||
Ok(intermediate_of(&data, render_params))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The leveled form of `render_intermediate`: the input's records materialize
|
|
||||||
/// into a run, which renders directly.
|
|
||||||
#[node_macro::node(category(""))]
|
|
||||||
fn render_intermediate_leveled<T: Clone + Send + Sync + core_types::CacheHash + dyn_any::StaticTypeSized + 'static>(
|
|
||||||
ctx: impl Ctx + ExtractVarArgs + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + ExtractVarArgs + ExtractIndex + InjectIndex + Copy,
|
||||||
#[implementations(Artboard, Graphic, Vector, Raster<CPU>, Color, Gradient, String)] data: IList<T>,
|
#[implementations(Artboard, Graphic, Vector, Raster<CPU>, Color, Gradient, String)] data: IList<T>,
|
||||||
) -> Result<RenderIntermediate, Interrupt>
|
) -> Result<RenderIntermediate, Interrupt>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
|
use core_types::attribute::{Attr, FontSize, LetterSpacing, LetterTilt, LineHeight, MaxHeight, MaxWidth, Transform as TransformAttr};
|
||||||
|
use core_types::extent::{LevelIn, ListIn};
|
||||||
|
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::{ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, Ctx};
|
use core_types::node::{Lane, RecordLane};
|
||||||
|
use core_types::{ATTR_TRANSFORM, Ctx, ExtractIndex, InjectIndex};
|
||||||
|
use glam::DAffine2;
|
||||||
use graph_craft::application_io::resource::Resource;
|
use graph_craft::application_io::resource::Resource;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
|
use text_nodes::markers::{Font as FontAttr, TextAlign as TextAlignAttr};
|
||||||
pub use text_nodes::*;
|
pub use text_nodes::*;
|
||||||
|
|
||||||
/// Produces a styled `String[]` carrying all typographic attributes.
|
/// Produces a styled `String` carrying all typographic attributes.
|
||||||
///
|
///
|
||||||
/// Use the **Text to Vector** node to convert this into vector geometry if desired.
|
/// Use the **Text to Vector** node to convert this into vector geometry if desired.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn text(
|
fn text<'e>(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractArena<'e>,
|
||||||
_primary: (),
|
_primary: (),
|
||||||
/// The text content to be drawn.
|
/// The text content to be drawn.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
||||||
@@ -59,55 +64,146 @@ fn text(
|
|||||||
/// The horizontal alignment of each line of text within its surrounding box. To have an effect on a single line of text, *Max Width* must be set.
|
/// The horizontal alignment of each line of text within its surrounding box. To have an effect on a single line of text, *Max Width* must be set.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_align")]
|
#[widget(ParsedWidgetOverride::Custom = "text_align")]
|
||||||
align: TextAlign,
|
align: TextAlign,
|
||||||
) -> List<String> {
|
) -> Result<
|
||||||
let mut list = List::new_from_element(text);
|
(
|
||||||
|
String,
|
||||||
if font != Resource::default() {
|
Attr<'e, FontAttr>,
|
||||||
list.set_attribute(ATTR_FONT, 0, font);
|
Attr<'e, FontSize>,
|
||||||
}
|
Attr<'e, LineHeight>,
|
||||||
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON {
|
Attr<'e, LetterSpacing>,
|
||||||
list.set_attribute(ATTR_FONT_SIZE, 0, size);
|
Attr<'e, LetterTilt>,
|
||||||
}
|
Attr<'e, MaxWidth>,
|
||||||
if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON {
|
Attr<'e, MaxHeight>,
|
||||||
list.set_attribute(ATTR_LINE_HEIGHT, 0, line_height);
|
Attr<'e, TextAlignAttr>,
|
||||||
}
|
),
|
||||||
if letter_spacing != 0. {
|
Interrupt,
|
||||||
list.set_attribute(ATTR_LETTER_SPACING, 0, letter_spacing);
|
> {
|
||||||
}
|
let (font, _) = ctx.arena().alloc(font).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?;
|
||||||
if letter_tilt != 0. {
|
Ok((
|
||||||
list.set_attribute(ATTR_LETTER_TILT, 0, letter_tilt);
|
text,
|
||||||
}
|
Attr(font),
|
||||||
if has_max_width {
|
Attr(size),
|
||||||
list.set_attribute(ATTR_MAX_WIDTH, 0, Some(max_width));
|
Attr(line_height),
|
||||||
}
|
Attr(letter_spacing),
|
||||||
if has_max_height {
|
Attr(letter_tilt),
|
||||||
list.set_attribute(ATTR_MAX_HEIGHT, 0, Some(max_height));
|
Attr(has_max_width.then_some(max_width)),
|
||||||
}
|
Attr(has_max_height.then_some(max_height)),
|
||||||
if align != TextAlign::default() {
|
Attr(align),
|
||||||
list.set_attribute(ATTR_TEXT_ALIGN, 0, align);
|
))
|
||||||
}
|
|
||||||
|
|
||||||
list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts styled text into vector compound paths.
|
/// Shapes one styled string lane into vector geometry, the font and
|
||||||
#[node_macro::node(category("Text"), name("Text to Vector"))]
|
/// typesetting read from the lane's columns. The paths keep their glyph-local
|
||||||
fn text_to_vector(
|
/// transforms; the lane's own transform composes on at emit.
|
||||||
_: impl Ctx,
|
fn shape_lane(lane: &RecordLane<'_>, text: &str, separate_glyphs: bool) -> List<Vector> {
|
||||||
/// A styled list of text strings produced by the **Text** node (or any other `String[]` source).
|
if text.is_empty() {
|
||||||
#[implementations(List<String>)]
|
return List::new();
|
||||||
strings: List<String>,
|
}
|
||||||
) -> List<Vector> {
|
let font = lane.attr::<FontAttr>();
|
||||||
shape_text_list(&strings, false)
|
let font = match font.is_empty() {
|
||||||
|
true => &FALLBACK_FONT_RESOURCE,
|
||||||
|
false => font,
|
||||||
|
};
|
||||||
|
let typesetting = TypesettingConfig {
|
||||||
|
font_size: lane.attr::<FontSize>(),
|
||||||
|
line_height_ratio: lane.attr::<LineHeight>(),
|
||||||
|
letter_spacing: lane.attr::<LetterSpacing>(),
|
||||||
|
letter_tilt: lane.attr::<LetterTilt>(),
|
||||||
|
max_width: lane.attr::<MaxWidth>(),
|
||||||
|
max_height: lane.attr::<MaxHeight>(),
|
||||||
|
align: lane.attr::<TextAlignAttr>(),
|
||||||
|
};
|
||||||
|
to_path(text, font, typesetting, separate_glyphs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits styled text into a separate vector item for each of its glyphs (letterforms).
|
/// The shaped paths of every string lane, valid for one key and generation,
|
||||||
#[node_macro::node(category("Text"), name("Text to Vector Glyphs"))]
|
/// so addressing the level's lanes shapes each string once.
|
||||||
fn text_to_vector_glyphs(
|
#[derive(Debug, Default)]
|
||||||
_: impl Ctx,
|
pub struct ShapedRows {
|
||||||
/// A styled list of text strings produced by the **Text** node (or any other `String[]` source).
|
key: u64,
|
||||||
#[implementations(List<String>)]
|
generation: u64,
|
||||||
strings: List<String>,
|
rows: Vec<List<Vector>>,
|
||||||
) -> List<Vector> {
|
}
|
||||||
shape_text_list(&strings, true)
|
|
||||||
|
type ShapedCache = std::sync::Arc<std::sync::Mutex<Option<ShapedRows>>>;
|
||||||
|
|
||||||
|
/// The lane-normalized cache key and arena generation of one evaluation.
|
||||||
|
macro_rules! eval_key {
|
||||||
|
($ctx:expr) => {{
|
||||||
|
let mut keyed = *$ctx;
|
||||||
|
InjectIndex::set_index(&mut keyed, 0);
|
||||||
|
(core_types::registry::cache_key(&keyed), $ctx.arena().generation())
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `lane`-th path over all the strings' shaped rows, carrying its
|
||||||
|
/// string's columns with the composed transform overriding. `key` and
|
||||||
|
/// `generation` scope the cache to one evaluation.
|
||||||
|
fn shaped_lane<'a, 'e>(
|
||||||
|
strings: core_types::node::List<'a, String>,
|
||||||
|
lane: usize,
|
||||||
|
(key, generation): (u64, u64),
|
||||||
|
cache: &ShapedCache,
|
||||||
|
separate_glyphs: bool,
|
||||||
|
) -> Result<(Lane<'a, Vector>, Attr<'e, TransformAttr>), Interrupt> {
|
||||||
|
let mut cached = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if !matches!(cached.as_ref(), Some(entry) if entry.key == key && entry.generation == generation) {
|
||||||
|
let rows = (0..strings.len()).map(|row| shape_lane(&strings.lane(row), strings.element_ref(row), separate_glyphs)).collect();
|
||||||
|
*cached = Some(ShapedRows { key, generation, rows });
|
||||||
|
}
|
||||||
|
let rows = &cached.as_ref().expect("populated above").rows;
|
||||||
|
|
||||||
|
let mut remaining = lane;
|
||||||
|
for (row, shaped) in rows.iter().enumerate() {
|
||||||
|
if remaining >= shaped.len() {
|
||||||
|
remaining -= shaped.len();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let element = shaped.element(remaining).cloned().unwrap_or_default();
|
||||||
|
let local: DAffine2 = shaped.attribute_cloned_or_default(ATTR_TRANSFORM, remaining);
|
||||||
|
let carrier = strings.lane(row);
|
||||||
|
let transform = carrier.attr::<TransformAttr>() * local;
|
||||||
|
return Ok((carrier.map_element(element), Attr(transform)));
|
||||||
|
}
|
||||||
|
Err(GraphError::past_end().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level holds every string's shaped paths in order.
|
||||||
|
fn shaped_extent(strings: ListIn<'_, String>, level: LevelIn, separate_glyphs: bool) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => strings
|
||||||
|
.get()
|
||||||
|
.map(|strings| Extent::Exactly((0..strings.len()).map(|row| shape_lane(&strings.lane(row), strings.element_ref(row), separate_glyphs).len()).sum())),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts styled text into vector compound paths, one per string.
|
||||||
|
#[node_macro::node(category("Text"), name("Text to Vector"), extent(text_to_vector_extent))]
|
||||||
|
fn text_to_vector<'e>(
|
||||||
|
ctx: impl Ctx + core_types::CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
/// Styled strings produced by the **Text** node (or any other `String` source).
|
||||||
|
strings: IList<String>,
|
||||||
|
#[data] shaped: ShapedCache,
|
||||||
|
) -> Result<IList<(Lane<Vector>, Attr<'e, TransformAttr>)>, Interrupt> {
|
||||||
|
shaped_lane(strings, ctx.index() as usize, eval_key!(ctx), shaped, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_to_vector_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
shaped_extent(strings, level, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits styled text into a separate vector path for each of its glyphs (letterforms).
|
||||||
|
#[node_macro::node(category("Text"), name("Text to Vector Glyphs"), extent(text_to_vector_glyphs_extent))]
|
||||||
|
fn text_to_vector_glyphs<'e>(
|
||||||
|
ctx: impl Ctx + core_types::CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
|
/// Styled strings produced by the **Text** node (or any other `String` source).
|
||||||
|
strings: IList<String>,
|
||||||
|
#[data] shaped: ShapedCache,
|
||||||
|
) -> Result<IList<(Lane<Vector>, Attr<'e, TransformAttr>)>, Interrupt> {
|
||||||
|
shaped_lane(strings, ctx.index() as usize, eval_key!(ctx), shaped, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_to_vector_glyphs_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
shaped_extent(strings, level, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use core_types::list::{Item, List};
|
use crate::{expanded_count, locate_expanded, unescape_string};
|
||||||
use core_types::{ATTR_TYPE, Ctx};
|
use core_types::attribute::{Attr, Type};
|
||||||
|
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||||
|
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||||
|
use core_types::node::Lane;
|
||||||
|
use core_types::{Ctx, ExtractIndex, InjectIndex};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::unescape_string;
|
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// Format JSON
|
// Format JSON
|
||||||
// ===========
|
// ===========
|
||||||
@@ -221,12 +223,12 @@ fn query_json(
|
|||||||
/// • **Index Elements**: access the `N`th query result.
|
/// • **Index Elements**: access the `N`th query result.
|
||||||
/// • **String to Number**: convert numeric query results to numbers.
|
/// • **String to Number**: convert numeric query results to numbers.
|
||||||
/// • **String Value** → **Equals**: convert "true", "false", or "null" query results to bools.
|
/// • **String Value** → **Equals**: convert "true", "false", or "null" query results to bools.
|
||||||
#[node_macro::node(name("Query JSON All"), category("Text: JSON"))]
|
#[node_macro::node(name("Query JSON All"), category("Text: JSON"), extent(query_json_all_extent))]
|
||||||
fn query_json_all(
|
fn query_json_all<'e>(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
/// The JSON string to extract values from.
|
/// The JSON strings to extract values from.
|
||||||
#[name("JSON")]
|
#[name("JSON")]
|
||||||
json: String,
|
json: IList<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.
|
||||||
@@ -240,15 +242,33 @@ fn query_json_all(
|
|||||||
/// 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: bool,
|
||||||
) -> List<String> {
|
) -> Result<IList<(Lane<String>, Attr<'e, Type>)>, Interrupt> {
|
||||||
let cleaned = strip_trailing_commas(&json);
|
let (row, (text, ty)) = locate_expanded(json, ctx.index() as usize, |json| query_all(json, &path, unquote_strings)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
|
||||||
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return List::new() };
|
Ok((json.lane(row).map_element(text), Attr(ty)))
|
||||||
let Some(segments) = parse_json_path(path.trim()) else { return List::new() };
|
}
|
||||||
|
|
||||||
|
/// Every value `path` matches in `json` with its JSON type, none for invalid
|
||||||
|
/// JSON or an invalid path.
|
||||||
|
fn query_all(json: &str, path: &str, unquote_strings: bool) -> Vec<(String, &'static str)> {
|
||||||
|
let cleaned = strip_trailing_commas(json);
|
||||||
|
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return Vec::new() };
|
||||||
|
let Some(segments) = parse_json_path(path.trim()) else { return Vec::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, &mut results);
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect()
|
/// The level holds every string's matched values in order.
|
||||||
|
fn query_json_all_extent(json: ListIn<'_, String>, path: ValueIn<'_, String>, unquote_strings: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => json
|
||||||
|
.get()
|
||||||
|
.zip(path.get())
|
||||||
|
.zip(unquote_strings.get())
|
||||||
|
.map(|((json, path), unquote_strings)| expanded_count(json, |json| query_all(json, &path, unquote_strings).len())),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A parsed segment of a JSON access path.
|
/// A parsed segment of a JSON access path.
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ mod text_context;
|
|||||||
mod to_path;
|
mod to_path;
|
||||||
|
|
||||||
use convert_case::{Boundary, Converter, pattern};
|
use convert_case::{Boundary, Converter, pattern};
|
||||||
use core_types::gpoll::Interrupt;
|
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||||
|
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||||
use core_types::graphene_hash::CacheHash;
|
use core_types::graphene_hash::CacheHash;
|
||||||
use core_types::list::{Item, List};
|
use core_types::node::Lane;
|
||||||
use core_types::registry::types::{SignedInteger, TextArea};
|
use core_types::registry::types::{SignedInteger, TextArea};
|
||||||
use core_types::{Context, Ctx, DeriveCtx, ExtractVarArgs};
|
use core_types::{Ctx, ExtractIndex, InjectIndex};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
@@ -361,7 +362,7 @@ fn format_number(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -727,14 +728,43 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
|
|||||||
string.graphemes(true).count() as f64
|
string.graphemes(true).count() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a string into a list of substrings based on the specified delimiter. This is the inverse of the **String Join** node.
|
/// The `lane`-th row of the level made by expanding every string in order,
|
||||||
|
/// with the row of the string it came from.
|
||||||
|
pub(crate) fn locate_expanded<R>(strings: core_types::node::List<'_, String>, lane: usize, expand: impl Fn(&str) -> Vec<R>) -> Option<(usize, R)> {
|
||||||
|
let mut remaining = lane;
|
||||||
|
for row in 0..strings.len() {
|
||||||
|
let mut expanded = expand(strings.element_ref(row));
|
||||||
|
if remaining >= expanded.len() {
|
||||||
|
remaining -= expanded.len();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Some((row, expanded.swap_remove(remaining)));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rows every string expands to, summed.
|
||||||
|
pub(crate) fn expanded_count(strings: core_types::node::List<'_, String>, expand: impl Fn(&str) -> usize) -> Extent {
|
||||||
|
Extent::Exactly((0..strings.len()).map(|row| expand(strings.element_ref(row))).sum())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parts of `string` around `delimiter`, unescaped when asked.
|
||||||
|
fn split_parts(string: &str, delimiter: &str, delimiter_escaping: bool) -> Vec<String> {
|
||||||
|
let delimiter = match delimiter_escaping {
|
||||||
|
true => unescape_string(delimiter.to_string()),
|
||||||
|
false => delimiter.to_string(),
|
||||||
|
};
|
||||||
|
string.split(&delimiter).map(str::to_string).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits each string into substrings based on the specified delimiter, producing one flat list of all the substrings. This is the inverse of the **String Join** node.
|
||||||
///
|
///
|
||||||
/// For example, splitting "a, b, c" with delimiter ", " produces `["a", "b", "c"]`.
|
/// For example, splitting "a, b, c" with delimiter ", " produces `["a", "b", "c"]`.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"), extent(string_split_extent))]
|
||||||
fn string_split(
|
fn string_split(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||||
/// The string to split into substrings.
|
/// The strings to split into substrings.
|
||||||
string: String,
|
strings: IList<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: String,
|
||||||
@@ -742,10 +772,21 @@ fn string_split(
|
|||||||
/// "\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: bool,
|
||||||
) -> List<String> {
|
) -> Result<IList<Lane<String>>, Interrupt> {
|
||||||
let delimiter = if delimiter_escaping { unescape_string(delimiter) } else { delimiter };
|
let (row, part) = locate_expanded(strings, ctx.index() as usize, |string| split_parts(string, &delimiter, delimiter_escaping)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
|
||||||
|
Ok(strings.lane(row).map_element(part))
|
||||||
|
}
|
||||||
|
|
||||||
string.split(&delimiter).map(str::to_string).map(Item::new_from_element).collect()
|
/// The level holds every string's parts in order.
|
||||||
|
fn string_split_extent(strings: ListIn<'_, String>, delimiter: ValueIn<'_, String>, delimiter_escaping: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => strings
|
||||||
|
.get()
|
||||||
|
.zip(delimiter.get())
|
||||||
|
.zip(delimiter_escaping.get())
|
||||||
|
.map(|((strings, delimiter), escaping)| expanded_count(strings, |string| split_parts(string, &delimiter, escaping).len())),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -755,7 +796,7 @@ fn string_split(
|
|||||||
fn string_join(
|
fn string_join(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The list of strings to join together.
|
/// The list of strings to join together.
|
||||||
strings: List<String>,
|
strings: IList<String>,
|
||||||
/// The text placed between each pair of strings.
|
/// The text placed between each pair of strings.
|
||||||
#[default(", ")]
|
#[default(", ")]
|
||||||
separator: String,
|
separator: String,
|
||||||
@@ -766,39 +807,7 @@ fn string_join(
|
|||||||
) -> String {
|
) -> String {
|
||||||
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)
|
(0..strings.len()).map(|row| strings.element_ref(row).as_str()).collect::<Vec<_>>().join(&separator)
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
#[node_macro::node(category("Text"))]
|
|
||||||
fn map_string(
|
|
||||||
ctx: impl Ctx + DeriveCtx,
|
|
||||||
strings: List<String>,
|
|
||||||
#[expose]
|
|
||||||
#[implementations(Context -> String)]
|
|
||||||
mapped: impl Node<Context<'_>, Output = String>,
|
|
||||||
) -> Result<List<String>, Interrupt> {
|
|
||||||
let spilled = ctx.index_head();
|
|
||||||
let mut result = List::new();
|
|
||||||
|
|
||||||
for (i, row) in strings.into_iter().enumerate() {
|
|
||||||
let string = row.into_element();
|
|
||||||
let scoped = ctx.push_vararg(&string);
|
|
||||||
let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
|
|
||||||
|
|
||||||
result.push(Item::new_from_element(mapped_string));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the current string from within a **Map String** node's loop.
|
|
||||||
#[node_macro::node(category("Context"))]
|
|
||||||
fn read_string(ctx: impl Ctx + ExtractVarArgs) -> String {
|
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return String::new() };
|
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
|
||||||
|
|
||||||
var_arg.downcast_ref::<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);
|
||||||
|
|||||||
+182
-120
@@ -1,6 +1,10 @@
|
|||||||
use core_types::list::{Item, List};
|
use crate::{expanded_count, locate_expanded};
|
||||||
|
use core_types::attribute::{Attr, End, Name, Start};
|
||||||
|
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||||
|
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||||
|
use core_types::node::Lane;
|
||||||
use core_types::registry::types::SignedInteger;
|
use core_types::registry::types::SignedInteger;
|
||||||
use core_types::{ATTR_END, ATTR_NAME, ATTR_START, Ctx};
|
use core_types::{Ctx, ExtractIndex, InjectIndex};
|
||||||
|
|
||||||
/// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string.
|
/// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string.
|
||||||
#[node_macro::node(category("Text: Regex"))]
|
#[node_macro::node(category("Text: Regex"))]
|
||||||
@@ -77,17 +81,101 @@ fn regex_replace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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).
|
/// The pattern with its flag prefix compiled, or nothing for an empty or
|
||||||
|
/// invalid pattern (the latter logged).
|
||||||
|
fn compile_regex(pattern: &str, case_insensitive: bool, multiline: bool) -> Option<fancy_regex::Regex> {
|
||||||
|
if pattern.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let flags = match (case_insensitive, multiline) {
|
||||||
|
(false, false) => "",
|
||||||
|
(true, false) => "(?i)",
|
||||||
|
(false, true) => "(?m)",
|
||||||
|
(true, true) => "(?im)",
|
||||||
|
};
|
||||||
|
match fancy_regex::Regex::new(&format!("{flags}{pattern}")) {
|
||||||
|
Ok(regex) => Some(regex),
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("Invalid regex pattern: {pattern}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One matched substring with its byte range in the searched string and, for
|
||||||
|
/// a capture, the group's name.
|
||||||
|
struct Span {
|
||||||
|
text: String,
|
||||||
|
start: u64,
|
||||||
|
end: u64,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The whole match then each capture group of the `match_index`-th match,
|
||||||
|
/// empty where the index resolves to no match.
|
||||||
|
fn capture_spans(regex: &fancy_regex::Regex, string: &str, match_index: f64) -> Vec<Span> {
|
||||||
|
// Capture group names indexed positionally; index 0 (the whole match) is always None.
|
||||||
|
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
|
||||||
|
let matches: Vec<_> = regex.captures_iter(string).filter_map(|c| c.ok()).collect();
|
||||||
|
let match_index = match_index as i32;
|
||||||
|
let resolved_index = match match_index < 0 {
|
||||||
|
true => match matches.len().checked_sub((-match_index) as usize) {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return Vec::new(),
|
||||||
|
},
|
||||||
|
false => match_index as usize,
|
||||||
|
};
|
||||||
|
let Some(captures) = matches.get(resolved_index) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
(0..captures.len())
|
||||||
|
.map(|i| {
|
||||||
|
let captured = captures.get(i);
|
||||||
|
Span {
|
||||||
|
text: captured.map_or(String::new(), |m| m.as_str().to_string()),
|
||||||
|
start: captured.map_or(0, |m| m.start() as u64),
|
||||||
|
end: captured.map_or(0, |m| m.end() as u64),
|
||||||
|
name: capture_names.get(i).cloned().flatten().unwrap_or_default(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_spans(regex: &fancy_regex::Regex, string: &str) -> Vec<Span> {
|
||||||
|
regex
|
||||||
|
.find_iter(string)
|
||||||
|
.filter_map(|m| m.ok())
|
||||||
|
.map(|m| Span {
|
||||||
|
text: m.as_str().to_string(),
|
||||||
|
start: m.start() as u64,
|
||||||
|
end: m.end() as u64,
|
||||||
|
name: String::new(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parts of `string` between matches, the whole string without a usable pattern.
|
||||||
|
fn split_parts(regex: Option<&fancy_regex::Regex>, string: &str) -> Vec<String> {
|
||||||
|
match regex {
|
||||||
|
Some(regex) => regex.split(string).filter_map(|s| s.ok()).map(str::to_string).collect(),
|
||||||
|
None => vec![string.to_string()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a regex match in each string and returns its components, as one flat list where a match contributes the whole match (`$0`) followed by its capture groups (`$1`, `$2`, etc., if any).
|
||||||
///
|
///
|
||||||
/// The match index selects which non-overlapping occurrence to return (0 for the first match). Returns an empty list if no match is found at the given index.
|
/// The match index selects which non-overlapping occurrence to return (0 for the first match). A string contributes nothing if no match is found at the given index.
|
||||||
///
|
///
|
||||||
/// Each item carries `start` and `end` byte-offset attributes pointing into the original string, plus a `name` attribute holding
|
/// Each item carries `start` and `end` byte-offset attributes pointing into its original string, plus a `name` attribute holding
|
||||||
/// the capture group's name (empty for unnamed groups, and for index 0 which is the whole match).
|
/// the capture group's name (empty for unnamed groups, and for index 0 which is the whole match).
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""), extent(regex_find_extent))]
|
||||||
fn regex_find(
|
fn regex_find<'e>(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
/// The string to search within.
|
/// The strings to search within.
|
||||||
string: String,
|
strings: IList<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: 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.
|
||||||
@@ -96,135 +184,109 @@ fn regex_find(
|
|||||||
case_insensitive: bool,
|
case_insensitive: 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: bool,
|
||||||
) -> List<String> {
|
) -> Result<IList<(Lane<String>, Attr<'e, Start>, Attr<'e, End>, Attr<'e, Name>)>, Interrupt> {
|
||||||
if pattern.is_empty() {
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
return List::new();
|
let (row, span) = locate_expanded(strings, ctx.index() as usize, |string| {
|
||||||
}
|
regex.as_ref().map_or_else(Vec::new, |regex| capture_spans(regex, string, match_index))
|
||||||
|
})
|
||||||
let flags = match (case_insensitive, multiline) {
|
.ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
|
||||||
(false, false) => "",
|
let (name, _) = ctx.arena().alloc(span.name).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?;
|
||||||
(true, false) => "(?i)",
|
Ok((strings.lane(row).map_element(span.text), Attr(span.start), Attr(span.end), Attr(name.as_str())))
|
||||||
(false, true) => "(?m)",
|
|
||||||
(true, true) => "(?im)",
|
|
||||||
};
|
|
||||||
let full_pattern = format!("{flags}{pattern}");
|
|
||||||
|
|
||||||
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
|
|
||||||
log::error!("Invalid regex pattern: {pattern}");
|
|
||||||
return List::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Capture group names indexed positionally; index 0 (the whole match) is always None.
|
|
||||||
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
|
|
||||||
let matches: Vec<_> = regex.captures_iter(&string).filter_map(|c| c.ok()).collect();
|
|
||||||
|
|
||||||
let match_index = match_index as i32;
|
|
||||||
let resolved_index = if match_index < 0 {
|
|
||||||
let from_end = (-match_index) as usize;
|
|
||||||
if from_end > matches.len() {
|
|
||||||
return List::new();
|
|
||||||
}
|
|
||||||
matches.len() - from_end
|
|
||||||
} else {
|
|
||||||
match_index as usize
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(captures) = matches.get(resolved_index) else {
|
|
||||||
return List::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Index 0 is the whole match, 1+ are capture groups
|
|
||||||
(0..captures.len())
|
|
||||||
.map(|i| {
|
|
||||||
let captured = captures.get(i);
|
|
||||||
let text = captured.map_or(String::new(), |m| m.as_str().to_string());
|
|
||||||
let start = captured.map_or(0_u64, |m| m.start() as u64);
|
|
||||||
let end = captured.map_or(0_u64, |m| m.end() as u64);
|
|
||||||
let name = capture_names.get(i).cloned().flatten().unwrap_or_default();
|
|
||||||
Item::new_from_element(text)
|
|
||||||
.with_attribute(ATTR_START, start)
|
|
||||||
.with_attribute(ATTR_END, end)
|
|
||||||
.with_attribute(ATTR_NAME, name)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all non-overlapping matches of a regular expression pattern in the string, returning a list of the matched substrings.
|
/// The level holds every string's captures in order.
|
||||||
|
fn regex_find_extent(
|
||||||
|
strings: ListIn<'_, String>,
|
||||||
|
pattern: ValueIn<'_, String>,
|
||||||
|
match_index: ValueIn<'_, f64>,
|
||||||
|
case_insensitive: ValueIn<'_, bool>,
|
||||||
|
multiline: ValueIn<'_, bool>,
|
||||||
|
level: LevelIn,
|
||||||
|
) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => strings
|
||||||
|
.get()
|
||||||
|
.zip(pattern.get())
|
||||||
|
.zip(match_index.get())
|
||||||
|
.zip(case_insensitive.get())
|
||||||
|
.zip(multiline.get())
|
||||||
|
.map(|((((strings, pattern), match_index), case_insensitive), multiline)| {
|
||||||
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
|
expanded_count(strings, |string| regex.as_ref().map_or(0, |regex| capture_spans(regex, string, match_index).len()))
|
||||||
|
}),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds all non-overlapping matches of a regular expression pattern in each string, returning one flat list of the matched substrings.
|
||||||
///
|
///
|
||||||
/// Each item carries `start` and `end` byte-offset attributes pointing into the original string.
|
/// Each item carries `start` and `end` byte-offset attributes pointing into its original string.
|
||||||
#[node_macro::node(category("Text: Regex"))]
|
#[node_macro::node(category("Text: Regex"), extent(regex_find_all_extent))]
|
||||||
fn regex_find_all(
|
fn regex_find_all<'e>(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||||
/// The string to search within.
|
/// The strings to search within.
|
||||||
string: String,
|
strings: IList<String>,
|
||||||
/// The regular expression pattern to search for.
|
/// The regular expression pattern to search for.
|
||||||
pattern: String,
|
pattern: String,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: 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: bool,
|
||||||
) -> List<String> {
|
) -> Result<IList<(Lane<String>, Attr<'e, Start>, Attr<'e, End>)>, Interrupt> {
|
||||||
if pattern.is_empty() {
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
return List::new();
|
let (row, span) =
|
||||||
}
|
locate_expanded(strings, ctx.index() as usize, |string| regex.as_ref().map_or_else(Vec::new, |regex| match_spans(regex, string))).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
|
||||||
|
Ok((strings.lane(row).map_element(span.text), Attr(span.start), Attr(span.end)))
|
||||||
let flags = match (case_insensitive, multiline) {
|
|
||||||
(false, false) => "",
|
|
||||||
(true, false) => "(?i)",
|
|
||||||
(false, true) => "(?m)",
|
|
||||||
(true, true) => "(?im)",
|
|
||||||
};
|
|
||||||
let full_pattern = format!("{flags}{pattern}");
|
|
||||||
|
|
||||||
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
|
|
||||||
log::error!("Invalid regex pattern: {pattern}");
|
|
||||||
return List::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
regex
|
|
||||||
.find_iter(&string)
|
|
||||||
.filter_map(|m| m.ok())
|
|
||||||
.map(|m| {
|
|
||||||
Item::new_from_element(m.as_str().to_string())
|
|
||||||
.with_attribute(ATTR_START, m.start() as u64)
|
|
||||||
.with_attribute(ATTR_END, m.end() as u64)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a string into a list of substrings pulled from between separator characters as matched by a regular expression.
|
/// The level holds every string's matches in order.
|
||||||
|
fn regex_find_all_extent(strings: ListIn<'_, String>, pattern: ValueIn<'_, String>, case_insensitive: ValueIn<'_, bool>, multiline: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => strings
|
||||||
|
.get()
|
||||||
|
.zip(pattern.get())
|
||||||
|
.zip(case_insensitive.get())
|
||||||
|
.zip(multiline.get())
|
||||||
|
.map(|(((strings, pattern), case_insensitive), multiline)| {
|
||||||
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
|
expanded_count(strings, |string| regex.as_ref().map_or(0, |regex| match_spans(regex, string).len()))
|
||||||
|
}),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits each string into substrings pulled from between separator characters as matched by a regular expression, producing one flat list of all the substrings.
|
||||||
///
|
///
|
||||||
/// For example, splitting "Three, two, one... LIFTOFF" with pattern `\W+` (non-word characters) produces `["Three", "two", "one", "LIFTOFF"]`.
|
/// For example, splitting "Three, two, one... LIFTOFF" with pattern `\W+` (non-word characters) produces `["Three", "two", "one", "LIFTOFF"]`.
|
||||||
#[node_macro::node(category("Text: Regex"))]
|
#[node_macro::node(category("Text: Regex"), extent(regex_split_extent))]
|
||||||
fn regex_split(
|
fn regex_split(
|
||||||
_: impl Ctx,
|
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||||
/// The string to split into substrings.
|
/// The strings to split into substrings.
|
||||||
string: String,
|
strings: IList<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: String,
|
||||||
/// Match letters regardless of case.
|
/// Match letters regardless of case.
|
||||||
case_insensitive: bool,
|
case_insensitive: 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: bool,
|
||||||
) -> List<String> {
|
) -> Result<IList<Lane<String>>, Interrupt> {
|
||||||
if pattern.is_empty() {
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
return List::new_from_element(string);
|
let (row, part) = locate_expanded(strings, ctx.index() as usize, |string| split_parts(regex.as_ref(), string)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
|
||||||
|
Ok(strings.lane(row).map_element(part))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level holds every string's parts in order.
|
||||||
|
fn regex_split_extent(strings: ListIn<'_, String>, pattern: ValueIn<'_, String>, case_insensitive: ValueIn<'_, bool>, multiline: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.top() {
|
||||||
|
true => strings
|
||||||
|
.get()
|
||||||
|
.zip(pattern.get())
|
||||||
|
.zip(case_insensitive.get())
|
||||||
|
.zip(multiline.get())
|
||||||
|
.map(|(((strings, pattern), case_insensitive), multiline)| {
|
||||||
|
let regex = compile_regex(&pattern, case_insensitive, multiline);
|
||||||
|
expanded_count(strings, |string| split_parts(regex.as_ref(), string).len())
|
||||||
|
}),
|
||||||
|
false => GPoll::Final(Extent::Exactly(1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
let flags = match (case_insensitive, multiline) {
|
|
||||||
(false, false) => "",
|
|
||||||
(true, false) => "(?i)",
|
|
||||||
(false, true) => "(?m)",
|
|
||||||
(true, true) => "(?im)",
|
|
||||||
};
|
|
||||||
let full_pattern = format!("{flags}{pattern}");
|
|
||||||
|
|
||||||
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
|
|
||||||
log::error!("Invalid regex pattern: {pattern}");
|
|
||||||
return List::new_from_element(string);
|
|
||||||
};
|
|
||||||
|
|
||||||
regex.split(&string).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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1278,10 +1278,10 @@ fn dimensions(_: impl Ctx, content: IList<Vector>) -> DVec2 {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type-asserts a value to be vector data.
|
/// Type-asserts a value to be vector data. A position becomes a single-anchor vector.
|
||||||
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
|
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
|
||||||
fn as_vector(_: impl Ctx, value: Vector) -> Vector {
|
fn as_vector<T: Into<Vector>>(_: impl Ctx, #[implementations(Vector, DVec2)] value: T) -> Vector {
|
||||||
value
|
value.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
|
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
|
||||||
|
|||||||
Reference in New Issue
Block a user