mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 18:18:12 +08:00
Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)
* Add rank polymorphism node audit classifying all 271 nodes
* Implement StaticType for Item<T>
* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input
* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate
* Document the Item kernel implementation and staging plan
* Route Item<Vector> through TaggedValue::TypeDefault
* Add executor integration tests covering the Item and List wire variants
* Collapse element-wise Item/List wire pairs to the List form for conversion insertion
* Migrate sixteen vector modifier nodes to Item element-wise kernels
* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels
* Fix bevel_with_transform test to actually exercise the transform attribute
* Implement From<T> for Item<T>
* Register PromoteNode rank adapters wrapping bare values into Item wires
* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor
* Define a real promote node backing the PromoteNode registry identifiers
* Zip ranked Item connectors by frame slot in the mapped element-wise variant
* Register ItemToListNode singleton raise adapters
* Resolve Item wires against List connectors by inserting promotion adapters at construction
* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end
* Implement Clampable for Item and List wires with per-variant clamp bounds
* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire
* Implement ApplyTransform for Item
* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0
* Detect element-wise nodes by lazy primary connectors declaring Output = Item
* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone
* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2
* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters
* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes
* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro
* Amend the audit with the DashPattern value type resolution
* Migrate the string family to Item element-wise kernels
* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters
* Shadow owned node parameters in bodies instead of mut in signatures
* Migrate the math family and string measure nodes to Item element-wise kernels
* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows
* Flat-map expander kernels returning List under the mapped variant's frame
* Migrate the expander nodes to Item kernels flat-mapping under the frame
* Remove the unused peel_list helper
* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in
Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).
Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.
* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk
* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites
* Flip the vector shape generators to emit rank-0 Item<Vector>
The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.
Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.
* Parse ranked Item<T> parameter defaults against the bare element type
A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.
* Parse an element-wise primary's scalar default against the bare element type
An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.
* Add the DashPattern value type for stroke dash sequences
Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.
* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector
Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.
Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.
Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.
* Register rank adapters for the ranked Stroke enum parameters
The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.
* Display Item wires in the Data panel without a List's ID column
Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.
* Register MonitorNode for Item wire types so the Data panel introspects them directly
Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.
* Color and double Item/List wires and cleave layer-stack connectors in the node graph
* Route wire color and rank through hidden nodes and refresh them on type changes
* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node
* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires
* Back DashPattern with a List<f64> so the Data panel can introspect its lengths
* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>
* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions
* Rank the remaining bare and unnecessarily-List connectors across the node catalog
* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values
* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill
* Restore generator frame-from-params ranking to the roadmap as a planned stage
* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract
* Unload only the wires whose displayed style changed when types update
* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized
* Restore the whole-List Transform variant so rank-1 content wires resolve again
* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes
* Give every ranked connector a field adapter and add numeric cast variants for legacy wires
* Key a ranked param's type default off its Item wire form when no literal default exists
* Inherit the layer's content value when splicing a node into an empty chain
* Migrate stale List-form TypeDefault inputs to the definition's current default
* Generate the mapped wire variant only when the element-wise node has a frame source
* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps
* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot
* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers
* Convert the measure family to element-wise Item kernels per the audit classification
* Prefer the bare element value over the Item type default so ranked params keep their widgets
* Rename GradientStopsUI to GradientUI
* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix
* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2
* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes
* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions
* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius
* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness
* Rank the value, color, and text generator params to Item
* Rank the raster, web-request, and context-reader generator params to Item
* Fix the repeat and brush test wirings left behind by the param-ranking sweeps
* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes
* Delete the Attach Attribute node, folding its role into Write Attribute
* Add the Filter and Sort list companion nodes
* Guard the removed-definition migration swap target with a test
* Add the Box Corners value type in place of the rectangle corner radius list
* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node
* Rank the Combine Channels node's channel connectors to Item
* Make Map Points an element-wise node
* Delete the deprecated Upload Texture node
* Update the implementation roadmap to reflect the landed stages
* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path
* Prefer the rank-0 default when disconnecting a rank-capable input
* Make Path Modify an element-wise node
* Wrap node paths in a NodeIdPath newtype so they flow as a single Item
* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves
* Stop the Brush node from setting layer attributes its paint operation doesn't produce
* Present-gate Flatten Path's adopted layer path like its fill and stroke
* Gate carried layer attributes on static column presence, not runtime values
* Give the remaining graphic Item<T> types a default so unconnected primaries resolve
* Dispatch a ranked param's Properties widget from its rank-0 element type
* Make Extract Transform an element-wise node, restoring the Origins to Polyline body
* Rename Flatten Path to Combine Paths
* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath
* Drop the dead List<u8> and List<NodeId> wire rows
* Rank Flatten Graphic's Fully Flatten toggle to Item
* Update the implementation roadmap with the endgame scope
* Make Combine Paths a reducer that collapses the whole frame into one path
* Stop type-converter nodes from carrying the source's unrelated attributes
* Format the Origins to Polyline regression test
* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value
* Make Switch a framed element-wise select, bundling whole collections
* Widen and align element-type coverage across the list and graphic nodes
* Register the compiler's cache chain pair for every ranked enum and newtype wire
* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires
* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion
* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg
* Delete the vestigial Clone debug node
* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters
* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR
* Deduplicate the promotion adapter registrations into the field adapter macro
* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge
* Vertical wire styling
* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>
* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row
* Rank Path Modify's modification parameter to Item<Box<VectorModification>>
* Rename the field adapter node family to input adapter
* Drop the dead bare scalar rows from Context Modification's implementations list
* Move the dynamic executor's test module into its own file
* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode
* Materialize stored TaggedValues as ranked Item wires at the source
* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization
* Plant the input adapter for List-only inputs, composing position conversion from standard rows
* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers
* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect
* Enforce ranked node inputs in the macro, rejecting bare wire declarations
* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel
* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration
* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced
* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs
* Rewrite the element-wise accessor wire type at the primary input, not raw index 0
* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row
* Gate the remaining Raster<GPU> registry rows behind the gpu feature
* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter
* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index
* Store paint picks as plain color/gradient values, removing the FillChoice value type
* Code review restructuring
* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node
* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR
* Parse Fill and Stroke color defaults through the paint wire's Graphic element
* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing
* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration
* Migrate the legacy 4-input Fill directly to the split has-transform shape
* Upgrade the demo artwork
* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues
* Remove the rank polymorphism working documents
* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation
* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types
* Derive PartialEq for Item now that attributes participate in equality
* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types
* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
Generated
+1
@@ -2038,6 +2038,7 @@ dependencies = [
|
|||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
|
"graphene-hash",
|
||||||
"graphene-resource",
|
"graphene-resource",
|
||||||
"log",
|
"log",
|
||||||
"raster-types",
|
"raster-types",
|
||||||
|
|||||||
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
@@ -252,9 +252,20 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
|||||||
let mut vectors = Vec::new();
|
let mut vectors = Vec::new();
|
||||||
let mut resources = Vec::new();
|
let mut resources = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
|
// Pasted templates skip document migration, so stored types normalize here
|
||||||
match item {
|
match item {
|
||||||
ClipboardItem::Layer(entry) => layers.push(entry),
|
ClipboardItem::Layer(mut entry) => {
|
||||||
ClipboardItem::Nodes(nodes) => node_groups.push(nodes),
|
for (_, template) in &mut entry.nodes {
|
||||||
|
template.document_node.normalize_stored_types();
|
||||||
|
}
|
||||||
|
layers.push(entry);
|
||||||
|
}
|
||||||
|
ClipboardItem::Nodes(mut nodes) => {
|
||||||
|
for (_, template) in &mut nodes {
|
||||||
|
template.document_node.normalize_stored_types();
|
||||||
|
}
|
||||||
|
node_groups.push(nodes);
|
||||||
|
}
|
||||||
ClipboardItem::Vector(vector) => vectors.push(vector),
|
ClipboardItem::Vector(vector) => vectors.push(vector),
|
||||||
ClipboardItem::Resource(resource) => resources.push(resource),
|
ClipboardItem::Resource(resource) => resources.push(resource),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::core_types::misc::parse_css_color;
|
use graphene_std::core_types::misc::parse_css_color;
|
||||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientStops, GradientStopsUI};
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI, Gradient, GradientUI};
|
||||||
|
|
||||||
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
||||||
const MIN_MIDPOINT: f64 = 0.01;
|
const MIN_MIDPOINT: f64 = 0.01;
|
||||||
@@ -28,7 +28,7 @@ pub struct ColorPickerMessageHandler {
|
|||||||
old_is_none: bool,
|
old_is_none: bool,
|
||||||
|
|
||||||
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
|
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
|
||||||
gradient: Option<GradientStops>,
|
gradient: Option<Gradient>,
|
||||||
active_marker_index: Option<u32>,
|
active_marker_index: Option<u32>,
|
||||||
active_marker_is_midpoint: bool,
|
active_marker_is_midpoint: bool,
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@ impl ColorPickerMessageHandler {
|
|||||||
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
||||||
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
||||||
let mut row_widgets = vec![
|
let mut row_widgets = vec![
|
||||||
SpectrumInput::new(GradientStopsUI::from(gradient))
|
SpectrumInput::new(GradientUI::from(gradient))
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.active_marker_index(self.active_marker_index)
|
.active_marker_index(self.active_marker_index)
|
||||||
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use derivative::*;
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::vector::style::{FillChoiceUI, GradientStopsUI};
|
use graphene_std::vector::style::{FillChoiceUI, GradientUI};
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -531,7 +531,7 @@ pub struct SpectrumInput {
|
|||||||
// Content
|
// Content
|
||||||
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub track: GradientStopsUI,
|
pub track: GradientUI,
|
||||||
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
||||||
#[serde(rename = "trackCSS")]
|
#[serde(rename = "trackCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ 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::gradient::GradientStops;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::list::List;
|
use graphene_std::gradient::Gradient;
|
||||||
|
use graphene_std::list::{Item, List, NodeIdPath};
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
|
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, Context, Graphic};
|
use graphene_std::{Artboard, Color, Context, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -176,10 +187,10 @@ macro_rules! generate_layout_downcast {
|
|||||||
}
|
}
|
||||||
// TODO: We simply try all these types sequentially. Find a better strategy.
|
// TODO: We simply try all these types sequentially. Find a better strategy.
|
||||||
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
||||||
// `List<NodeId>` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a
|
// `Item<NodeIdPath>` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a
|
||||||
// `List` where each item's NodeId resolves against the prefix made up of the items above it.
|
// `List` where each item's NodeId resolves against the prefix made up of the items above it.
|
||||||
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<NodeId>>>() {
|
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<NodeIdPath>>>() {
|
||||||
return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
|
return Some(table_node_id_path_layout_with_breadcrumb(&io.output.element().0, data));
|
||||||
}
|
}
|
||||||
generate_layout_downcast!(introspected_data, data, [
|
generate_layout_downcast!(introspected_data, data, [
|
||||||
List<Artboard>,
|
List<Artboard>,
|
||||||
@@ -188,27 +199,106 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
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>,
|
||||||
GradientStops,
|
List<DashPattern>,
|
||||||
f64,
|
List<BoxCorners>,
|
||||||
u32,
|
List<StrokeJoin>,
|
||||||
u64,
|
List<StrokeAlign>,
|
||||||
bool,
|
List<StrokeCap>,
|
||||||
String,
|
List<PaintOrder>,
|
||||||
Option<f64>,
|
List<MergeByDistanceAlgorithm>,
|
||||||
DVec2,
|
List<ExtrudeJoiningAlgorithm>,
|
||||||
DAffine2,
|
List<PointSpacingType>,
|
||||||
BlendMode,
|
List<StringCapitalization>,
|
||||||
GradientType,
|
List<LuminanceCalculation>,
|
||||||
GradientSpreadMethod,
|
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>,
|
||||||
|
Item<Artboard>,
|
||||||
|
Item<Graphic>,
|
||||||
|
Item<Vector>,
|
||||||
|
Item<Raster<CPU>>,
|
||||||
|
Item<Raster<GPU>>,
|
||||||
|
Item<Color>,
|
||||||
|
Item<Gradient>,
|
||||||
|
Item<String>,
|
||||||
|
Item<f64>,
|
||||||
|
Item<f32>,
|
||||||
|
Item<u32>,
|
||||||
|
Item<u64>,
|
||||||
|
Item<i32>,
|
||||||
|
Item<i64>,
|
||||||
|
Item<bool>,
|
||||||
|
Item<DVec2>,
|
||||||
|
Item<DAffine2>,
|
||||||
|
Item<BlendMode>,
|
||||||
|
Item<GradientType>,
|
||||||
|
Item<GradientSpreadMethod>,
|
||||||
|
Item<DashPattern>,
|
||||||
|
Item<BoxCorners>,
|
||||||
|
Item<StrokeJoin>,
|
||||||
|
Item<StrokeAlign>,
|
||||||
|
Item<StrokeCap>,
|
||||||
|
Item<PaintOrder>,
|
||||||
|
Item<MergeByDistanceAlgorithm>,
|
||||||
|
Item<ExtrudeJoiningAlgorithm>,
|
||||||
|
Item<PointSpacingType>,
|
||||||
|
Item<StringCapitalization>,
|
||||||
|
Item<LuminanceCalculation>,
|
||||||
|
Item<RedGreenBlue>,
|
||||||
|
Item<RedGreenBlueAlpha>,
|
||||||
|
Item<RelativeAbsolute>,
|
||||||
|
Item<SelectiveColorChoice>,
|
||||||
|
Item<XY>,
|
||||||
|
Item<ScaleType>,
|
||||||
|
Item<ReferencePoint>,
|
||||||
|
Item<CentroidType>,
|
||||||
|
Item<BooleanOperation>,
|
||||||
|
Item<NoiseType>,
|
||||||
|
Item<FractalType>,
|
||||||
|
Item<CellularDistanceFunction>,
|
||||||
|
Item<CellularReturnType>,
|
||||||
|
Item<DomainWarpType>,
|
||||||
|
Item<RealTimeMode>,
|
||||||
|
Item<GridType>,
|
||||||
|
Item<ArcType>,
|
||||||
|
Item<SpiralType>,
|
||||||
|
Item<TextAlign>,
|
||||||
|
Item<QRCodeErrorCorrectionLevel>,
|
||||||
|
Item<InterpolationDistribution>,
|
||||||
|
Item<RowsOrColumns>,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +336,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"
|
||||||
@@ -324,12 +465,53 @@ 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"
|
||||||
}
|
}
|
||||||
fn identifier(&self) -> String {
|
fn identifier(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
|
Self::None => "None".to_string(),
|
||||||
Self::Graphic(list) => list.identifier(),
|
Self::Graphic(list) => list.identifier(),
|
||||||
Self::Vector(list) => list.identifier(),
|
Self::Vector(list) => list.identifier(),
|
||||||
Self::RasterCPU(list) => list.identifier(),
|
Self::RasterCPU(list) => list.identifier(),
|
||||||
@@ -345,6 +527,7 @@ impl TableItemLayout for Graphic {
|
|||||||
}
|
}
|
||||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::None => label("None"),
|
||||||
Self::Graphic(list) => list.layout_with_breadcrumb(data),
|
Self::Graphic(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::Vector(list) => list.layout_with_breadcrumb(data),
|
Self::Vector(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::RasterCPU(list) => list.layout_with_breadcrumb(data),
|
Self::RasterCPU(list) => list.layout_with_breadcrumb(data),
|
||||||
@@ -512,7 +695,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)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,7 +720,7 @@ impl TableItemLayout for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableItemLayout for GradientStops {
|
impl TableItemLayout for Gradient {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"Gradient"
|
"Gradient"
|
||||||
}
|
}
|
||||||
@@ -585,6 +768,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)"
|
||||||
@@ -600,6 +798,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)"
|
||||||
@@ -726,45 +955,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)])]
|
||||||
@@ -905,12 +1162,10 @@ macro_rules! known_item_types {
|
|||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
List<NodeId>,
|
|
||||||
List<f64>,
|
List<f64>,
|
||||||
List<u8>,
|
Gradient,
|
||||||
GradientStops,
|
|
||||||
Color,
|
Color,
|
||||||
NodeId,
|
NodeId,
|
||||||
DAffine2,
|
DAffine2,
|
||||||
@@ -919,9 +1174,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,
|
||||||
@@ -929,6 +1187,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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -949,6 +1243,10 @@ fn display_value_override(any: &dyn Any) -> Option<String> {
|
|||||||
/// element-column rendering and attribute-column rendering. Returns `None` for unrecognized
|
/// element-column rendering and attribute-column rendering. Returns `None` for unrecognized
|
||||||
/// types so the caller can fall back to a debug-formatted [`TextLabel`].
|
/// types so the caller can fall back to a debug-formatted [`TextLabel`].
|
||||||
fn dispatch_value_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> Option<WidgetInstance> {
|
fn dispatch_value_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> Option<WidgetInstance> {
|
||||||
|
// `NodeIdPath` (e.g. the `editor:layer_path` attribute) drills into its inner path list, matching `drilldown_attribute_layout`.
|
||||||
|
if let Some(path) = any.downcast_ref::<NodeIdPath>() {
|
||||||
|
return Some(path.0.value_widget(target, data));
|
||||||
|
}
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
( $($ty:ty),* $(,)? ) => {
|
( $($ty:ty),* $(,)? ) => {
|
||||||
$(
|
$(
|
||||||
@@ -1005,10 +1303,10 @@ fn table_node_id_path_layout_with_breadcrumb(path: &List<NodeId>, data: &mut Lay
|
|||||||
/// Mirrors [`dispatch_value_widget`] but routes to [`TableItemLayout::layout_with_breadcrumb`].
|
/// Mirrors [`dispatch_value_widget`] but routes to [`TableItemLayout::layout_with_breadcrumb`].
|
||||||
/// Returns `None` for unrecognized types.
|
/// Returns `None` for unrecognized types.
|
||||||
fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
|
||||||
// `List<NodeId>` is interpreted as a path (e.g. the `editor:layer_path` attribute), so each item's NodeId value
|
// `NodeIdPath` is interpreted as a path (e.g. the `editor:layer_path` attribute), so each item's NodeId value
|
||||||
// resolves against the prefix made up of preceding items. Handled before the generic `List<T>` blanket impl.
|
// resolves against the prefix made up of preceding items. Handled before the generic blanket impl.
|
||||||
if let Some(path) = any.downcast_ref::<List<NodeId>>() {
|
if let Some(path) = any.downcast_ref::<NodeIdPath>() {
|
||||||
return Some(table_node_id_path_layout_with_breadcrumb(path, data));
|
return Some(table_node_id_path_layout_with_breadcrumb(&path.0, data));
|
||||||
}
|
}
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
( $($ty:ty),* $(,)? ) => {
|
( $($ty:ty),* $(,)? ) => {
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ use crate::node_graph_executor::NodeGraphExecutor;
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::application_io::wgpu_available;
|
use graph_craft::application_io::wgpu_available;
|
||||||
use graph_craft::descriptor;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||||
|
use graph_craft::list;
|
||||||
use graphene_std::graphic::is_paint_present;
|
use graphene_std::graphic::is_paint_present;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||||
@@ -125,7 +125,7 @@ pub struct DocumentMessageHandler {
|
|||||||
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
||||||
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)>,
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
@@ -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>) {
|
||||||
@@ -2758,7 +2758,7 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
||||||
let index_elements_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::index_elements::IDENTIFIER).expect("Index Elements node should exist");
|
let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::item_at_index::IDENTIFIER).expect("Item at Index node should exist");
|
||||||
|
|
||||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||||
|
|
||||||
@@ -2791,7 +2791,7 @@ impl DocumentMessageHandler {
|
|||||||
if has_fill && has_stroke {
|
if has_fill && has_stroke {
|
||||||
let (existing_index, new_index) = (0_f64, 1_f64);
|
let (existing_index, new_index) = (0_f64, 1_f64);
|
||||||
|
|
||||||
let existing_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
let existing_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(existing_index), false))]);
|
||||||
let existing_index_id = NodeId::new();
|
let existing_index_id = NodeId::new();
|
||||||
self.network_interface.insert_node(existing_index_id, existing_index_template, &[]);
|
self.network_interface.insert_node(existing_index_id, existing_index_template, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false);
|
self.network_interface.move_node_to_chain_start(&existing_index_id, layer, &[], false);
|
||||||
@@ -2813,7 +2813,7 @@ impl DocumentMessageHandler {
|
|||||||
self.network_interface.set_display_name(&new_layer_id, original_name, &[]);
|
self.network_interface.set_display_name(&new_layer_id, original_name, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_index_template = index_elements_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
let new_index_template = item_at_index_definition.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(new_index), false))]);
|
||||||
let new_index_id = NodeId::new();
|
let new_index_id = NodeId::new();
|
||||||
self.network_interface.insert_node(new_index_id, new_index_template, &[]);
|
self.network_interface.insert_node(new_index_id, new_index_template, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false);
|
self.network_interface.move_node_to_chain_start(&new_index_id, new_layer, &[], false);
|
||||||
@@ -3789,7 +3789,7 @@ impl DocumentMessageHandler {
|
|||||||
/// Create a network interface with a single export
|
/// Create a network interface with a single export
|
||||||
fn default_document_network_interface() -> NodeNetworkInterface {
|
fn default_document_network_interface() -> NodeNetworkInterface {
|
||||||
let mut network_interface = NodeNetworkInterface::default();
|
let mut network_interface = NodeNetworkInterface::default();
|
||||||
network_interface.add_export(TaggedValue::TypeDefault(descriptor!(graphene_std::list::List<graphene_std::Artboard>)), -1, "", &[]);
|
network_interface.add_export(TaggedValue::TypeDefault(list!(graphene_std::Artboard)), -1, "", &[]);
|
||||||
network_interface
|
network_interface
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4007,7 +4007,7 @@ mod document_message_handler_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pending_gradient_bakes_round_trip_through_serialization() {
|
fn pending_gradient_bakes_round_trip_through_serialization() {
|
||||||
let document = DocumentMessageHandler {
|
let document = DocumentMessageHandler {
|
||||||
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::Gradient::default())],
|
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::LegacyGradient::default())],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4318,4 +4318,35 @@ 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();
|
||||||
|
|
||||||
|
let base_lengths: Vec<usize> = instrumented
|
||||||
|
.grab_all_input::<graphene_std::graphic::extend::BaseInput<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::<graphene_std::graphic::extend::NewInput<graphene_std::Graphic>>(&editor.runtime).collect();
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use graphene_std::raster_types::Image;
|
|||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
||||||
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
use graphene_std::vector::{Gradient, PointId, VectorModificationType};
|
||||||
|
|
||||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -22,7 +22,7 @@ pub enum GraphOperationMessage {
|
|||||||
},
|
},
|
||||||
FillGradientSet {
|
FillGradientSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
gradient: GradientStops,
|
gradient: Gradient,
|
||||||
gradient_type: GradientType,
|
gradient_type: GradientType,
|
||||||
spread_method: GradientSpreadMethod,
|
spread_method: GradientSpreadMethod,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
@@ -33,7 +33,7 @@ pub enum GraphOperationMessage {
|
|||||||
},
|
},
|
||||||
GradientStopsSet {
|
GradientStopsSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
stops: GradientStops,
|
stops: Gradient,
|
||||||
},
|
},
|
||||||
GradientTransformSet {
|
GradientTransformSet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
|
|||||||
+13
-21
@@ -8,12 +8,11 @@ use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
use graph_craft::descriptor;
|
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::list::List;
|
use graph_craft::list;
|
||||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Gradient, GradientSpreadMethod, GradientStop, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::{Artboard, Color};
|
use graphene_std::{Artboard, Color};
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
@@ -180,7 +179,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the bottom input of the artboard back to artboard
|
// Set the bottom input of the artboard back to artboard
|
||||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
||||||
} else {
|
} else {
|
||||||
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
|
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
|
||||||
@@ -188,7 +187,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
|
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
|
||||||
|
|
||||||
// Set the bottom input of the artboard back to artboard
|
// Set the bottom input of the artboard back to artboard
|
||||||
let bottom_input = NodeInput::type_default(descriptor!(List<Artboard>), true);
|
let bottom_input = NodeInput::type_default(list!(Artboard), true);
|
||||||
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,8 +507,8 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
|||||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||||
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
|
/// Returns a map from gradient element `id` to `Gradient` containing only the real stops.
|
||||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
|
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, Gradient> {
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
||||||
@@ -555,7 +554,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if has_any_midpoint && !real_stops.is_empty() {
|
if has_any_midpoint && !real_stops.is_empty() {
|
||||||
result.insert(gradient_id, GradientStops::new(real_stops));
|
result.insert(gradient_id, Gradient::new(real_stops));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,14 +578,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
|||||||
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
/// interact with any existing layers in the parent stack. All descendant layers use a lightweight
|
||||||
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
/// O(n) import path that skips collision detection and instead calculates positions directly from
|
||||||
/// the known tree structure.
|
/// the known tree structure.
|
||||||
fn import_usvg_node(
|
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
||||||
modify_inputs: &mut ModifyInputsContext,
|
|
||||||
node: &usvg::Node,
|
|
||||||
id: NodeId,
|
|
||||||
parent: LayerNodeIdentifier,
|
|
||||||
insert_index: usize,
|
|
||||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
|
||||||
) {
|
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
|
|
||||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||||
@@ -649,7 +641,7 @@ fn import_usvg_node_inner(
|
|||||||
id: NodeId,
|
id: NodeId,
|
||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
insert_index: usize,
|
insert_index: usize,
|
||||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
graphite_gradient_stops: &HashMap<String, Gradient>,
|
||||||
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
group_extents_map: &mut HashMap<LayerNodeIdentifier, Vec<u32>>,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
let layer = modify_inputs.create_layer(id);
|
let layer = modify_inputs.create_layer(id);
|
||||||
@@ -692,7 +684,7 @@ fn import_usvg_node_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
||||||
let subpaths = convert_usvg_path(path);
|
let subpaths = convert_usvg_path(path);
|
||||||
|
|
||||||
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||||
@@ -807,7 +799,7 @@ fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, Gradient>) {
|
||||||
match &fill.paint() {
|
match &fill.paint() {
|
||||||
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
|
||||||
usvg::Paint::LinearGradient(linear) => {
|
usvg::Paint::LinearGradient(linear) => {
|
||||||
@@ -827,7 +819,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||||
});
|
});
|
||||||
GradientStops::new(stops)
|
Gradient::new(stops)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let spread_method = convert_spread_method(linear.spread_method());
|
let spread_method = convert_spread_method(linear.spread_method());
|
||||||
@@ -851,7 +843,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: usvg_color(stop.color(), stop.opacity().get()),
|
color: usvg_color(stop.color(), stop.opacity().get()),
|
||||||
});
|
});
|
||||||
GradientStops::new(stops)
|
Gradient::new(stops)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let spread_method = convert_spread_method(radial.spread_method());
|
let spread_method = convert_spread_method(radial.spread_method());
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2, IVec2};
|
|||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete, descriptor};
|
use graph_craft::{ProtoNodeIdentifier, list};
|
||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
@@ -16,7 +16,7 @@ use graphene_std::raster_types::Image;
|
|||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
|
||||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{Gradient, PointId, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -134,11 +134,11 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
/// Creates an artboard as the primary export for the document network.
|
/// Creates an artboard as the primary export for the document network.
|
||||||
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
|
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
|
||||||
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
|
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
|
||||||
Some(NodeInput::type_default(descriptor!(List<Artboard>), true)),
|
Some(NodeInput::type_default(list!(Artboard), true)),
|
||||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
Some(NodeInput::type_default(list!(Graphic), true)),
|
||||||
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
|
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
|
||||||
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
|
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
|
||||||
Some(NodeInput::value(TaggedValue::Color(Some(background)), false)),
|
Some(NodeInput::value(TaggedValue::Color(background), false)),
|
||||||
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
|
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
|
||||||
]);
|
]);
|
||||||
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
||||||
@@ -149,7 +149,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
|
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
|
||||||
.expect("Boolean node does not exist")
|
.expect("Boolean node does not exist")
|
||||||
.node_template_input_override([
|
.node_template_input_override([
|
||||||
Some(NodeInput::type_default(descriptor!(List<Graphic>), true)),
|
Some(NodeInput::type_default(list!(Graphic), true)),
|
||||||
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
|
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
|
||||||
let blend = resolve_network_node_type("Blend")
|
let blend = resolve_network_node_type("Blend")
|
||||||
.expect("Blend node does not exist")
|
.expect("Blend node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
.node_template_input_override([Some(NodeInput::type_default(list!(Graphic), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
|
||||||
|
|
||||||
let blend_id = NodeId::new();
|
let blend_id = NodeId::new();
|
||||||
self.network_interface.insert_node(blend_id, blend, &[]);
|
self.network_interface.insert_node(blend_id, blend, &[]);
|
||||||
@@ -173,7 +173,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
|
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
|
||||||
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
|
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
|
||||||
.expect("Morph node does not exist")
|
.expect("Morph node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::type_default(descriptor!(List<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
.node_template_input_override([Some(NodeInput::type_default(list!(Graphic), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
||||||
|
|
||||||
let morph_id = NodeId::new();
|
let morph_id = NodeId::new();
|
||||||
self.network_interface.insert_node(morph_id, morph, &[]);
|
self.network_interface.insert_node(morph_id, morph, &[]);
|
||||||
@@ -300,7 +300,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
|
||||||
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
|
||||||
.expect("Color Value node does not exist")
|
.expect("Color Value node does not exist")
|
||||||
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(Some(color)), false))]);
|
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(color), false))]);
|
||||||
|
|
||||||
let color_value_id = NodeId::new();
|
let color_value_id = NodeId::new();
|
||||||
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
self.network_interface.insert_node(color_value_id, color_value, &[]);
|
||||||
@@ -437,7 +437,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
|
// TODO: Allow the 'Path' node to operate on `List` data by utilizing the reference (index or ID?) for each item.
|
||||||
if node_definition.identifier == "Path" {
|
if node_definition.identifier == "Path" {
|
||||||
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]);
|
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]);
|
||||||
if layer_input_type.compiled_nested_type() == Some(&concrete!(List<Graphic>)) {
|
if layer_input_type.compiled_element_name().as_deref() == Some("Graphic") {
|
||||||
let Some(flatten_path_definition) = resolve_proto_node_type(graphene_std::vector_nodes::flatten_path::IDENTIFIER) else {
|
let Some(flatten_path_definition) = resolve_proto_node_type(graphene_std::vector_nodes::flatten_path::IDENTIFIER) else {
|
||||||
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
|
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
|
||||||
return None;
|
return None;
|
||||||
@@ -460,11 +460,15 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
|
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
|
||||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||||
|
|
||||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
// The backup remembers the last solid color, so the red-slash "none" choice leaves it untouched
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
|
if let Some(color) = color {
|
||||||
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
||||||
|
}
|
||||||
|
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
|
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
|
||||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -488,9 +492,14 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
||||||
.is_some_and(|input| input.as_value().is_some());
|
.is_some_and(|input| input.as_value().is_some());
|
||||||
if transform_is_value {
|
if transform_is_value {
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::HasTransformInput::INDEX),
|
||||||
|
NodeInput::value(TaggedValue::Bool(true), false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
self.set_input_with_refresh(
|
self.set_input_with_refresh(
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
||||||
NodeInput::value(TaggedValue::OptionalDAffine2(Some(transform)), false),
|
NodeInput::value(TaggedValue::DAffine2(transform), false),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -563,7 +572,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
||||||
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
|
pub fn gradient_stops_set(&mut self, stops: Gradient) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
||||||
@@ -716,7 +725,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(color.map_or_else(TaggedValue::no_paint, TaggedValue::Color), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
|
||||||
@@ -729,8 +738,8 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::<graphene_std::list::List<f64>>::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashPatternInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths.into()), false), true);
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
|
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ use glam::DVec2;
|
|||||||
use graph_craft::ProtoNodeIdentifier;
|
use graph_craft::ProtoNodeIdentifier;
|
||||||
use graph_craft::document::value::*;
|
use graph_craft::document::value::*;
|
||||||
use graph_craft::document::*;
|
use graph_craft::document::*;
|
||||||
use graph_craft::{concrete, descriptor};
|
use graph_craft::{concrete, list};
|
||||||
use graphene_std::extract_xy::XY;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::list::List;
|
|
||||||
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
|
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
|
||||||
use graphene_std::raster_types::{CPU, Raster};
|
use graphene_std::raster_types::{CPU, Raster};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -207,7 +206,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![NodeInput::type_default(descriptor!(List<Graphic>), true), NodeInput::type_default(descriptor!(List<Graphic>), true)],
|
inputs: vec![NodeInput::type_default(list!(Graphic), true), NodeInput::type_default(list!(Graphic), true)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||||
@@ -292,11 +291,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
call_argument: generic!(T),
|
call_argument: generic!(T),
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(TaggedValue), 1),
|
NodeInput::import(Type::Fn(concrete!(Context).into(), generic!(T).into()), 1),
|
||||||
NodeInput::import(concrete!(TaggedValue), 2),
|
NodeInput::import(item!(TaggedValue), 2),
|
||||||
NodeInput::import(concrete!(TaggedValue), 3),
|
NodeInput::import(item!(TaggedValue), 3),
|
||||||
NodeInput::import(concrete!(TaggedValue), 4),
|
NodeInput::import(item!(TaggedValue), 4),
|
||||||
NodeInput::import(concrete!(TaggedValue), 5),
|
NodeInput::import(item!(TaggedValue), 5),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -327,7 +326,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(List<Artboard>))), 0),
|
NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(list!(Artboard))), 0),
|
||||||
NodeInput::node(NodeId(3), 0),
|
NodeInput::node(NodeId(3), 0),
|
||||||
],
|
],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||||
@@ -341,11 +340,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::type_default(descriptor!(List<Artboard>), true),
|
NodeInput::type_default(list!(Artboard), true),
|
||||||
NodeInput::type_default(descriptor!(List<Graphic>), true),
|
NodeInput::type_default(list!(Graphic), true),
|
||||||
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
|
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
|
||||||
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
|
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
|
||||||
NodeInput::value(TaggedValue::Color(Some(Color::WHITE)), false),
|
NodeInput::value(TaggedValue::Color(Color::WHITE), false),
|
||||||
NodeInput::value(TaggedValue::Bool(true), false),
|
NodeInput::value(TaggedValue::Bool(true), false),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -453,9 +452,9 @@ 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::count_elements::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(vector::list_length::IDENTIFIER),
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -468,7 +467,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
// 3: Floor (integer count per subpath)
|
// 3: Floor (integer count per subpath)
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER),
|
||||||
inputs: vec![NodeInput::import(concrete!(f64), 1)],
|
inputs: vec![NodeInput::import(item!(f64), 1)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// 4: Multiply (total_instances = count × subpath_count)
|
// 4: Multiply (total_instances = count × subpath_count)
|
||||||
@@ -544,8 +543,8 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
NodeInput::import(generic!(T), 0),
|
NodeInput::import(generic!(T), 0),
|
||||||
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(item!(vector::misc::InterpolationDistribution), 3),
|
||||||
NodeInput::import(generic!(T), 4),
|
NodeInput::import(item!(Vector), 4),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -575,11 +574,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::type_default(descriptor!(List<Vector>), true),
|
NodeInput::type_default(list!(Vector), true),
|
||||||
NodeInput::value(TaggedValue::F64(10.), false),
|
NodeInput::value(TaggedValue::F64(10.), false),
|
||||||
NodeInput::value(TaggedValue::Bool(Default::default()), false),
|
NodeInput::value(TaggedValue::Bool(Default::default()), false),
|
||||||
NodeInput::value(TaggedValue::InterpolationDistribution(Default::default()), false),
|
NodeInput::value(TaggedValue::InterpolationDistribution(Default::default()), false),
|
||||||
NodeInput::type_default(descriptor!(List<Vector>), false),
|
NodeInput::type_default(list!(Vector), false),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -604,7 +603,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)),
|
||||||
@@ -826,7 +825,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![NodeInput::type_default(descriptor!(List<Vector>), true)],
|
inputs: vec![NodeInput::type_default(list!(Vector), true)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||||
@@ -924,7 +923,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||||
nodes: [
|
nodes: [
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::import(concrete!(String), 1)],
|
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::import(item!(String), 1)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -999,7 +998,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::import(generic!(T), 0), NodeInput::import(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
|
inputs: vec![NodeInput::import(generic!(T), 0), NodeInput::import(item!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::rasterize::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::rasterize::IDENTIFIER),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -1011,7 +1010,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::type_default(descriptor!(List<Vector>), true),
|
NodeInput::type_default(list!(Vector), true),
|
||||||
NodeInput::value(
|
NodeInput::value(
|
||||||
TaggedValue::Footprint(Footprint {
|
TaggedValue::Footprint(Footprint {
|
||||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
|
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
|
||||||
@@ -1081,7 +1080,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
nodes: [
|
nodes: [
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(List<Raster<CPU>>), 0),
|
NodeInput::import(list!(Raster<CPU>), 0),
|
||||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
|
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
|
||||||
],
|
],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||||
@@ -1090,7 +1089,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(List<Raster<CPU>>), 0),
|
NodeInput::import(list!(Raster<CPU>), 0),
|
||||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
|
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
|
||||||
],
|
],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||||
@@ -1099,7 +1098,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(List<Raster<CPU>>), 0),
|
NodeInput::import(list!(Raster<CPU>), 0),
|
||||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
|
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
|
||||||
],
|
],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||||
@@ -1108,7 +1107,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(List<Raster<CPU>>), 0),
|
NodeInput::import(list!(Raster<CPU>), 0),
|
||||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
|
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
|
||||||
],
|
],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||||
@@ -1122,7 +1121,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![NodeInput::type_default(descriptor!(List<Raster<CPU>>), true)],
|
inputs: vec![NodeInput::type_default(list!(Raster<CPU>), true)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||||
@@ -1183,13 +1182,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
|
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
|
||||||
nodes: [
|
nodes: [
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||||
call_argument: generic!(T),
|
call_argument: generic!(T),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||||
call_argument: generic!(T),
|
call_argument: generic!(T),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1244,85 +1243,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
),
|
),
|
||||||
properties: None,
|
properties: None,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
DocumentNodeDefinition {
|
|
||||||
identifier: "Upload Texture",
|
|
||||||
category: "Debug",
|
|
||||||
node_template: NodeTemplate {
|
|
||||||
document_node: DocumentNode {
|
|
||||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
|
||||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
|
||||||
nodes: [
|
|
||||||
DocumentNode {
|
|
||||||
inputs: vec![NodeInput::import(concrete!(List<Raster<CPU>>), 0), NodeInput::scope(platform_application_io::wgpu_executor::IDENTIFIER)],
|
|
||||||
call_argument: generic!(T),
|
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_conversion::upload_texture::IDENTIFIER),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
DocumentNode {
|
|
||||||
call_argument: generic!(T),
|
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memoize::IDENTIFIER),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, node)| (NodeId(id as u64), node))
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
inputs: vec![NodeInput::type_default(descriptor!(List<Raster<CPU>>), true)],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
|
||||||
output_names: vec!["Texture".to_string()],
|
|
||||||
network_metadata: Some(NodeNetworkMetadata {
|
|
||||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
|
||||||
node_metadata: [
|
|
||||||
DocumentNodeMetadata {
|
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, 0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
DocumentNodeMetadata {
|
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
DocumentNodeMetadata {
|
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, node)| (NodeId(id as u64), node))
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: Cow::Borrowed("TODO"),
|
|
||||||
properties: None,
|
|
||||||
},
|
|
||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
identifier: "Extract",
|
identifier: "Extract",
|
||||||
category: "",
|
category: "",
|
||||||
node_template: NodeTemplate {
|
node_template: NodeTemplate {
|
||||||
document_node: DocumentNode {
|
document_node: DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::Extract,
|
implementation: DocumentNodeImplementation::Extract,
|
||||||
inputs: vec![NodeInput::type_default(descriptor!(DocumentNode), true)],
|
inputs: vec![NodeInput::type_default(concrete!(DocumentNode), true)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||||
@@ -1350,25 +1277,25 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
// Node 0: regex_find proto node — returns List<String> of [whole_match, ...capture_groups]
|
// Node 0: regex_find proto node — returns List<String> of [whole_match, ...capture_groups]
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::import(concrete!(String), 0),
|
NodeInput::import(item!(String), 0),
|
||||||
NodeInput::import(concrete!(String), 1),
|
NodeInput::import(item!(String), 1),
|
||||||
NodeInput::import(concrete!(f64), 2),
|
NodeInput::import(item!(f64), 2),
|
||||||
NodeInput::import(concrete!(bool), 3),
|
NodeInput::import(item!(bool), 3),
|
||||||
NodeInput::import(concrete!(bool), 4),
|
NodeInput::import(item!(bool), 4),
|
||||||
],
|
],
|
||||||
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::extract_element::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::omit_element::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -1450,7 +1377,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,
|
||||||
@@ -1474,7 +1401,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
NodeInput::type_default(descriptor!(List<Vector>), true),
|
NodeInput::type_default(item!(Vector), true),
|
||||||
NodeInput::value(TaggedValue::VectorModification(Default::default()), false),
|
NodeInput::value(TaggedValue::VectorModification(Default::default()), false),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -2207,3 +2134,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 {
|
||||||
@@ -2167,7 +2175,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 {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use graph_craft::{Type, concrete};
|
|||||||
use graphene_std::Graphic;
|
use graphene_std::Graphic;
|
||||||
use graphene_std::NodeInputDecleration;
|
use graphene_std::NodeInputDecleration;
|
||||||
use graphene_std::animation::RealTimeMode;
|
use graphene_std::animation::RealTimeMode;
|
||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
use graphene_std::brush::brush_stroke::BrushTrace;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::extract_xy::XY;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
@@ -32,9 +32,11 @@ use graphene_std::text::{Font, TextAlign};
|
|||||||
use graphene_std::text_nodes::StringCapitalization;
|
use graphene_std::text_nodes::StringCapitalization;
|
||||||
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
use graphene_std::vector::misc::{
|
||||||
|
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
|
};
|
||||||
use graphene_std::vector::style::{
|
use graphene_std::vector::style::{
|
||||||
FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
DashPattern, FillChoiceUI, Gradient, GradientSpreadMethod, GradientType, GradientUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||||
|
|
||||||
@@ -213,6 +215,24 @@ pub(crate) fn property_from_type(
|
|||||||
|
|
||||||
let default_info = ParameterWidgetsInfo::new(node_id, index, true, context);
|
let default_info = ParameterWidgetsInfo::new(node_id, index, true, context);
|
||||||
|
|
||||||
|
// A type with no widget can only be supplied through the graph, labeled with a placeholder row
|
||||||
|
let unsupported_widgets = |default_info: ParameterWidgetsInfo, type_label: String| {
|
||||||
|
let is_exposed = default_info.is_exposed();
|
||||||
|
|
||||||
|
let mut widgets = start_widgets(default_info);
|
||||||
|
if !is_exposed {
|
||||||
|
widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
TextLabel::new("-")
|
||||||
|
.tooltip_label(type_label)
|
||||||
|
.tooltip_description("This data can only be supplied through the node graph because no widget exists for its type.")
|
||||||
|
.widget_instance(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![LayoutGroup::from(widgets)]
|
||||||
|
};
|
||||||
|
|
||||||
let mut extra_widgets = vec![];
|
let mut extra_widgets = vec![];
|
||||||
let widgets = match ty {
|
let widgets = match ty {
|
||||||
Type::Concrete(concrete_type) => {
|
Type::Concrete(concrete_type) => {
|
||||||
@@ -234,92 +254,84 @@ 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
|
||||||
// ===============
|
// ===============
|
||||||
Some(x) if x == TypeId::of::<f64>() || x == TypeId::of::<f32>() => number_widget(default_info, bounded(number_input, f64::NEG_INFINITY, f64::INFINITY)).into(),
|
Some(x) if id_is::<f64>(x) || id_is::<f32>(x) => number_widget(default_info, bounded(number_input, f64::NEG_INFINITY, f64::INFINITY)).into(),
|
||||||
Some(x) if x == TypeId::of::<u32>() => number_widget(default_info, bounded(number_input.int(), 0., f64::from(u32::MAX))).into(),
|
Some(x) if id_is::<u32>(x) => number_widget(default_info, bounded(number_input.int(), 0., f64::from(u32::MAX))).into(),
|
||||||
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, bounded(number_input.int(), 0., f64::INFINITY)).into(),
|
Some(x) if id_is::<u64>(x) => number_widget(default_info, bounded(number_input.int(), 0., f64::INFINITY)).into(),
|
||||||
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
|
Some(x) if id_is::<bool>(x) => bool_widget(default_info, CheckboxInput::default()).into(),
|
||||||
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
|
Some(x) if id_is::<String>(x) => text_widget(default_info).into(),
|
||||||
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
|
Some(x) if id_is::<DVec2>(x) => vec2_widget(default_info, "X", "Y", "", None, false),
|
||||||
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
|
Some(x) if id_is::<DAffine2>(x) => transform_widget(default_info, &mut extra_widgets),
|
||||||
// ==========
|
Some(x) if id_is::<Color>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
|
||||||
// LIST TYPES
|
Some(x) if id_is::<Gradient>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
|
||||||
// ==========
|
Some(x) if id_is::<BrushTrace>(x) => brush_strokes_widget(default_info).into(),
|
||||||
Some(x) if x == TypeId::of::<List<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
|
|
||||||
Some(x) if x == TypeId::of::<List<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
|
|
||||||
Some(x) if x == TypeId::of::<List<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
|
|
||||||
Some(x) if x == TypeId::of::<List<BrushStroke>>() => brush_strokes_widget(default_info).into(),
|
|
||||||
// ============
|
// ============
|
||||||
// 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
|
||||||
// =====
|
// =====
|
||||||
_ => {
|
_ => return Err(unsupported_widgets(default_info, concrete_type.to_string())),
|
||||||
let is_exposed = default_info.is_exposed();
|
|
||||||
|
|
||||||
let mut widgets = start_widgets(default_info);
|
|
||||||
|
|
||||||
if !is_exposed {
|
|
||||||
widgets.extend_from_slice(&[
|
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
|
||||||
TextLabel::new("-")
|
|
||||||
.tooltip_label(concrete_type.to_string())
|
|
||||||
.tooltip_description("This data can only be supplied through the node graph because no widget exists for its type.")
|
|
||||||
.widget_instance(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return Err(vec![widgets.into()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Type::Item(element) => return property_from_type(node_id, index, element, number_options, unit, display_decimal_places, step, context),
|
||||||
|
Type::List(element) => match element.as_ref() {
|
||||||
|
Type::Concrete(element_type) if element_type.name == std::any::type_name::<f64>() => array_of_number_widget(default_info, TextInput::default()).into(),
|
||||||
|
_ => return Err(unsupported_widgets(default_info, ty.to_string())),
|
||||||
|
},
|
||||||
Type::Generic(_) => vec![TextLabel::new("Generic Type (Not Supported)").widget_instance()].into(),
|
Type::Generic(_) => vec![TextLabel::new("Generic Type (Not Supported)").widget_instance()].into(),
|
||||||
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
||||||
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
||||||
@@ -854,6 +866,32 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
|
|||||||
widgets
|
widgets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dash_pattern_widget(parameter_widgets_info: ParameterWidgetsInfo, text_input: TextInput) -> Vec<WidgetInstance> {
|
||||||
|
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||||
|
|
||||||
|
let mut widgets = start_widgets(parameter_widgets_info);
|
||||||
|
|
||||||
|
let Some(document_node) = document_node else { return Vec::new() };
|
||||||
|
let Some(input) = document_node.inputs.get(index) else {
|
||||||
|
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
if let Some(TaggedValue::DashPattern(pattern)) = &input.as_non_exposed_value() {
|
||||||
|
widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
text_input
|
||||||
|
.value(pattern.0.iter_element_values().map(|length| length.to_string()).collect::<Vec<_>>().join(", "))
|
||||||
|
.on_update(optionally_update_value(
|
||||||
|
move |input: &TextInput| Some(TaggedValue::DashPattern(DashPattern::from(input.value.as_str()))),
|
||||||
|
node_id,
|
||||||
|
index,
|
||||||
|
))
|
||||||
|
.widget_instance(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
||||||
pub fn assign_font_message(node_id: NodeId, font: Font) -> Message {
|
pub fn assign_font_message(node_id: NodeId, font: Font) -> Message {
|
||||||
let resource_id = ResourceId::new();
|
let resource_id = ResourceId::new();
|
||||||
@@ -1166,30 +1204,37 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
|||||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
|
|
||||||
// Add the color input
|
// Add the color input
|
||||||
match &**tagged_value {
|
let widget_value = match &**tagged_value {
|
||||||
TaggedValue::Color(color) => widgets.push(
|
TaggedValue::Color(color) => FillChoiceUI::Solid(SRGBA8::from(*color)),
|
||||||
color_button
|
TaggedValue::Gradient(stops) => FillChoiceUI::Gradient(GradientUI::from(stops)),
|
||||||
.value(FillChoiceUI::from(&match color {
|
value if value.is_no_paint() => FillChoiceUI::None,
|
||||||
Some(color) => FillChoice::Solid(*color),
|
x => {
|
||||||
None => FillChoice::None,
|
warn!("Color {x:?}");
|
||||||
}))
|
return LayoutGroup::row(widgets);
|
||||||
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid().map(Color::from)), node_id, index))
|
}
|
||||||
.on_commit(commit_value)
|
};
|
||||||
.widget_instance(),
|
|
||||||
),
|
// A paint input (`allow_none`) stores the pick as a plain color, gradient, or no-paint type default,
|
||||||
TaggedValue::Gradient(stops) => widgets.push(
|
// while a plain color or gradient input always keeps its own value type
|
||||||
color_button
|
let on_update: fn(&ColorInput) -> TaggedValue = if color_button.allow_none {
|
||||||
.value(FillChoiceUI::from(&FillChoice::Gradient(stops.clone())))
|
|input| match &input.value {
|
||||||
.on_update(update_value(
|
FillChoiceUI::None => TaggedValue::no_paint(),
|
||||||
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().map(GradientStops::from).unwrap_or_default()),
|
FillChoiceUI::Solid(srgba) => TaggedValue::Color(Color::from(*srgba)),
|
||||||
node_id,
|
FillChoiceUI::Gradient(gradient_ui) => TaggedValue::Gradient(Gradient::from(gradient_ui)),
|
||||||
index,
|
}
|
||||||
))
|
} else if matches!(&**tagged_value, TaggedValue::Gradient(_)) {
|
||||||
.on_commit(commit_value)
|
|input| TaggedValue::Gradient(input.value.as_gradient().map(Gradient::from).unwrap_or_default())
|
||||||
.widget_instance(),
|
} else {
|
||||||
),
|
|input| TaggedValue::Color(input.value.as_solid().map(Color::from).unwrap_or(Color::TRANSPARENT))
|
||||||
x => warn!("Color {x:?}"),
|
};
|
||||||
}
|
|
||||||
|
widgets.push(
|
||||||
|
color_button
|
||||||
|
.value(widget_value)
|
||||||
|
.on_update(update_value(on_update, node_id, index))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
);
|
||||||
|
|
||||||
LayoutGroup::row(widgets)
|
LayoutGroup::row(widgets)
|
||||||
}
|
}
|
||||||
@@ -1258,8 +1303,8 @@ pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 2-stop black-to-white gradient track for spectrum sliders that map a value to a grayscale axis.
|
/// 2-stop black-to-white gradient track for spectrum sliders that map a value to a grayscale axis.
|
||||||
fn bw_track() -> GradientStops {
|
fn bw_track() -> Gradient {
|
||||||
GradientStops {
|
Gradient {
|
||||||
position: vec![0., 1.],
|
position: vec![0., 1.],
|
||||||
midpoint: vec![0.5, 0.5],
|
midpoint: vec![0.5, 0.5],
|
||||||
color: vec![Color::BLACK, Color::WHITE],
|
color: vec![Color::BLACK, Color::WHITE],
|
||||||
@@ -1267,8 +1312,8 @@ fn bw_track() -> GradientStops {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 3-stop black-to-color-to-white gradient track for spectrum sliders that map a value to a hue's full luminance range.
|
/// 3-stop black-to-color-to-white gradient track for spectrum sliders that map a value to a hue's full luminance range.
|
||||||
fn color_track(color: Color) -> GradientStops {
|
fn color_track(color: Color) -> Gradient {
|
||||||
GradientStops {
|
Gradient {
|
||||||
position: vec![0., 0.5, 1.],
|
position: vec![0., 0.5, 1.],
|
||||||
midpoint: vec![0.5; 3],
|
midpoint: vec![0.5; 3],
|
||||||
color: vec![Color::BLACK, color, Color::WHITE],
|
color: vec![Color::BLACK, color, Color::WHITE],
|
||||||
@@ -1303,7 +1348,7 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
|||||||
|
|
||||||
let contrast_min = if use_classic_value { -100. } else { -50. };
|
let contrast_min = if use_classic_value { -100. } else { -50. };
|
||||||
let zero_position = -contrast_min / (100. - contrast_min);
|
let zero_position = -contrast_min / (100. - contrast_min);
|
||||||
let contrast_track = GradientStops {
|
let contrast_track = Gradient {
|
||||||
position: vec![0., zero_position, 1.],
|
position: vec![0., zero_position, 1.],
|
||||||
midpoint: vec![0.5; 3],
|
midpoint: vec![0.5; 3],
|
||||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::BLACK, Color::from_rgbf32_unchecked(0.5, 0.5, 0.5)],
|
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::BLACK, Color::from_rgbf32_unchecked(0.5, 0.5, 0.5)],
|
||||||
@@ -1400,7 +1445,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
|
|
||||||
// Build the shared spectrum widget (placed on the first non-exposed row)
|
// Build the shared spectrum widget (placed on the first non-exposed row)
|
||||||
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
||||||
SpectrumInput::new(GradientStopsUI::from(&bw_track()))
|
SpectrumInput::new(GradientUI::from(&bw_track()))
|
||||||
.markers(spectrum_markers)
|
.markers(spectrum_markers)
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
.allow_insert(false)
|
.allow_insert(false)
|
||||||
@@ -1493,13 +1538,13 @@ pub(crate) fn hue_saturation_properties(node_id: NodeId, context: &mut NodePrope
|
|||||||
let saturated_current_hue = Color::from_hsva(marker_hue, 1., 1., 1.);
|
let saturated_current_hue = Color::from_hsva(marker_hue, 1., 1., 1.);
|
||||||
|
|
||||||
// Hue: cyclic rainbow
|
// Hue: cyclic rainbow
|
||||||
let hue_track = GradientStops {
|
let hue_track = Gradient {
|
||||||
position: vec![0., 1. / 6., 2. / 6., 3. / 6., 4. / 6., 5. / 6., 1.],
|
position: vec![0., 1. / 6., 2. / 6., 3. / 6., 4. / 6., 5. / 6., 1.],
|
||||||
midpoint: vec![0.5; 7],
|
midpoint: vec![0.5; 7],
|
||||||
color: vec![Color::RED, Color::YELLOW, Color::GREEN, Color::CYAN, Color::BLUE, Color::MAGENTA, Color::RED],
|
color: vec![Color::RED, Color::YELLOW, Color::GREEN, Color::CYAN, Color::BLUE, Color::MAGENTA, Color::RED],
|
||||||
};
|
};
|
||||||
// Saturation: gray to the fully saturated current hue
|
// Saturation: gray to the fully saturated current hue
|
||||||
let saturation_track = GradientStops {
|
let saturation_track = Gradient {
|
||||||
position: vec![0., 1.],
|
position: vec![0., 1.],
|
||||||
midpoint: vec![0.5, 0.5],
|
midpoint: vec![0.5, 0.5],
|
||||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), saturated_current_hue],
|
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), saturated_current_hue],
|
||||||
@@ -1549,7 +1594,7 @@ fn spectrum_slider_row(
|
|||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
context: &mut NodePropertiesContext,
|
context: &mut NodePropertiesContext,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
track: GradientStops,
|
track: Gradient,
|
||||||
handle_color: Color,
|
handle_color: Color,
|
||||||
value_min: f64,
|
value_min: f64,
|
||||||
value_max: f64,
|
value_max: f64,
|
||||||
@@ -1574,7 +1619,7 @@ fn spectrum_slider_row(
|
|||||||
|
|
||||||
let position_to_value = move |position: f64| value_min + position * value_range;
|
let position_to_value = move |position: f64| value_min + position * value_range;
|
||||||
row.push(
|
row.push(
|
||||||
SpectrumInput::new(GradientStopsUI::from(&track))
|
SpectrumInput::new(GradientUI::from(&track))
|
||||||
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
.allow_insert(false)
|
.allow_insert(false)
|
||||||
@@ -1634,7 +1679,7 @@ pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
pub(crate) fn vibrance_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn vibrance_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::raster::vibrance::*;
|
use graphene_std::raster::vibrance::*;
|
||||||
|
|
||||||
let track = GradientStops {
|
let track = Gradient {
|
||||||
position: vec![0., 1.],
|
position: vec![0., 1.],
|
||||||
midpoint: vec![0.5, 0.5],
|
midpoint: vec![0.5, 0.5],
|
||||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::RED],
|
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::RED],
|
||||||
@@ -2167,7 +2212,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
use graphene_std::vector::generator_nodes::rectangle::*;
|
use graphene_std::vector::generator_nodes::rectangle::*;
|
||||||
|
|
||||||
// Corner Radius
|
// Corner Radius
|
||||||
let mut corner_radius_row_1 = start_widgets(ParameterWidgetsInfo::new(node_id, CornerRadiusInput::<f64>::INDEX, true, context));
|
let mut corner_radius_row_1 = start_widgets(ParameterWidgetsInfo::new(node_id, CornerRadiusInput::INDEX, true, context));
|
||||||
corner_radius_row_1.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
corner_radius_row_1.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
|
|
||||||
let mut corner_radius_row_2 = vec![Separator::new(SeparatorStyle::Unrelated).widget_instance()];
|
let mut corner_radius_row_2 = vec![Separator::new(SeparatorStyle::Unrelated).widget_instance()];
|
||||||
@@ -2187,20 +2232,15 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
};
|
};
|
||||||
if let Some(&TaggedValue::Bool(is_individual)) = input.as_non_exposed_value() {
|
if let Some(&TaggedValue::Bool(is_individual)) = input.as_non_exposed_value() {
|
||||||
// Values
|
// Values
|
||||||
let Some(input) = document_node.inputs.get(CornerRadiusInput::<f64>::INDEX) else {
|
let Some(input) = document_node.inputs.get(CornerRadiusInput::INDEX) else {
|
||||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
let uniform_val = match input.as_non_exposed_value() {
|
let corner_values = match input.as_non_exposed_value() {
|
||||||
Some(TaggedValue::F64(x)) => *x,
|
Some(TaggedValue::BoxCorners(corners)) => corners.to_corner_values(),
|
||||||
Some(TaggedValue::F64Array(values)) => values.first().copied().unwrap_or(0.),
|
_ => [0.; 4],
|
||||||
_ => 0.,
|
|
||||||
};
|
|
||||||
let individual_val = match input.as_non_exposed_value() {
|
|
||||||
Some(&TaggedValue::F64(x)) => vec![x; 4],
|
|
||||||
Some(TaggedValue::F64Array(values)) => values.clone(),
|
|
||||||
_ => vec![0.; 4],
|
|
||||||
};
|
};
|
||||||
|
let uniform_val = corner_values[0];
|
||||||
|
|
||||||
// Uniform/individual radio input widget
|
// Uniform/individual radio input widget
|
||||||
let uniform = RadioEntryData::new("Uniform")
|
let uniform = RadioEntryData::new("Uniform")
|
||||||
@@ -2215,14 +2255,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
.into(),
|
.into(),
|
||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
input_index: CornerRadiusInput::<f64>::INDEX,
|
input_index: CornerRadiusInput::INDEX,
|
||||||
value: TaggedValue::F64(uniform_val),
|
value: TaggedValue::BoxCorners(BoxCorners::from(uniform_val)),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
.on_commit(commit_value);
|
.on_commit(commit_value);
|
||||||
let individual_val_for_switch = individual_val.clone();
|
|
||||||
let individual = RadioEntryData::new("Individual")
|
let individual = RadioEntryData::new("Individual")
|
||||||
.label("Individual")
|
.label("Individual")
|
||||||
.on_update(move |_| Message::Batched {
|
.on_update(move |_| Message::Batched {
|
||||||
@@ -2235,8 +2274,8 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
.into(),
|
.into(),
|
||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
input_index: CornerRadiusInput::<f64>::INDEX,
|
input_index: CornerRadiusInput::INDEX,
|
||||||
value: TaggedValue::F64Array(individual_val_for_switch.clone()),
|
value: TaggedValue::BoxCorners(BoxCorners::from(corner_values.to_vec())),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
]),
|
]),
|
||||||
@@ -2247,24 +2286,23 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||||||
|
|
||||||
// Radius value input widget
|
// Radius value input widget
|
||||||
let input_widget = if is_individual {
|
let input_widget = if is_individual {
|
||||||
let from_string = |string: &str| {
|
|
||||||
string
|
|
||||||
.split(&[',', ' '])
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.map(str::parse::<f64>)
|
|
||||||
.collect::<Result<Vec<f64>, _>>()
|
|
||||||
.ok()
|
|
||||||
.map(|values| TaggedValue::F64Array(values.into_iter().take(4).collect()))
|
|
||||||
};
|
|
||||||
TextInput::default()
|
TextInput::default()
|
||||||
.value(individual_val.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
|
.value(corner_values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
|
||||||
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, CornerRadiusInput::<f64>::INDEX))
|
.on_update(optionally_update_value(
|
||||||
|
move |x: &TextInput| Some(TaggedValue::BoxCorners(BoxCorners::from(x.value.as_str()))),
|
||||||
|
node_id,
|
||||||
|
CornerRadiusInput::INDEX,
|
||||||
|
))
|
||||||
.widget_instance()
|
.widget_instance()
|
||||||
} else {
|
} else {
|
||||||
NumberInput::default()
|
NumberInput::default()
|
||||||
.value(Some(uniform_val))
|
.value(Some(uniform_val))
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, CornerRadiusInput::<f64>::INDEX))
|
.on_update(update_value(
|
||||||
|
move |x: &NumberInput| TaggedValue::BoxCorners(BoxCorners::from(x.value.unwrap())),
|
||||||
|
node_id,
|
||||||
|
CornerRadiusInput::INDEX,
|
||||||
|
))
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
.widget_instance()
|
.widget_instance()
|
||||||
};
|
};
|
||||||
@@ -2333,10 +2371,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 {
|
||||||
@@ -2349,12 +2391,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();
|
||||||
};
|
};
|
||||||
@@ -2442,7 +2486,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
enum ResolvedFill {
|
enum ResolvedFill {
|
||||||
Solid(Option<Color>),
|
Solid(Option<Color>),
|
||||||
Gradient {
|
Gradient {
|
||||||
gradient: GradientStops,
|
gradient: Gradient,
|
||||||
gradient_type: GradientType,
|
gradient_type: GradientType,
|
||||||
spread_method: GradientSpreadMethod,
|
spread_method: GradientSpreadMethod,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
@@ -2452,9 +2496,6 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
let connector = InputConnector::node(node_id, FillInput::<List<Graphic>>::INDEX);
|
|
||||||
let input_type = context.network_interface.input_type(&connector, context.selection_network_path);
|
|
||||||
|
|
||||||
// Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode)
|
// Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode)
|
||||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<List<Graphic>>::INDEX, false, context));
|
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<List<Graphic>>::INDEX, false, context));
|
||||||
|
|
||||||
@@ -2466,51 +2507,42 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
// bounding-box default transform needs the layer, and it falls back to a unit box when there isn't one.
|
// bounding-box default transform needs the layer, and it falls back to a unit box when there isn't one.
|
||||||
let layer = root_layer_for_chain_node(node_id, context);
|
let layer = root_layer_for_chain_node(node_id, context);
|
||||||
|
|
||||||
let fill = match input_type.compiled_nested_type() {
|
let fill = match get_document_node(node_id, context) {
|
||||||
Some(ty) if ty == &concrete!(List<Color>) => {
|
Ok(document_node) => match document_node.inputs[FillInput::<List<Graphic>>::INDEX].as_value() {
|
||||||
if let Ok(document_node) = get_document_node(node_id, context) {
|
Some(TaggedValue::Color(color)) => ResolvedFill::Solid(Some(*color)),
|
||||||
let color = match document_node.inputs[FillInput::<List<Graphic>>::INDEX].as_value() {
|
Some(value) if value.is_no_paint() => ResolvedFill::Solid(None),
|
||||||
Some(&TaggedValue::Color(c)) => c,
|
Some(TaggedValue::Gradient(_)) => {
|
||||||
_ => None,
|
match graph_modification_utils::read_fill_node_gradient(document_node, || {
|
||||||
};
|
|
||||||
ResolvedFill::Solid(color)
|
|
||||||
} else {
|
|
||||||
ResolvedFill::Other
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(ty) if ty == &concrete!(List<GradientStops>) => {
|
|
||||||
// Read this node's own inputs rather than the layer's nearest Fill, which may be a different node when Fills are chained
|
|
||||||
if let Ok(document_node) = get_document_node(node_id, context)
|
|
||||||
&& let Some(gradient) = graph_modification_utils::read_fill_node_gradient(document_node, || {
|
|
||||||
layer.map_or([DVec2::ZERO, DVec2::ONE], |layer| context.network_interface.document_metadata().nonzero_bounding_box(layer))
|
layer.map_or([DVec2::ZERO, DVec2::ONE], |layer| context.network_interface.document_metadata().nonzero_bounding_box(layer))
|
||||||
}) {
|
}) {
|
||||||
ResolvedFill::Gradient {
|
Some(gradient) => ResolvedFill::Gradient {
|
||||||
gradient: gradient.stops,
|
gradient: gradient.stops,
|
||||||
gradient_type: gradient.gradient_type,
|
gradient_type: gradient.gradient_type,
|
||||||
spread_method: gradient.spread_method,
|
spread_method: gradient.spread_method,
|
||||||
transform: gradient.transform,
|
transform: gradient.transform,
|
||||||
transform_is_value: gradient.transform_is_value,
|
transform_is_value: gradient.transform_is_value,
|
||||||
|
},
|
||||||
|
None => ResolvedFill::Other,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ResolvedFill::Other
|
|
||||||
}
|
}
|
||||||
}
|
_ => ResolvedFill::Other,
|
||||||
_ => ResolvedFill::Other,
|
},
|
||||||
|
Err(_) => ResolvedFill::Other,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (backup_color, backup_gradient) = match get_document_node(node_id, context) {
|
let (backup_color, backup_gradient) = match get_document_node(node_id, context) {
|
||||||
Ok(document_node) => {
|
Ok(document_node) => {
|
||||||
let backup_color = match document_node.inputs[BackupColorInput::INDEX].as_value() {
|
let backup_color = match document_node.inputs[BackupColorInput::INDEX].as_value() {
|
||||||
Some(&TaggedValue::Color(color)) => color,
|
Some(&TaggedValue::Color(color)) => Some(color),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let backup_stops = match document_node.inputs[BackupGradientInput::INDEX].as_value() {
|
let backup_stops = match document_node.inputs[BackupGradientInput::INDEX].as_value() {
|
||||||
Some(TaggedValue::Gradient(stops)) => stops.clone(),
|
Some(TaggedValue::Gradient(stops)) => stops.clone(),
|
||||||
_ => GradientStops::default(),
|
_ => Gradient::default(),
|
||||||
};
|
};
|
||||||
(backup_color, backup_stops)
|
(backup_color, backup_stops)
|
||||||
}
|
}
|
||||||
Err(_) => (None, GradientStops::default()),
|
Err(_) => (None, Gradient::default()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match &fill {
|
match &fill {
|
||||||
@@ -2536,28 +2568,33 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
FillChoiceUI::None
|
FillChoiceUI::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStopsUI::from(stops)),
|
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientUI::from(stops)),
|
||||||
ResolvedFill::Other => FillChoiceUI::None,
|
ResolvedFill::Other => FillChoiceUI::None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let solid_set_messages = move |color: Option<Color>| Message::Batched {
|
let solid_set_messages = move |color: Option<Color>| {
|
||||||
messages: Box::new([
|
let mut messages = vec![
|
||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
input_index: FillInput::<List<Graphic>>::INDEX,
|
input_index: FillInput::<List<Graphic>>::INDEX,
|
||||||
value: TaggedValue::Color(color),
|
value: color.map_or_else(TaggedValue::no_paint, TaggedValue::Color),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
NodeGraphMessage::SetInputValue {
|
];
|
||||||
node_id,
|
if let Some(color) = color {
|
||||||
input_index: BackupColorInput::INDEX,
|
messages.push(
|
||||||
value: TaggedValue::Color(color),
|
NodeGraphMessage::SetInputValue {
|
||||||
}
|
node_id,
|
||||||
.into(),
|
input_index: BackupColorInput::INDEX,
|
||||||
]),
|
value: TaggedValue::Color(color),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Message::Batched { messages: messages.into() }
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradient_set_messages = move |gradient: GradientStops| Message::Batched {
|
let gradient_set_messages = move |gradient: Gradient| Message::Batched {
|
||||||
messages: Box::new([
|
messages: Box::new([
|
||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
@@ -2585,7 +2622,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
solid_set_messages(color)
|
solid_set_messages(color)
|
||||||
}
|
}
|
||||||
FillChoiceUI::Gradient(gradient_stops_ui) => {
|
FillChoiceUI::Gradient(gradient_stops_ui) => {
|
||||||
let gradient = GradientStops::from(gradient_stops_ui);
|
let gradient = Gradient::from(gradient_stops_ui);
|
||||||
gradient_set_messages(gradient)
|
gradient_set_messages(gradient)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2602,7 +2639,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
let entries = vec![
|
let entries = vec![
|
||||||
RadioEntryData::new("solid")
|
RadioEntryData::new("solid")
|
||||||
.label("Solid")
|
.label("Solid")
|
||||||
.on_update(update_value(move |_| TaggedValue::Color(backup_color), node_id, FillInput::<List<Graphic>>::INDEX))
|
.on_update(update_value(
|
||||||
|
move |_| backup_color.map_or_else(TaggedValue::no_paint, TaggedValue::Color),
|
||||||
|
node_id,
|
||||||
|
FillInput::<List<Graphic>>::INDEX,
|
||||||
|
))
|
||||||
.on_commit(commit_value),
|
.on_commit(commit_value),
|
||||||
RadioEntryData::new("gradient")
|
RadioEntryData::new("gradient")
|
||||||
.label("Gradient")
|
.label("Gradient")
|
||||||
@@ -2668,7 +2709,22 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
} else {
|
} else {
|
||||||
"Swap the start and end points of the gradient line."
|
"Swap the start and end points of the gradient line."
|
||||||
})
|
})
|
||||||
.on_update(update_value(move |_| TaggedValue::OptionalDAffine2(Some(new_transform)), node_id, TransformInput::INDEX))
|
.on_update(move |_| Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: HasTransformInput::INDEX,
|
||||||
|
value: TaggedValue::Bool(true),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: TransformInput::INDEX,
|
||||||
|
value: TaggedValue::DAffine2(new_transform),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
})
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
spread_methods_row.push(reverse_direction_button);
|
spread_methods_row.push(reverse_direction_button);
|
||||||
@@ -2712,8 +2768,8 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||||||
_ => &StrokeJoin::Miter,
|
_ => &StrokeJoin::Miter,
|
||||||
};
|
};
|
||||||
|
|
||||||
let has_dash_lengths = match &document_node.inputs[DashLengthsInput::<List<f64>>::INDEX].as_value() {
|
let has_dash_lengths = match &document_node.inputs[DashPatternInput::INDEX].as_value() {
|
||||||
Some(TaggedValue::F64Array(values)) => values.is_empty(),
|
Some(TaggedValue::DashPattern(pattern)) => pattern.0.is_empty(),
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
|
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
|
||||||
@@ -2739,10 +2795,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||||||
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context))
|
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context))
|
||||||
.property_row();
|
.property_row();
|
||||||
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
|
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
|
||||||
let dash_lengths = array_of_number_widget(
|
let dash_lengths = dash_pattern_widget(ParameterWidgetsInfo::new(node_id, DashPatternInput::INDEX, true, context), TextInput::default().centered(true));
|
||||||
ParameterWidgetsInfo::new(node_id, DashLengthsInput::<List<f64>>::INDEX, true, context),
|
|
||||||
TextInput::default().centered(true),
|
|
||||||
);
|
|
||||||
let number_input = disabled_number_input;
|
let number_input = disabled_number_input;
|
||||||
let dash_offset = number_widget(ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context), number_input);
|
let dash_offset = number_widget(ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context), number_input);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graphene_std::{Type, simplify_identifier_name};
|
||||||
use graphene_std::Type;
|
|
||||||
use graphene_std::list::List;
|
|
||||||
use graphene_std::raster_types::{CPU, Raster};
|
|
||||||
use graphene_std::vector::Vector;
|
|
||||||
use graphene_std::{Artboard, Graphic};
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -24,20 +19,22 @@ pub enum FrontendGraphDataType {
|
|||||||
|
|
||||||
impl FrontendGraphDataType {
|
impl FrontendGraphDataType {
|
||||||
pub fn from_type(input: &Type) -> Self {
|
pub fn from_type(input: &Type) -> Self {
|
||||||
match TaggedValue::from_type_or_none(input) {
|
// Color a wire by its element type, peeling a rank-0 `Item` or rank-1 `List` wrapper (and a whole-list `Bundle` cell) so all ranks share the element's color
|
||||||
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number,
|
let nested_type = input.nested_type();
|
||||||
TaggedValue::Color(_) => Self::Color,
|
let element = match nested_type.bundle_element_name() {
|
||||||
TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => Self::Gradient,
|
Some(bundle_element) => simplify_identifier_name(bundle_element),
|
||||||
TaggedValue::String(_) => Self::Typography,
|
None => nested_type.list_element().unwrap_or(nested_type).identifier_name(),
|
||||||
// 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 == std::any::type_name::<List<Graphic>>() => Self::Graphic,
|
match element.as_str() {
|
||||||
n if n == std::any::type_name::<List<Artboard>>() => Self::Artboard,
|
"Vector" => Self::Vector,
|
||||||
n if n == std::any::type_name::<List<Raster<CPU>>>() => Self::Raster,
|
"Graphic" => Self::Graphic,
|
||||||
n if n == std::any::type_name::<List<Vector>>() => Self::Vector,
|
"Artboard" => Self::Artboard,
|
||||||
n if n == std::any::type_name::<List<String>>() => Self::Typography,
|
"Color" => Self::Color,
|
||||||
_ => Self::General,
|
"Gradient" => Self::Gradient,
|
||||||
},
|
"String" => Self::Typography,
|
||||||
|
"f64" | "f32" | "u32" | "u64" | "bool" | "DVec2" | "DAffine2" => Self::Number,
|
||||||
|
raster if raster.starts_with("Raster") => Self::Raster,
|
||||||
_ => Self::General,
|
_ => Self::General,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,3 +708,103 @@ async fn demo_artwork_edit_autosaves_and_round_trips() {
|
|||||||
// Autosaving the undone state still round-trips cleanly (no drift panic).
|
// Autosaving the undone state still round-trips cleanly (no drift panic).
|
||||||
editor.active_document_mut().commit_storage_snapshot(&byte_store, true);
|
editor.active_document_mut().commit_storage_snapshot(&byte_store, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The document's single Fill node, as `(network_path, node_id)`.
|
||||||
|
fn find_fill_node(document: &DocumentMessageHandler) -> (Vec<graph_craft::document::NodeId>, graph_craft::document::NodeId) {
|
||||||
|
node_paths(&document.network_interface)
|
||||||
|
.into_iter()
|
||||||
|
.find(|(network_path, node_id)| {
|
||||||
|
let Some(network) = document.network_interface.nested_network(network_path) else { return false };
|
||||||
|
network.nodes[node_id].implementation == graph_craft::document::DocumentNodeImplementation::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER)
|
||||||
|
})
|
||||||
|
.expect("the document should contain a Fill node")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stored paint value of the document's single Fill node.
|
||||||
|
fn fill_paint_value(document: &DocumentMessageHandler) -> graph_craft::document::value::TaggedValue {
|
||||||
|
use graphene_std::NodeInputDecleration as _;
|
||||||
|
|
||||||
|
let (network_path, node_id) = find_fill_node(document);
|
||||||
|
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||||
|
let input = network.nodes[&node_id]
|
||||||
|
.inputs
|
||||||
|
.get(graphene_std::vector::fill::FillInput::<graphene_std::list::List<graphene_std::Graphic>>::INDEX)
|
||||||
|
.expect("Fill should have a paint input");
|
||||||
|
input.as_value().expect("the paint input should hold a value").clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn none_fill_survives_document_reopen() {
|
||||||
|
use graphene_std::NodeInputDecleration as _;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
// Pick the red-slash "none" paint, stored the same way as the Fill widget's None choice
|
||||||
|
let (_, fill_node_id) = find_fill_node(editor.active_document());
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
|
node_id: fill_node_id,
|
||||||
|
input_index: graphene_std::vector::fill::FillInput::<graphene_std::list::List<graphene_std::Graphic>>::INDEX,
|
||||||
|
value: graph_craft::document::value::TaggedValue::no_paint(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(fill_paint_value(editor.active_document()).is_no_paint(), "the None pick should store as no_paint");
|
||||||
|
|
||||||
|
// Reopen through the editor's real open path, which runs the document migrations
|
||||||
|
let serialized = editor.active_document().serialize_document();
|
||||||
|
editor
|
||||||
|
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: None,
|
||||||
|
document_path: None,
|
||||||
|
document_serialized_content: serialized,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let reopened_paint = fill_paint_value(editor.active_document());
|
||||||
|
assert!(reopened_paint.is_no_paint(), "a none fill should survive reopening, but the stored paint became {reopened_paint:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::NodeInputDecleration as _;
|
||||||
|
|
||||||
|
// A minimal master-era document: a 4-input Fill (content, fill: wired, backup color, backup gradient) fed by another node
|
||||||
|
const LEGACY_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Node":{"node_id":2,"output_index":0,"lambda":false}},{"Value":{"tagged_value":{"OptionalColor":null},"exposed":false}},{"Value":{"tagged_value":{"Gradient":{"stops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]],"gradient_type":"Linear","start":[0.0,0.5],"end":[1.0,0.5],"transform":[1.0,0.0,0.0,1.0,0.0,0.0]}},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}],[2,{"inputs":[{"Value":{"tagged_value":"None","exposed":false}},{"Value":{"tagged_value":{"GradientStops":[[0.0,{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}],[1.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]]},"exposed":false}},{"Value":{"tagged_value":{"F64":0.5},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::ops::SampleGradientNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Vector Data"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null}],"output_names":["Future<Instances<VectorData>>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}],[2,{"persistent_metadata":{"reference":"Sample Gradient","display_name":"","input_properties":[{"input_data":{"input_name":"Primary"},"widget_override":null},{"input_data":{"input_name":"Gradient"},"widget_override":null},{"input_data":{"input_name":"Position"},"widget_override":null}],"output_names":["Future<Color>"],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[-20,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"legacy_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
|
||||||
|
|
||||||
|
// Deserializing alone must succeed, so a failure below is attributable to the migrations
|
||||||
|
DocumentMessageHandler::deserialize_document(LEGACY_DOCUMENT).expect("the legacy document should deserialize");
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor
|
||||||
|
.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||||
|
document_name: None,
|
||||||
|
document_path: None,
|
||||||
|
document_serialized_content: LEGACY_DOCUMENT.to_string(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let (network_path, node_id) = find_fill_node(document);
|
||||||
|
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
|
||||||
|
let inputs = &network.nodes[&node_id].inputs;
|
||||||
|
|
||||||
|
assert_eq!(inputs.len(), 8, "the legacy Fill should upgrade to the 8-input shape");
|
||||||
|
let paint = &inputs[graphene_std::vector::fill::FillInput::<graphene_std::list::List<graphene_std::Graphic>>::INDEX];
|
||||||
|
assert!(
|
||||||
|
matches!(paint, graph_craft::document::NodeInput::Node { .. }),
|
||||||
|
"the wired legacy fill should keep its connection, but became {paint:?}"
|
||||||
|
);
|
||||||
|
let has_transform = inputs[graphene_std::vector::fill::HasTransformInput::INDEX].as_value();
|
||||||
|
assert!(
|
||||||
|
matches!(has_transform, Some(TaggedValue::Bool(_))),
|
||||||
|
"the has-transform input should hold a bool, but became {has_transform:?}"
|
||||||
|
);
|
||||||
|
let transform = inputs[graphene_std::vector::fill::TransformInput::INDEX].as_value();
|
||||||
|
assert!(
|
||||||
|
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||||
|
"the transform input should hold a matrix, but became {transform:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -2499,14 +2499,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 {
|
||||||
@@ -2516,15 +2522,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);
|
||||||
@@ -2533,21 +2539,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6105,6 +6119,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);
|
||||||
|
|||||||
+38
-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,28 @@ 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 == std::any::type_name::<List<Graphic>>() => FrontendGraphDataType::Graphic,
|
|
||||||
n if n == std::any::type_name::<List<Artboard>>() => FrontendGraphDataType::Artboard,
|
|
||||||
n if n == std::any::type_name::<List<Raster<CPU>>>() => FrontendGraphDataType::Raster,
|
|
||||||
n if n == std::any::type_name::<List<Vector>>() => FrontendGraphDataType::Vector,
|
|
||||||
n if n == std::any::type_name::<List<String>>() => FrontendGraphDataType::Typography,
|
|
||||||
_ => FrontendGraphDataType::General,
|
|
||||||
},
|
|
||||||
_ => FrontendGraphDataType::General,
|
|
||||||
},
|
|
||||||
None => FrontendGraphDataType::General,
|
None => FrontendGraphDataType::General,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the compiled type is a rank-1 `List<T>`, as opposed to a rank-0 `Item<T>` or a bare value.
|
||||||
|
/// A bundled cell carrying a whole list displays as the list it carries.
|
||||||
|
pub fn is_list(&self) -> bool {
|
||||||
|
self.compiled_nested_type().is_some_and(|ty| matches!(ty, Type::List(_)) || ty.bundle_element_name().is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element type's identifier name with any rank-0 `Item` or rank-1 `List` wrapper peeled, so semantic type checks can be rank-agnostic.
|
||||||
|
pub fn compiled_element_name(&self) -> Option<String> {
|
||||||
|
let nested_type = self.compiled_nested_type()?;
|
||||||
|
// A rank-0 `Item` or rank-1 `List` peels to its element; a bare value reports itself
|
||||||
|
let element = match nested_type {
|
||||||
|
Type::Item(element) | Type::List(element) => element.as_ref(),
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
Some(element.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 +202,19 @@ impl NodeNetworkInterface {
|
|||||||
concrete!(())
|
concrete!(())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A List-typed default drops to its Item counterpart (when that has a default value and the connector accepts rank 0),
|
||||||
|
// since a stored Item default can promote back onto a List connector but a stored List default can never return to rank 0
|
||||||
|
if let Some(element) = guaranteed_type.nested_type().list_element()
|
||||||
|
&& let Some(item_type) = self
|
||||||
|
.potential_valid_input_types(input_connector, network_path)
|
||||||
|
.into_iter()
|
||||||
|
.find(|ty| matches!(ty.nested_type(), Type::Item(item_element) if item_element.as_ref() == element))
|
||||||
|
&& let Some(item_default) = TaggedValue::from_type(&item_type)
|
||||||
|
{
|
||||||
|
return item_default;
|
||||||
|
}
|
||||||
|
|
||||||
TaggedValue::from_type_or_none(&guaranteed_type)
|
TaggedValue::from_type_or_none(&guaranteed_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,12 +342,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;
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp
|
|||||||
use crate::messages::prelude::DocumentMessageHandler;
|
use crate::messages::prelude::DocumentMessageHandler;
|
||||||
use glam::{DVec2, IVec2};
|
use glam::{DVec2, IVec2};
|
||||||
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
|
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
|
||||||
use graph_craft::descriptor;
|
|
||||||
use graph_craft::document::DocumentNode;
|
use graph_craft::document::DocumentNode;
|
||||||
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
|
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
|
||||||
|
use graph_craft::{Type, item};
|
||||||
|
use graphene_std::Color;
|
||||||
|
use graphene_std::NodeInputDecleration;
|
||||||
use graphene_std::ProtoNodeIdentifier;
|
use graphene_std::ProtoNodeIdentifier;
|
||||||
use graphene_std::text::{TextAlign, TypesettingConfig};
|
use graphene_std::text::{TextAlign, TypesettingConfig};
|
||||||
use graphene_std::transform::ScaleType;
|
use graphene_std::transform::ScaleType;
|
||||||
use graphene_std::uuid::NodeId;
|
use graphene_std::uuid::NodeId;
|
||||||
use graphene_std::vector::graphic_types;
|
use graphene_std::vector::graphic_types;
|
||||||
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
|
use graphene_std::vector::misc::BoxCorners;
|
||||||
|
use graphene_std::vector::style::{DashPattern, PaintOrder, StrokeAlign};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
@@ -32,6 +35,9 @@ const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
|
|||||||
("\"OptionalF64\":", "\"F64\":"),
|
("\"OptionalF64\":", "\"F64\":"),
|
||||||
("\"path_bool_nodes::BooleanOperation\"", "\"vector_types::vector::misc::BooleanOperation\""),
|
("\"path_bool_nodes::BooleanOperation\"", "\"vector_types::vector::misc::BooleanOperation\""),
|
||||||
("\"core_types::table::Table<", "\"core_types::list::List<"),
|
("\"core_types::table::Table<", "\"core_types::list::List<"),
|
||||||
|
// The `GradientStops` type was renamed to `Gradient`; stale stored output names are cleared so the display falls back to the live type name
|
||||||
|
("\"output_names\":[\"GradientStops\"]", "\"output_names\":[\"\"]"),
|
||||||
|
("vector_types::gradient::GradientStops", "vector_types::gradient::Gradient"),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub struct NodeReplacement<'a> {
|
pub struct NodeReplacement<'a> {
|
||||||
@@ -87,10 +93,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::animation::animation_time::IDENTIFIER,
|
node: graphene_std::animation::animation_time::IDENTIFIER,
|
||||||
aliases: &["graphene_core::animation::AnimationTimeNode"],
|
aliases: &["graphene_core::animation::AnimationTimeNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::debug::clone::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::ops::CloneNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::extract_xy::extract_xy::IDENTIFIER,
|
node: graphene_std::extract_xy::extract_xy::IDENTIFIER,
|
||||||
aliases: &["graphene_core::ops::ExtractXyNode"],
|
aliases: &["graphene_core::ops::ExtractXyNode"],
|
||||||
@@ -106,6 +108,14 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
"graphene_core::transform_nodes::FreezeRealTimeNode",
|
"graphene_core::transform_nodes::FreezeRealTimeNode",
|
||||||
"graphene_core::vector::SubpathSegmentLengthsNode",
|
"graphene_core::vector::SubpathSegmentLengthsNode",
|
||||||
"core_types::vector::SubpathSegmentLengthsNode",
|
"core_types::vector::SubpathSegmentLengthsNode",
|
||||||
|
// The deleted debug Option trio degrades to a passthrough of its single input (audit resolution 8)
|
||||||
|
"graphene_core::ops::SizeOfNode",
|
||||||
|
"graphene_core::debug::SizeOfNode",
|
||||||
|
"graphene_core::ops::SomeNode",
|
||||||
|
"graphene_core::debug::SomeNode",
|
||||||
|
"graphene_core::ops::UnwrapNode",
|
||||||
|
"graphene_core::debug::UnwrapNode",
|
||||||
|
"graphene_core::debug::UnwrapOptionNode",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
@@ -120,18 +130,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::animation::real_time::IDENTIFIER,
|
node: graphene_std::animation::real_time::IDENTIFIER,
|
||||||
aliases: &["graphene_core::animation::RealTimeNode"],
|
aliases: &["graphene_core::animation::RealTimeNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::debug::size_of::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::ops::SizeOfNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::debug::some::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::ops::SomeNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::debug::unwrap_option::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"],
|
|
||||||
},
|
|
||||||
// ================================
|
// ================================
|
||||||
// graphic
|
// graphic
|
||||||
// ================================
|
// ================================
|
||||||
@@ -161,13 +159,19 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
aliases: &["graphene_core::graphic::FlattenVectorNode", "graphene_core::graphic_element::FlattenVectorNode"],
|
aliases: &["graphene_core::graphic::FlattenVectorNode", "graphene_core::graphic_element::FlattenVectorNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::graphic::index_elements::IDENTIFIER,
|
node: graphene_std::graphic::item_at_index::IDENTIFIER,
|
||||||
aliases: &[
|
aliases: &[
|
||||||
"graphene_core::graphic_element::IndexNode",
|
"graphene_core::graphic_element::IndexNode",
|
||||||
"graphene_core::graphic::IndexNode",
|
"graphene_core::graphic::IndexNode",
|
||||||
"graphene_core::graphic::IndexElementsNode",
|
"graphene_core::graphic::IndexElementsNode",
|
||||||
|
"graphic_nodes::graphic::IndexElementsNode",
|
||||||
|
"graphic_nodes::graphic::ExtractElementNode",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
NodeReplacement {
|
||||||
|
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
||||||
|
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
||||||
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER,
|
node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER,
|
||||||
aliases: &[
|
aliases: &[
|
||||||
@@ -735,8 +739,12 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
// vector
|
// vector
|
||||||
// ================================
|
// ================================
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::apply_transform::IDENTIFIER,
|
node: graphene_std::vector::bake_transform::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::ApplyTransformNode", "graphene_core::vector::vector_modification::ApplyTransformNode"],
|
aliases: &[
|
||||||
|
"graphene_core::vector::ApplyTransformNode",
|
||||||
|
"graphene_core::vector::vector_modification::ApplyTransformNode",
|
||||||
|
"vector_nodes::vector_modification_nodes::ApplyTransformNode",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::area::IDENTIFIER,
|
node: graphene_std::vector::area::IDENTIFIER,
|
||||||
@@ -771,7 +779,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
aliases: &["graphene_core::vector::ClosePathNode"],
|
aliases: &["graphene_core::vector::ClosePathNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::count_elements::IDENTIFIER,
|
node: graphene_std::vector::list_length::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::CountElementsNode"],
|
aliases: &["graphene_core::vector::CountElementsNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
@@ -1121,6 +1129,7 @@ pub fn document_migration_replace_resources_referenced_by_hash(document_serializ
|
|||||||
|
|
||||||
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
|
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
|
||||||
document.network_interface.migrate_path_modify_node();
|
document.network_interface.migrate_path_modify_node();
|
||||||
|
document.network_interface.document_network_mut().normalize_stored_types();
|
||||||
|
|
||||||
let network = document.network_interface.document_network().clone();
|
let network = document.network_interface.document_network().clone();
|
||||||
|
|
||||||
@@ -1285,9 +1294,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()
|
||||||
@@ -1321,7 +1330,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
|
||||||
@@ -1333,44 +1343,62 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a legacy stroke dash input (a `List<f64>`, single `f64`, or comma/space separated `String`) to the `DashPattern` value type.
|
||||||
|
fn migrate_dash_input(input: &NodeInput) -> Option<NodeInput> {
|
||||||
|
let NodeInput::Value { tagged_value, exposed } = input else { return None };
|
||||||
|
let pattern = match &*tagged_value.clone().into_inner() {
|
||||||
|
TaggedValue::F64Array(lengths) => DashPattern::from(lengths.clone()),
|
||||||
|
TaggedValue::F64(length) => DashPattern::from(*length),
|
||||||
|
TaggedValue::String(text) => DashPattern::from(text.as_str()),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(NodeInput::value(TaggedValue::DashPattern(pattern), *exposed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a legacy rectangle corner radius input (a single `f64` or a `List<f64>` of up to four values) to the `BoxCorners` value type.
|
||||||
|
fn migrate_corner_radius_input(input: &NodeInput) -> Option<NodeInput> {
|
||||||
|
let NodeInput::Value { tagged_value, exposed } = input else { return None };
|
||||||
|
let corners = match &*tagged_value.clone().into_inner() {
|
||||||
|
TaggedValue::F64Array(values) => BoxCorners::from(values.clone()),
|
||||||
|
TaggedValue::F64(value) => BoxCorners::from(*value),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(NodeInput::value(TaggedValue::BoxCorners(corners), *exposed))
|
||||||
|
}
|
||||||
|
|
||||||
fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> {
|
fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> {
|
||||||
// Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort
|
// Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort
|
||||||
// `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running.
|
// `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running.
|
||||||
@@ -1572,8 +1600,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);
|
||||||
@@ -1582,21 +1610,21 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
// Content: no change
|
// Content: no change
|
||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||||
|
|
||||||
// Fill: a literal Fill value is decomposed, and a wired input (`List<GradientStops> / List<Color>`) is kept as-is
|
// Fill: a literal Fill value is decomposed, and a wired input (`List<Gradient> / List<Color>`) is kept as-is
|
||||||
match old_inputs[1].as_value() {
|
match old_inputs[1].as_value() {
|
||||||
Some(TaggedValue::LegacyFill(old_fill)) => {
|
Some(TaggedValue::LegacyFill(old_fill)) => {
|
||||||
let exposed = old_inputs[1].is_exposed();
|
let exposed = old_inputs[1].is_exposed();
|
||||||
let fill_value = match old_fill {
|
let fill_value = match old_fill {
|
||||||
graphic_types::migrations::legacy::Fill::None => TaggedValue::Color(None),
|
graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(),
|
||||||
graphic_types::migrations::legacy::Fill::Solid(color) => TaggedValue::Color(Some(*color)),
|
graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color),
|
||||||
graphic_types::migrations::legacy::Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
|
graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
|
||||||
};
|
};
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
|
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
|
||||||
|
|
||||||
// Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults
|
// Gradient metadata (4, 5, 6, 7): applies only to a literal gradient, solids/none keep the template defaults
|
||||||
if let graphic_types::migrations::legacy::Fill::Gradient(gradient) = old_fill {
|
if let graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) = old_fill {
|
||||||
document.network_interface.set_input(
|
document.network_interface.set_input(
|
||||||
&InputConnector::node(*node_id, 4),
|
&InputConnector::node(*node_id, 4),
|
||||||
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||||
@@ -1608,20 +1636,23 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
network_path,
|
network_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
let transform = if gradient.absolute {
|
if gradient.absolute {
|
||||||
Some(gradient.transform * gradient.to_transform())
|
let transform = gradient.transform * gradient.to_transform();
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
|
||||||
} else {
|
} else {
|
||||||
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint target's bounds
|
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint
|
||||||
|
// target's bounds, so the template's unbaked `_has_transform = false` stands until the bake lands
|
||||||
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, gradient.clone()));
|
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, gradient.clone()));
|
||||||
None
|
}
|
||||||
};
|
|
||||||
document
|
|
||||||
.network_interface
|
|
||||||
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wired/exposed fill keeps the connection.
|
// Wired/exposed fill keeps the connection.
|
||||||
// The generic paint connector accepts the existing `List<Color>`/`List<GradientStops>` paint sources directly.
|
// The generic paint connector accepts the existing `List<Color>`/`List<Gradient>` paint sources directly.
|
||||||
_ => {
|
_ => {
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -1640,7 +1671,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
if matches!(
|
if matches!(
|
||||||
old_inputs[1].as_value(),
|
old_inputs[1].as_value(),
|
||||||
Some(TaggedValue::LegacyFill(
|
Some(TaggedValue::LegacyFill(
|
||||||
graphic_types::migrations::legacy::Fill::None | graphic_types::migrations::legacy::Fill::Solid(_)
|
graphic_types::migrations::legacy::LegacyFill::None | graphic_types::migrations::legacy::LegacyFill::Solid(_)
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
document
|
document
|
||||||
@@ -1652,19 +1683,54 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
network_path,
|
network_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
let transform = if g.absolute {
|
if g.absolute {
|
||||||
Some(g.transform * g.to_transform())
|
let transform = g.transform * g.to_transform();
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
|
||||||
} else {
|
} else {
|
||||||
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone()));
|
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone()));
|
||||||
None
|
}
|
||||||
};
|
|
||||||
document
|
|
||||||
.network_interface
|
|
||||||
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs_count = 7;
|
inputs_count = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill split its `Option<DAffine2>` placement into a `_has_transform` bool immediately before the `_transform` matrix
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) && inputs_count == 7 {
|
||||||
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
|
||||||
|
for (index, input) in old_inputs.iter().enumerate().take(6) {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
match old_inputs.get(6).and_then(|input| input.as_value()) {
|
||||||
|
Some(TaggedValue::LegacyOptionalDAffine2(value)) => {
|
||||||
|
let has_transform = value.is_some();
|
||||||
|
let transform = value.unwrap_or(glam::DAffine2::IDENTITY);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(has_transform), false), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
|
||||||
|
}
|
||||||
|
// A wired (or otherwise non-value) transform keeps its connection and is treated as present
|
||||||
|
_ => {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||||
|
let transform_input = old_inputs.get(6).cloned().unwrap_or_else(|| NodeInput::value(TaggedValue::DAffine2(glam::DAffine2::IDENTITY), false));
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 7), transform_input, network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs_count = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||||
@@ -1683,10 +1749,111 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
|
let dash_input = migrate_dash_input(&old_inputs[3]).unwrap_or_else(|| old_inputs[3].clone());
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 8), dash_input, network_path);
|
||||||
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
|
// A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color,
|
||||||
|
// since only paint connectors keep the no-paint choice
|
||||||
|
{
|
||||||
|
let migrate_color_input = |input: &NodeInput, fallback: Color| -> Option<NodeInput> {
|
||||||
|
let NodeInput::Value { tagged_value, exposed } = input else { return None };
|
||||||
|
if !tagged_value.is_no_paint() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(NodeInput::value(TaggedValue::Color(fallback), *exposed))
|
||||||
|
};
|
||||||
|
|
||||||
|
let conversions: &[(ProtoNodeIdentifier, usize, Color)] = &[
|
||||||
|
(graphene_std::vector::fill::IDENTIFIER, graphene_std::vector::fill::BackupColorInput::INDEX, Color::BLACK),
|
||||||
|
(
|
||||||
|
graphene_std::artboard::create_artboard::IDENTIFIER,
|
||||||
|
graphene_std::artboard::create_artboard::BackgroundInput::INDEX,
|
||||||
|
Color::WHITE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
graphene_std::math_nodes::color_value::IDENTIFIER,
|
||||||
|
graphene_std::math_nodes::color_value::ColorInput::INDEX,
|
||||||
|
Color::TRANSPARENT,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
graphene_std::raster_nodes::adjustments::black_and_white::IDENTIFIER,
|
||||||
|
graphene_std::raster_nodes::adjustments::black_and_white::TintInput::INDEX,
|
||||||
|
Color::BLACK,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER,
|
||||||
|
graphene_std::raster_nodes::blending_nodes::color_overlay::ColorInput::INDEX,
|
||||||
|
Color::BLACK,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
graphene_std::raster_nodes::std_nodes::empty_image::IDENTIFIER,
|
||||||
|
graphene_std::raster_nodes::std_nodes::empty_image::ColorInput::INDEX,
|
||||||
|
Color::WHITE,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for &(ref identifier, index, fallback) in conversions {
|
||||||
|
if reference != DefinitionIdentifier::ProtoNode(identifier.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(input) = node.inputs.get(index) else { continue };
|
||||||
|
if let Some(migrated) = migrate_color_input(input, fallback) {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, index), migrated, network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stroke dash sequence became the `DashPattern` value type; convert any already-shaped stroke that still stores a legacy dash input
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER)
|
||||||
|
&& let Some(dash_input) = node.inputs.get(graphene_std::vector::stroke::DashPatternInput::INDEX)
|
||||||
|
&& let Some(migrated) = migrate_dash_input(dash_input)
|
||||||
|
{
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, graphene_std::vector::stroke::DashPatternInput::INDEX), migrated, network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rectangle's corner radius became the `BoxCorners` value type and its hidden individual-radii toggle moved after the
|
||||||
|
// user-visible inputs. A legacy rectangle stores that toggle (a plain `bool`) at index 3, where the new shape stores the corner
|
||||||
|
// radius, so a `bool` value there identifies the old input order: [width, height, individual, corner_radius, clamped].
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER)
|
||||||
|
&& let Some(toggle_input) = node.inputs.get(3)
|
||||||
|
&& matches!(toggle_input.as_value(), Some(TaggedValue::Bool(_)))
|
||||||
|
{
|
||||||
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
|
||||||
|
let corner_radius = migrate_corner_radius_input(&old_inputs[4]).unwrap_or_else(|| old_inputs[4].clone());
|
||||||
|
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||||
|
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, 2), old_inputs[2].clone(), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 3), corner_radius, network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Text to Vector node's runtime `separate_glyphs` toggle became the dedicated "Text to Vector Glyphs" node, leaving Text to Vector as a plain
|
||||||
|
// string-to-compound-path converter. A 2-input Text to Vector is the old toggled shape: a `true` toggle routes to Text to Vector Glyphs, otherwise
|
||||||
|
// the node stays Text to Vector; either way the toggle input is dropped and the string wire is preserved.
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER) && inputs_count == 2 {
|
||||||
|
let separate_glyphs = matches!(node.inputs.get(1).and_then(|input| input.as_value()), Some(TaggedValue::Bool(true)));
|
||||||
|
let target = if separate_glyphs {
|
||||||
|
graphene_std::text::text_to_vector_glyphs::IDENTIFIER
|
||||||
|
} else {
|
||||||
|
graphene_std::text::text_to_vector::IDENTIFIER
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut node_template = resolve_proto_node_type(target)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
if let Some(string_input) = node.inputs.first() {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), string_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
||||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 {
|
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 {
|
||||||
let mut template: NodeTemplate = legacy_text_node_template()?;
|
let mut template: NodeTemplate = legacy_text_node_template()?;
|
||||||
@@ -1986,6 +2153,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);
|
||||||
@@ -2202,7 +2376,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();
|
||||||
@@ -2217,14 +2391,14 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Count Elements node: counts content `List` items → N
|
// Create List Length node: counts content `List` items → N
|
||||||
let Some(count_elements_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::count_elements::IDENTIFIER)) else {
|
let Some(list_length_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::list_length::IDENTIFIER)) else {
|
||||||
log::error!("Could not get count_elements node from definition when upgrading morph");
|
log::error!("Could not get list_length node from definition when upgrading morph");
|
||||||
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);
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let count_elements_template = count_elements_def.default_node_template();
|
let list_length_template = list_length_def.default_node_template();
|
||||||
let count_elements_id = NodeId::new();
|
let list_length_id = NodeId::new();
|
||||||
|
|
||||||
// Create Subtract node: N → N-1
|
// Create Subtract node: N → N-1
|
||||||
let Some(subtract_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::subtract::IDENTIFIER)) else {
|
let Some(subtract_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::subtract::IDENTIFIER)) else {
|
||||||
@@ -2246,10 +2420,10 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
let divide_id = NodeId::new();
|
let divide_id = NodeId::new();
|
||||||
|
|
||||||
// Insert and position nodes
|
// Insert and position nodes
|
||||||
document.network_interface.insert_node(count_elements_id, count_elements_template, network_path);
|
document.network_interface.insert_node(list_length_id, list_length_template, network_path);
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.shift_absolute_node_position(&count_elements_id, morph_position + IVec2::new(-21, 2), network_path);
|
.shift_absolute_node_position(&list_length_id, morph_position + IVec2::new(-21, 2), network_path);
|
||||||
|
|
||||||
document.network_interface.insert_node(subtract_id, subtract_template, network_path);
|
document.network_interface.insert_node(subtract_id, subtract_template, network_path);
|
||||||
document.network_interface.shift_absolute_node_position(&subtract_id, morph_position + IVec2::new(-14, 2), network_path);
|
document.network_interface.shift_absolute_node_position(&subtract_id, morph_position + IVec2::new(-14, 2), network_path);
|
||||||
@@ -2257,13 +2431,13 @@ 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(count_elements_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(count_elements_id, 0), network_path);
|
.set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(list_length_id, 0), network_path);
|
||||||
|
|
||||||
// Wire: old progression → Divide input 0 (numerator)
|
// Wire: old progression → Divide input 0 (numerator)
|
||||||
document.network_interface.set_input(&InputConnector::node(divide_id, 0), old_inputs[1].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node(divide_id, 0), old_inputs[1].clone(), network_path);
|
||||||
@@ -2444,11 +2618,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
let modification = modification.clone();
|
let modification = modification.clone();
|
||||||
let was_exposed = *exposed;
|
let was_exposed = *exposed;
|
||||||
|
|
||||||
document.network_interface.set_input(
|
document
|
||||||
&InputConnector::node(*node_id, 0),
|
.network_interface
|
||||||
NodeInput::type_default(descriptor!(graphene_std::list::List<graphene_std::vector::Vector>), true),
|
.set_input(&InputConnector::node(*node_id, 0), NodeInput::type_default(item!(graphene_std::vector::Vector), true), network_path);
|
||||||
network_path,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !was_exposed {
|
if !was_exposed {
|
||||||
document
|
document
|
||||||
@@ -2480,6 +2652,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 matches!(stored_type, Type::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
|
||||||
// ==================================
|
// ==================================
|
||||||
@@ -2565,6 +2765,31 @@ fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, ne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The removed Attach Attribute node (merged into Write Attribute per audit resolution 6) degrades to a passthrough of its
|
||||||
|
// content: its eager whole-list source input cannot be mechanically rewired as Write Attribute's lazy per-item value producer.
|
||||||
|
if let Some(DefinitionIdentifier::ProtoNode(identifier)) = document.network_interface.reference(node_id, network_path)
|
||||||
|
&& identifier.as_str().ends_with("::AttachAttributeNode")
|
||||||
|
{
|
||||||
|
let mut node_template = resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
if let Some(content) = old_inputs.first() {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), content.clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Upload Texture node's old wrapper-network form maps onto its proto node form, which draws the executor from scope
|
||||||
|
if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path)
|
||||||
|
&& name == "Upload Texture"
|
||||||
|
{
|
||||||
|
let mut node_template = resolve_proto_node_type(graphene_std::platform_application_io::upload_texture::IDENTIFIER)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
if let Some(content) = old_inputs.first() {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), content.clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2572,6 +2797,13 @@ fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, ne
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// The removed-definition blocks above abort silently via `?` if their swap target ever leaves the catalog
|
||||||
|
#[test]
|
||||||
|
fn removed_definition_swap_targets_resolve() {
|
||||||
|
assert!(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).is_some());
|
||||||
|
assert!(resolve_proto_node_type(graphene_std::platform_application_io::upload_texture::IDENTIFIER).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_duplicate_node_replacements() {
|
fn test_no_duplicate_node_replacements() {
|
||||||
let mut hashmap = HashMap::<ProtoNodeIdentifier, u32>::new();
|
let mut hashmap = HashMap::<ProtoNodeIdentifier, u32>::new();
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
use graph_craft::ProtoNodeIdentifier;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
|
||||||
use graphene_std::NodeInputDecleration;
|
use graphene_std::NodeInputDecleration;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::raster_types::{CPU, GPU, Image, Raster};
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::misc::ManipulatorPointId;
|
use graphene_std::vector::misc::ManipulatorPointId;
|
||||||
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
|
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
|
||||||
use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{Gradient, GradientSpreadMethod, GradientType, PointId, SegmentId, VectorModificationType};
|
||||||
use graphene_std::{Color, Graphic};
|
use graphene_std::{Color, Graphic};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &No
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the gradient stops of a layer, if any.
|
/// Get the gradient stops of a layer, if any.
|
||||||
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> {
|
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||||
// Try to find the gradient stops value that is created by a Fill node first
|
// Try to find the gradient stops value that is created by a Fill node first
|
||||||
if let Some(fill_node_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
if let Some(fill_node_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
||||||
return network_interface
|
return network_interface
|
||||||
@@ -329,7 +329,7 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
|||||||
Some(stops.clone())
|
Some(stops.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<GradientStops>`
|
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<Gradient>`
|
||||||
/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
|
/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
|
||||||
/// transform with the [0,1]² → bounding-box mapping.
|
/// transform with the [0,1]² → bounding-box mapping.
|
||||||
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
|
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
|
||||||
@@ -363,10 +363,10 @@ pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
|
|||||||
/// Get the current fill of a layer from the closest "Fill" node.
|
/// Get the current fill of a layer from the closest "Fill" node.
|
||||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||||
let &TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? else {
|
let TaggedValue::Color(color) = inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
color
|
Some(*color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
||||||
@@ -570,8 +570,8 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
|||||||
Some(TaggedValue::PaintOrder(value)) => *value,
|
Some(TaggedValue::PaintOrder(value)) => *value,
|
||||||
_ => PaintOrder::default(),
|
_ => PaintOrder::default(),
|
||||||
};
|
};
|
||||||
let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::<List<f64>>::INDEX) {
|
let dash_lengths = match read(graphene_std::vector::stroke::DashPatternInput::INDEX) {
|
||||||
Some(TaggedValue::F64Array(value)) => value.clone(),
|
Some(TaggedValue::DashPattern(value)) => value.0.iter_element_values().copied().collect(),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
let dash_offset = match read(graphene_std::vector::stroke::DashOffsetInput::INDEX) {
|
let dash_offset = match read(graphene_std::vector::stroke::DashOffsetInput::INDEX) {
|
||||||
@@ -626,7 +626,7 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
|
|||||||
|
|
||||||
/// A Fill node's decoded gradient inputs, with the transform kept in its raw form (not yet baked into `start`/`end`).
|
/// A Fill node's decoded gradient inputs, with the transform kept in its raw form (not yet baked into `start`/`end`).
|
||||||
pub struct FillNodeGradient {
|
pub struct FillNodeGradient {
|
||||||
pub stops: GradientStops,
|
pub stops: Gradient,
|
||||||
pub gradient_type: GradientType,
|
pub gradient_type: GradientType,
|
||||||
pub spread_method: GradientSpreadMethod,
|
pub spread_method: GradientSpreadMethod,
|
||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
@@ -649,9 +649,11 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
|
|||||||
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
|
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
|
||||||
_ => GradientSpreadMethod::default(),
|
_ => GradientSpreadMethod::default(),
|
||||||
};
|
};
|
||||||
|
let has_transform = matches!(fill_node.inputs.get(fill::HasTransformInput::INDEX).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true)));
|
||||||
let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value());
|
let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value());
|
||||||
let transform = match transform_input {
|
let transform = match (has_transform, transform_input) {
|
||||||
Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())),
|
(true, Some(&TaggedValue::DAffine2(value))) => value,
|
||||||
|
(false, _) => initial_gradient_transform_for_bounding_box(bounding_box()),
|
||||||
_ => DAffine2::IDENTITY,
|
_ => DAffine2::IDENTITY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -667,7 +669,11 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
|
|||||||
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
|
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
|
||||||
let color_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
|
let color_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
|
||||||
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?;
|
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), color_index)?;
|
||||||
if let TaggedValue::Color(color) = tagged { Some(*color) } else { None }
|
match tagged {
|
||||||
|
TaggedValue::Color(color) => Some(Some(*color)),
|
||||||
|
value if value.is_no_paint() => Some(None),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aggregated fill state across all selected non-artboard layers.
|
/// Aggregated fill state across all selected non-artboard layers.
|
||||||
@@ -687,7 +693,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();
|
||||||
@@ -700,8 +706,9 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<Selected
|
|||||||
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
|
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||||
|
|
||||||
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
|
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
|
||||||
&TaggedValue::Color(color) => Some(color.map_or(FillChoice::None, FillChoice::Solid)),
|
TaggedValue::Color(color) => Some(FillChoice::Solid(*color)),
|
||||||
TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())),
|
TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())),
|
||||||
|
value if value.is_no_paint() => Some(FillChoice::None),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -799,10 +806,10 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
|
|||||||
Some(TaggedValue::GradientSpreadMethod(value)) => *value,
|
Some(TaggedValue::GradientSpreadMethod(value)) => *value,
|
||||||
_ => GradientSpreadMethod::default(),
|
_ => GradientSpreadMethod::default(),
|
||||||
};
|
};
|
||||||
let transform = match read(graphene_std::vector::fill::TransformInput::INDEX) {
|
let has_transform = matches!(read(graphene_std::vector::fill::HasTransformInput::INDEX), Some(TaggedValue::Bool(true)));
|
||||||
Some(TaggedValue::OptionalDAffine2(value)) => {
|
let transform = match (has_transform, read(graphene_std::vector::fill::TransformInput::INDEX)) {
|
||||||
value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)))
|
(true, Some(TaggedValue::DAffine2(value))) => *value,
|
||||||
}
|
(false, _) => initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)),
|
||||||
_ => DAffine2::IDENTITY,
|
_ => DAffine2::IDENTITY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -826,7 +833,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
|
|||||||
for layer in layers {
|
for layer in layers {
|
||||||
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
if let Some(node_id) = get_stroke_id(layer, &document.network_interface) {
|
||||||
let input_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
|
let input_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
|
||||||
let value = TaggedValue::Color(color);
|
let value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||||
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
||||||
} else {
|
} else {
|
||||||
let stroke = graphene_std::vector::style::Stroke::new(weight);
|
let stroke = graphene_std::vector::style::Stroke::new(weight);
|
||||||
@@ -977,6 +984,6 @@ impl<'a> NodeGraphLayer<'a> {
|
|||||||
pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool {
|
pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool {
|
||||||
let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]);
|
let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]);
|
||||||
|
|
||||||
layer_input_type.compiled_nested_type() == Some(&concrete!(List<Raster<CPU>>)) || layer_input_type.compiled_nested_type() == Some(&concrete!(List<Raster<GPU>>))
|
matches!(layer_input_type.compiled_element_name().as_deref(), Some("Raster<CPU>" | "Raster<GPU>"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
|
|||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::NodeInputDecleration;
|
use graphene_std::NodeInputDecleration;
|
||||||
use graphene_std::choice_type::ChoiceTypeStatic;
|
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||||
use graphene_std::list::List;
|
|
||||||
use graphene_std::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
|
||||||
/// All non-color stroke-related options surfaced in the control bar popover.
|
/// All non-color stroke-related options surfaced in the control bar popover.
|
||||||
@@ -215,7 +214,7 @@ pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, docu
|
|||||||
|
|
||||||
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
drawing.dash_lengths = Some(lengths.clone());
|
drawing.dash_lengths = Some(lengths.clone());
|
||||||
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashLengthsInput::<List<f64>>::INDEX, TaggedValue::F64Array(lengths), responses);
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashPatternInput::INDEX, TaggedValue::DashPattern(lengths.into()), responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_dash_offset(drawing: &mut DrawingToolState, offset: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
pub fn apply_dash_offset(drawing: &mut DrawingToolState, offset: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ use crate::messages::tool::common_functionality::transformation_cage::SelectedEd
|
|||||||
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
||||||
use crate::messages::tool::utility_types::ToolType;
|
use crate::messages::tool::utility_types::ToolType;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::concrete;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::list::List;
|
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||||
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
||||||
@@ -568,11 +566,11 @@ 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), &[]);
|
||||||
if output_type.compiled_nested_type() != Some(&concrete!(List<Vector>)) {
|
if output_type.compiled_element_name().as_deref() != Some("Vector") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,17 +205,19 @@ impl Fsm for FillToolFsmState {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_fill {
|
mod test_fill {
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
|
use graphene_std::Graphic;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::{Item, List};
|
||||||
use graphene_std::vector::fill;
|
use graphene_std::vector::fill;
|
||||||
|
|
||||||
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<List<Color>> {
|
// The Fill tool writes solid colors, whose stored values the input monitor records as `Item<Color>` wires
|
||||||
|
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Item<Color>> {
|
||||||
let instrumented = match editor.eval_graph().await {
|
let instrumented = match editor.eval_graph().await {
|
||||||
Ok(instrumented) => instrumented,
|
Ok(instrumented) => instrumented,
|
||||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
instrumented.grab_all_input::<fill::FillInput<List<Color>>>(&editor.runtime).collect()
|
instrumented.grab_all_input_as::<fill::FillInput<List<Graphic>>, Item<Color>>(&editor.runtime).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -245,7 +247,7 @@ mod test_fill {
|
|||||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
let fills = get_fills(&mut editor).await;
|
let fills = get_fills(&mut editor).await;
|
||||||
assert_eq!(fills.len(), 1);
|
assert_eq!(fills.len(), 1);
|
||||||
let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
|
let color = fills.first().unwrap().element();
|
||||||
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN));
|
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,7 @@ mod test_fill {
|
|||||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
|
||||||
let fills = get_fills(&mut editor).await;
|
let fills = get_fills(&mut editor).await;
|
||||||
assert_eq!(fills.len(), 1);
|
assert_eq!(fills.len(), 1);
|
||||||
let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
|
let color = fills.first().unwrap().element();
|
||||||
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW));
|
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use glam::DMat2;
|
|||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType, build_transform_with_y_preservation};
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI, Gradient, GradientSpreadMethod, GradientStop, GradientType, GradientUI, build_transform_with_y_preservation};
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
pub struct GradientTool {
|
pub struct GradientTool {
|
||||||
@@ -53,7 +53,7 @@ pub enum GradientToolMessage {
|
|||||||
CommitTransactionForColorStop,
|
CommitTransactionForColorStop,
|
||||||
CloseStopColorPicker,
|
CloseStopColorPicker,
|
||||||
UpdateStopColor { color: Color },
|
UpdateStopColor { color: Color },
|
||||||
UpdateStops { stops: GradientStopsUI },
|
UpdateStops { stops: GradientUI },
|
||||||
UpdateOptions { options: GradientOptionsUpdate },
|
UpdateOptions { options: GradientOptionsUpdate },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
||||||
apply_stops_update(&mut self.data, context, responses, GradientStops::from(&stops));
|
apply_stops_update(&mut self.data, context, responses, Gradient::from(&stops));
|
||||||
}
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
||||||
if self.data.color_picker_transaction_open {
|
if self.data.color_picker_transaction_open {
|
||||||
@@ -264,7 +264,7 @@ impl LayoutHolder for GradientTool {
|
|||||||
.or_else(|| self.data.default_gradient_stops.clone())
|
.or_else(|| self.data.default_gradient_stops.clone())
|
||||||
.map(FillChoice::Gradient)
|
.map(FillChoice::Gradient)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
FillChoice::Gradient(GradientStops::new([
|
FillChoice::Gradient(Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -389,7 +389,7 @@ enum GradientSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the gradient with appearance information from Fill node values, or the chain connected to Fill node / layer.
|
/// Get the gradient with appearance information from Fill node values, or the chain connected to Fill node / layer.
|
||||||
fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(GradientStops, GradientAppearance, GradientSource)> {
|
fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(Gradient, GradientAppearance, GradientSource)> {
|
||||||
if let Some(stops) = get_gradient_stops(layer, network_interface) {
|
if let Some(stops) = get_gradient_stops(layer, network_interface) {
|
||||||
// A Fill node holding a direct gradient value decodes through the shared reader
|
// A Fill node holding a direct gradient value decodes through the shared reader
|
||||||
if let Some(fill_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
if let Some(fill_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
||||||
@@ -521,15 +521,15 @@ struct SelectedGradient {
|
|||||||
dragging: GradientDragTarget,
|
dragging: GradientDragTarget,
|
||||||
/// Transform from the geometry's local gradient space to viewport space.
|
/// Transform from the geometry's local gradient space to viewport space.
|
||||||
gradient_space_transform: DAffine2,
|
gradient_space_transform: DAffine2,
|
||||||
gradient: GradientStops,
|
gradient: Gradient,
|
||||||
appearance: GradientAppearance,
|
appearance: GradientAppearance,
|
||||||
initial_gradient: GradientStops,
|
initial_gradient: Gradient,
|
||||||
/// Transform from unit [0, 1] line to the geometry's local gradient space, the snapshot from `GradientAppearance.transform`.
|
/// Transform from unit [0, 1] line to the geometry's local gradient space, the snapshot from `GradientAppearance.transform`.
|
||||||
initial_gradient_transform: DAffine2,
|
initial_gradient_transform: DAffine2,
|
||||||
is_gradient_chain: bool,
|
is_gradient_chain: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, mouse: DVec2) -> Option<f64> {
|
||||||
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length();
|
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length();
|
||||||
let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||||
|
|
||||||
@@ -568,7 +568,7 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SelectedGradient {
|
impl SelectedGradient {
|
||||||
pub fn new(gradient: GradientStops, appearance: GradientAppearance, source: GradientSource, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
|
pub fn new(gradient: Gradient, appearance: GradientAppearance, source: GradientSource, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
|
||||||
let gradient_space_transform = gradient_space_transform(layer, document);
|
let gradient_space_transform = gradient_space_transform(layer, document);
|
||||||
Self {
|
Self {
|
||||||
layer: Some(layer),
|
layer: Some(layer),
|
||||||
@@ -820,7 +820,7 @@ impl SelectedGradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send the four per-attribute graph operations that mirror the in-memory `Gradient` onto the chain feeding the layer.
|
/// Send the four per-attribute graph operations that mirror the in-memory `Gradient` onto the chain feeding the layer.
|
||||||
fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &GradientStops, appearance: GradientAppearance, responses: &mut VecDeque<Message>) {
|
fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradient, appearance: GradientAppearance, responses: &mut VecDeque<Message>) {
|
||||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.clone() });
|
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.clone() });
|
||||||
responses.add(GraphOperationMessage::GradientTransformSet {
|
responses.add(GraphOperationMessage::GradientTransformSet {
|
||||||
layer,
|
layer,
|
||||||
@@ -868,11 +868,11 @@ struct GradientToolData {
|
|||||||
has_selected_gradient: bool,
|
has_selected_gradient: bool,
|
||||||
/// Cached stops of the currently selected layer's gradient, mirrored into the control-bar widget.
|
/// Cached stops of the currently selected layer's gradient, mirrored into the control-bar widget.
|
||||||
/// Independent of any in-progress drag (which uses `selected_gradient`) so it stays current after selection changes too.
|
/// Independent of any in-progress drag (which uses `selected_gradient`) so it stays current after selection changes too.
|
||||||
current_gradient_stops: Option<GradientStops>,
|
current_gradient_stops: Option<Gradient>,
|
||||||
/// User-customized default gradient stop colors: used when nothing that has a gradient is selected.
|
/// User-customized default gradient stop colors: used when nothing that has a gradient is selected.
|
||||||
/// `None` means to follow the working colors.
|
/// `None` means to follow the working colors.
|
||||||
/// Cleared on tool deactivation so each fresh activation starts from the working colors again.
|
/// Cleared on tool deactivation so each fresh activation starts from the working colors again.
|
||||||
default_gradient_stops: Option<GradientStops>,
|
default_gradient_stops: Option<Gradient>,
|
||||||
/// Cached viewport-space orientation (true = predominantly rightward) of the selected gradient line.
|
/// Cached viewport-space orientation (true = predominantly rightward) of the selected gradient line.
|
||||||
/// Used to refresh the control bar's "Reverse Direction" icon only when the line's apparent direction flips.
|
/// Used to refresh the control bar's "Reverse Direction" icon only when the line's apparent direction flips.
|
||||||
gradient_orientation_rightward: bool,
|
gradient_orientation_rightward: bool,
|
||||||
@@ -1496,7 +1496,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
// Generate a new gradient running primary → secondary so the default working colors
|
// Generate a new gradient running primary → secondary so the default working colors
|
||||||
// (primary = black, secondary = white) produce the expected black-to-white gradient
|
// (primary = black, secondary = white) produce the expected black-to-white gradient
|
||||||
None => (
|
None => (
|
||||||
GradientStops::new([
|
Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -1738,7 +1738,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_stop_at_point(gradient: &mut GradientStops, point: DVec2, unit_to_viewport: DAffine2) -> Option<usize> {
|
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2) -> Option<usize> {
|
||||||
let (start, end) = gradient_handle_positions(unit_to_viewport);
|
let (start, end) = gradient_handle_positions(unit_to_viewport);
|
||||||
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
|
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
|
||||||
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t))
|
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t))
|
||||||
@@ -1831,8 +1831,8 @@ fn apply_gradient_update(
|
|||||||
data: &mut GradientToolData,
|
data: &mut GradientToolData,
|
||||||
context: &mut ToolActionMessageContext,
|
context: &mut ToolActionMessageContext,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
condition: impl Fn((&GradientStops, &GradientAppearance)) -> bool,
|
condition: impl Fn((&Gradient, &GradientAppearance)) -> bool,
|
||||||
update: impl Fn((&mut GradientStops, &mut GradientAppearance)),
|
update: impl Fn((&mut Gradient, &mut GradientAppearance)),
|
||||||
) {
|
) {
|
||||||
let selected_layers: Vec<_> = context
|
let selected_layers: Vec<_> = context
|
||||||
.document
|
.document
|
||||||
@@ -1888,7 +1888,7 @@ fn apply_gradient_update(
|
|||||||
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
||||||
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
||||||
/// single undo entry by the surrounding 'on_commit' callback.
|
/// single undo entry by the surrounding 'on_commit' callback.
|
||||||
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: GradientStops) {
|
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient) {
|
||||||
let selected_layers: Vec<_> = context
|
let selected_layers: Vec<_> = context
|
||||||
.document
|
.document
|
||||||
.network_interface
|
.network_interface
|
||||||
@@ -1933,7 +1933,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the first selected visible layer that has a gradient and return both the layer ID and its resolved gradient.
|
/// Find the first selected visible layer that has a gradient and return both the layer ID and its resolved gradient.
|
||||||
fn current_layer_and_gradient(document: &DocumentMessageHandler) -> (Option<LayerNodeIdentifier>, Option<(GradientStops, GradientAppearance)>) {
|
fn current_layer_and_gradient(document: &DocumentMessageHandler) -> (Option<LayerNodeIdentifier>, Option<(Gradient, GradientAppearance)>) {
|
||||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||||
if let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) {
|
if let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) {
|
||||||
return (Some(layer), Some((gradient, appearance)));
|
return (Some(layer), Some((gradient, appearance)));
|
||||||
@@ -1942,7 +1942,7 @@ fn current_layer_and_gradient(document: &DocumentMessageHandler) -> (Option<Laye
|
|||||||
(None, None)
|
(None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_gradient_on_selected_layer(document: &DocumentMessageHandler) -> Option<(GradientStops, GradientAppearance, GradientSource)> {
|
fn get_gradient_on_selected_layer(document: &DocumentMessageHandler) -> Option<(Gradient, GradientAppearance, GradientSource)> {
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.selected_nodes()
|
.selected_nodes()
|
||||||
@@ -2015,19 +2015,19 @@ mod test_gradient {
|
|||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::vector::style::{GradientSpreadMethod, build_transform_with_y_preservation};
|
use graphene_std::vector::style::{GradientSpreadMethod, build_transform_with_y_preservation};
|
||||||
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
use graphene_std::vector::{Gradient, GradientStop, fill};
|
||||||
use graphene_std::{Graphic, NodeInputDecleration};
|
use graphene_std::{Graphic, NodeInputDecleration};
|
||||||
|
|
||||||
use super::gradient_space_transform;
|
use super::gradient_space_transform;
|
||||||
|
|
||||||
struct ResolvedGradient {
|
struct ResolvedGradient {
|
||||||
stops: GradientStops,
|
stops: Gradient,
|
||||||
spread_method: GradientSpreadMethod,
|
spread_method: GradientSpreadMethod,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedGradient {
|
impl ResolvedGradient {
|
||||||
fn new(stops: GradientStops, appearance: super::GradientAppearance) -> Self {
|
fn new(stops: Gradient, appearance: super::GradientAppearance) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stops,
|
stops,
|
||||||
spread_method: appearance.spread_method,
|
spread_method: appearance.spread_method,
|
||||||
@@ -2068,8 +2068,9 @@ mod test_gradient {
|
|||||||
_ => GradientSpreadMethod::default(),
|
_ => GradientSpreadMethod::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let has_transform = matches!(fill_node.inputs.get(fill::HasTransformInput::INDEX).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true)));
|
||||||
let local_transform = match fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value()) {
|
let local_transform = match fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value()) {
|
||||||
Some(&TaggedValue::OptionalDAffine2(Some(value))) => value,
|
Some(&TaggedValue::DAffine2(value)) if has_transform => value,
|
||||||
_ => DAffine2::IDENTITY,
|
_ => DAffine2::IDENTITY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2147,7 +2148,7 @@ mod test_gradient {
|
|||||||
.handle_message(NodeGraphMessage::SetInputValue {
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
node_id: gradient_node_id,
|
node_id: gradient_node_id,
|
||||||
input_index: 1,
|
input_index: 1,
|
||||||
value: TaggedValue::Gradient(GradientStops::new([
|
value: TaggedValue::Gradient(Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -2184,7 +2185,7 @@ mod test_gradient {
|
|||||||
.handle_message(NodeGraphMessage::SetInputValue {
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
node_id: gradient_node_id,
|
node_id: gradient_node_id,
|
||||||
input_index: 1,
|
input_index: 1,
|
||||||
value: TaggedValue::Gradient(GradientStops::new([
|
value: TaggedValue::Gradient(Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -2655,7 +2656,7 @@ mod test_gradient {
|
|||||||
// Create original transform for the control geometry and apply it
|
// Create original transform for the control geometry and apply it
|
||||||
let initial_start = DVec2::new(10., 50.);
|
let initial_start = DVec2::new(10., 50.);
|
||||||
let initial_end = DVec2::new(200., 50.);
|
let initial_end = DVec2::new(200., 50.);
|
||||||
let stops = GradientStops::new([
|
let stops = Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -2726,7 +2727,7 @@ mod test_gradient {
|
|||||||
let layer = create_gradient_list_layer(&mut editor).await;
|
let layer = create_gradient_list_layer(&mut editor).await;
|
||||||
|
|
||||||
// Set up a 3-stop gradient with distinct colors
|
// Set up a 3-stop gradient with distinct colors
|
||||||
let original_stops = GradientStops::new([
|
let original_stops = Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -2827,7 +2828,7 @@ mod test_gradient {
|
|||||||
.handle_message(NodeGraphMessage::SetInputValue {
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
node_id: gradient_value_id,
|
node_id: gradient_value_id,
|
||||||
input_index: 1,
|
input_index: 1,
|
||||||
value: TaggedValue::Gradient(GradientStops::new([
|
value: TaggedValue::Gradient(Gradient::new([
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ struct ExecutionContext {
|
|||||||
/// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
|
/// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
|
||||||
/// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
|
/// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
|
||||||
/// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured.
|
/// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured.
|
||||||
measure_fill: Option<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
measure_fill: Option<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
@@ -92,7 +92,7 @@ struct ExecutionContext {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct GradientMigration {
|
struct GradientMigration {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
|
remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)>,
|
||||||
resolution: UVec2,
|
resolution: UVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,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}"));
|
||||||
}
|
}
|
||||||
@@ -461,7 +460,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();
|
||||||
@@ -492,7 +490,7 @@ impl NodeGraphExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands.
|
// Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands.
|
||||||
let remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
|
let remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
|
||||||
let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
|
let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -519,7 +517,7 @@ impl NodeGraphExecutor {
|
|||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
network_path: Vec<NodeId>,
|
network_path: Vec<NodeId>,
|
||||||
fill_node_id: NodeId,
|
fill_node_id: NodeId,
|
||||||
gradient: graphic_types::migrations::legacy::Gradient,
|
gradient: graphic_types::migrations::legacy::LegacyGradient,
|
||||||
resolution: UVec2,
|
resolution: UVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
@@ -588,7 +586,7 @@ impl NodeGraphExecutor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
document: &mut DocumentMessageHandler,
|
document: &mut DocumentMessageHandler,
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
bake_target: (Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient),
|
bake_target: (Vec<NodeId>, NodeId, graphic_types::migrations::legacy::LegacyGradient),
|
||||||
inspect_result: Option<InspectResult>,
|
inspect_result: Option<InspectResult>,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) {
|
) {
|
||||||
@@ -610,10 +608,14 @@ impl NodeGraphExecutor {
|
|||||||
if fill_transform_unbaked(document, &network_path, fill_node_id) {
|
if fill_transform_unbaked(document, &network_path, fill_node_id) {
|
||||||
let absolute_gradient = gradient.to_absolute(bounding_box, item_transform);
|
let absolute_gradient = gradient.to_absolute(bounding_box, item_transform);
|
||||||
let gradient_transform = absolute_gradient.transform * absolute_gradient.to_transform();
|
let gradient_transform = absolute_gradient.transform * absolute_gradient.to_transform();
|
||||||
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX);
|
let has_transform_input = InputConnector::node(fill_node_id, graphene_std::vector::fill::HasTransformInput::INDEX);
|
||||||
|
let transform_input = InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX);
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
.set_input(&input, NodeInput::value(TaggedValue::OptionalDAffine2(Some(gradient_transform)), false), &network_path);
|
.set_input(&has_transform_input, NodeInput::value(TaggedValue::Bool(true), false), &network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&transform_input, NodeInput::value(TaggedValue::DAffine2(gradient_transform), false), &network_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The transform is settled, so its entry no longer needs to persist for a retry on the next open
|
// The transform is settled, so its entry no longer needs to persist for a retry on the next open
|
||||||
@@ -821,16 +823,16 @@ 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;
|
||||||
};
|
};
|
||||||
let Some(node) = network.nodes.get(&fill_node_id) else { return false };
|
let Some(node) = network.nodes.get(&fill_node_id) else { return false };
|
||||||
matches!(
|
matches!(
|
||||||
node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX).and_then(|input| input.as_value()),
|
node.inputs.get(graphene_std::vector::fill::HasTransformInput::INDEX).and_then(|input| input.as_value()),
|
||||||
Some(TaggedValue::OptionalDAffine2(None))
|
Some(TaggedValue::Bool(false))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,9 +919,18 @@ mod test {
|
|||||||
use graph_craft::document::NodeNetwork;
|
use graph_craft::document::NodeNetwork;
|
||||||
use graphene_std::Context;
|
use graphene_std::Context;
|
||||||
use graphene_std::NodeInputDecleration;
|
use graphene_std::NodeInputDecleration;
|
||||||
|
use graphene_std::list::Item;
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
use test_prelude::LayerNodeIdentifier;
|
use test_prelude::LayerNodeIdentifier;
|
||||||
|
|
||||||
|
/// A ranked input whose `Item<E>` Result carries an element `E`, recovered by `grab_ranked_input`.
|
||||||
|
pub trait RankedResult {
|
||||||
|
type Element: Send + Sync + Clone + 'static;
|
||||||
|
}
|
||||||
|
impl<E: Send + Sync + Clone + 'static> RankedResult for Item<E> {
|
||||||
|
type Element = E;
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores all of the monitor nodes that have been attached to a graph
|
/// Stores all of the monitor nodes that have been attached to a graph
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Instrumented {
|
pub struct Instrumented {
|
||||||
@@ -942,11 +953,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);
|
||||||
@@ -978,15 +995,21 @@ mod test {
|
|||||||
where
|
where
|
||||||
Input::Result: Send + Sync + Clone + 'static,
|
Input::Result: Send + Sync + Clone + 'static,
|
||||||
{
|
{
|
||||||
// This is quite inflexible since it only allows the footprint as inputs.
|
Self::downcast_record::<Input::Result>(dynamic).or_else(|| {
|
||||||
if let Some(x) = dynamic.downcast_ref::<IORecord<(), Input::Result>>() {
|
warn!("cannot downcast type for introspection");
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pulls a concrete output type out of a monitor record, tolerating the three context shapes the executor records against.
|
||||||
|
fn downcast_record<Output: Send + Sync + Clone + 'static>(dynamic: Arc<dyn std::any::Any + Send + Sync>) -> Option<Output> {
|
||||||
|
if let Some(x) = dynamic.downcast_ref::<IORecord<(), Output>>() {
|
||||||
Some(x.output.clone())
|
Some(x.output.clone())
|
||||||
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
|
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Output>>() {
|
||||||
Some(x.output.clone())
|
Some(x.output.clone())
|
||||||
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Context, Input::Result>>() {
|
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Context, Output>>() {
|
||||||
Some(x.output.clone())
|
Some(x.output.clone())
|
||||||
} else {
|
} else {
|
||||||
warn!("cannot downcast type for introspection");
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1028,18 @@ mod test {
|
|||||||
.filter_map(Instrumented::downcast::<Input>) // Some might not resolve (e.g. generics that don't work properly)
|
.filter_map(Instrumented::downcast::<Input>) // Some might not resolve (e.g. generics that don't work properly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`Self::grab_all_input`], but downcasting the recorded values to `Output` instead of the marker's `Result`.
|
||||||
|
/// Useful when a stored value's wire form (e.g. `Item<Color>`) 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,
|
||||||
@@ -1016,6 +1051,17 @@ mod test {
|
|||||||
Self::downcast::<Input>(dynamic)
|
Self::downcast::<Input>(dynamic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grabs a ranked (`Item<E>`) input's recorded value as its bare element `E`.
|
||||||
|
/// A stored value materializes as an `Item<E>` wire, so the monitor records the whole cell and this unwraps its element.
|
||||||
|
pub fn grab_ranked_input<Input: NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<<Input::Result as RankedResult>::Element>
|
||||||
|
where
|
||||||
|
Input::Result: RankedResult,
|
||||||
|
{
|
||||||
|
let input_monitor_node = self.protonodes_by_path.get(path)?.get(Input::INDEX)?;
|
||||||
|
let dynamic = runtime.executor.introspect(input_monitor_node).ok()?;
|
||||||
|
Self::downcast_record::<Item<<Input::Result as RankedResult>::Element>>(dynamic).map(|item| item.into_element())
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use graph_craft::graphene_compiler::Compiler;
|
|||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture};
|
use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture};
|
||||||
use graphene_std::bounds::RenderBoundingBox;
|
use graphene_std::bounds::RenderBoundingBox;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::{Item, List};
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
use graphene_std::ops::Convert;
|
use graphene_std::ops::Convert;
|
||||||
#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
|
#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
|
||||||
@@ -378,7 +378,7 @@ impl NodeRuntime {
|
|||||||
use graph_craft::graphene_compiler::Executor;
|
use graph_craft::graphene_compiler::Executor;
|
||||||
|
|
||||||
match self.executor.input_type() {
|
match self.executor.input_type() {
|
||||||
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()),
|
Some(t) if t == concrete!(Context) => (&self.executor).execute(render_config.into_context()).await.map_err(|e| e.to_string()),
|
||||||
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
|
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
|
||||||
Some(t) => Err(format!("Invalid input type {t:?}")),
|
Some(t) => Err(format!("Invalid input type {t:?}")),
|
||||||
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
|
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
|
||||||
@@ -441,6 +441,28 @@ impl NodeRuntime {
|
|||||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Rank-0 wires record single items; each arm mirrors its list counterpart through a singleton raise
|
||||||
|
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Graphic>>>() {
|
||||||
|
if update_thumbnails {
|
||||||
|
let singleton = List::new_from_item(io.output.clone());
|
||||||
|
let bounds = graphene_std::renderer::graphic_list_bounding_box(&singleton, DAffine2::IDENTITY);
|
||||||
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &singleton, bounds, responses)
|
||||||
|
}
|
||||||
|
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Artboard>>>() {
|
||||||
|
if update_thumbnails {
|
||||||
|
let singleton = List::new_from_item(io.output.clone());
|
||||||
|
let bounds = artboard_clip_bounds(&singleton);
|
||||||
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &singleton, bounds, responses)
|
||||||
|
}
|
||||||
|
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<Vector>>>() {
|
||||||
|
self.vector_modify.insert(parent_network_node_id, io.output.element().clone());
|
||||||
|
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Item<String>>>() {
|
||||||
|
if update_thumbnails {
|
||||||
|
let singleton = List::new_from_item(io.output.clone());
|
||||||
|
let bounds = graphene_std::renderer::text_list_bounding_box(&singleton, DAffine2::IDENTITY);
|
||||||
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &singleton, bounds, responses)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Other
|
// Other
|
||||||
else {
|
else {
|
||||||
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
|
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||||
import { contrastingOutlineFactor, fillChoiceUIColor, fillChoiceUIGradientStops } from "/src/utility-functions/colors";
|
import { contrastingOutlineFactor, fillChoiceUIColor, fillChoiceUIGradient } from "/src/utility-functions/colors";
|
||||||
import type { FillChoiceUI, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FillChoiceUI, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ value: FillChoiceUI; startHistoryTransaction: undefined }>();
|
const dispatch = createEventDispatcher<{ value: FillChoiceUI; startHistoryTransaction: undefined }>();
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
|
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
|
||||||
$: outlined = outlineFactor > 0.0001;
|
$: outlined = outlineFactor > 0.0001;
|
||||||
$: gradientStops = fillChoiceUIGradientStops(value);
|
$: gradient = fillChoiceUIGradient(value);
|
||||||
$: solidColor = fillChoiceUIColor(value);
|
$: solidColor = fillChoiceUIColor(value);
|
||||||
$: none = value === "None";
|
$: none = value === "None";
|
||||||
$: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 255) : solidColor ? solidColor.alpha < 255 : false;
|
$: transparency = gradient ? gradient.color.some((color) => color.alpha < 255) : solidColor ? solidColor.alpha < 255 : false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol
|
<LayoutCol
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FillChoiceUI, GradientStopsUI, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FillChoiceUI, GradientUI, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
||||||
export type HSV = { h: number; s: number; v: number };
|
export type HSV = { h: number; s: number; v: number };
|
||||||
@@ -167,12 +167,12 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st
|
|||||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - sRgba8ToHSV(color).s);
|
return (1 - Math.min(distance / proximityRange, 1)) * (1 - sRgba8ToHSV(color).s);
|
||||||
};
|
};
|
||||||
|
|
||||||
const gradientStops = fillChoiceUIGradientStops(value);
|
const gradient = fillChoiceUIGradient(value);
|
||||||
if (gradientStops) {
|
if (gradient) {
|
||||||
if (gradientStops.color.length === 0) return 0;
|
if (gradient.color.length === 0) return 0;
|
||||||
|
|
||||||
const first = contrast(gradientStops.color[0]);
|
const first = contrast(gradient.color[0]);
|
||||||
const last = contrast(gradientStops.color[gradientStops.color.length - 1]);
|
const last = contrast(gradient.color[gradient.color.length - 1]);
|
||||||
|
|
||||||
return Math.min(first, last);
|
return Math.min(first, last);
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st
|
|||||||
|
|
||||||
// GRADIENT UTILITY FUNCTIONS
|
// GRADIENT UTILITY FUNCTIONS
|
||||||
|
|
||||||
export function isGradientStopsUI(value: unknown): value is GradientStopsUI {
|
export function isGradientUI(value: unknown): value is GradientUI {
|
||||||
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value;
|
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ export function fillChoiceUIColor(value: FillChoiceUI): SRGBA8 | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillChoiceUIGradientStops(value: FillChoiceUI): GradientStopsUI | undefined {
|
export function fillChoiceUIGradient(value: FillChoiceUI): GradientUI | undefined {
|
||||||
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
|
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -201,6 +201,6 @@ export function fillChoiceUIGradientStops(value: FillChoiceUI): GradientStopsUI
|
|||||||
export function parseFillChoiceUI(value: unknown): FillChoiceUI {
|
export function parseFillChoiceUI(value: unknown): FillChoiceUI {
|
||||||
if (value === "None" || value === undefined || value === null) return "None";
|
if (value === "None" || value === undefined || value === null) return "None";
|
||||||
if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid };
|
if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid };
|
||||||
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStopsUI(value.Gradient)) return { Gradient: value.Gradient };
|
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientUI(value.Gradient)) return { Gradient: value.Gradient };
|
||||||
return "None";
|
return "None";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,18 @@ impl OriginalLocation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl DocumentNode {
|
impl DocumentNode {
|
||||||
|
/// Normalizes this node's stored types (call argument, `Import` input types, `TypeDefault` value payloads, and any nested network) to their structural form.
|
||||||
|
/// Applied once at ingestion (document migration and clipboard paste) so no name-encoded ranked type enters a live document.
|
||||||
|
pub fn normalize_stored_types(&mut self) {
|
||||||
|
self.call_argument = self.call_argument.clone().normalize_rank();
|
||||||
|
for input in &mut self.inputs {
|
||||||
|
normalize_input_stored_type(input);
|
||||||
|
}
|
||||||
|
if let Some(network) = self.implementation.get_network_mut() {
|
||||||
|
network.normalize_stored_types();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Locate the input that is a [`NodeInput::Import`] at index `offset` and replace it with a [`NodeInput::Node`].
|
/// Locate the input that is a [`NodeInput::Import`] at index `offset` and replace it with a [`NodeInput::Node`].
|
||||||
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, source: impl Iterator<Item = Source>, skip: usize) {
|
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, source: impl Iterator<Item = Source>, skip: usize) {
|
||||||
let (index, _) = self
|
let (index, _) = self
|
||||||
@@ -220,7 +232,7 @@ pub enum DocumentNodeMetadata {
|
|||||||
impl DocumentNodeMetadata {
|
impl DocumentNodeMetadata {
|
||||||
pub fn ty(&self) -> Type {
|
pub fn ty(&self) -> Type {
|
||||||
match self {
|
match self {
|
||||||
DocumentNodeMetadata::DocumentNodePath => concrete!(core_types::list::List<NodeId>),
|
DocumentNodeMetadata::DocumentNodePath => item!(core_types::list::NodeIdPath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,10 +247,10 @@ impl NodeInput {
|
|||||||
Self::Value { tagged_value, exposed }
|
Self::Value { tagged_value, exposed }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a `NodeInput::Value` whose tagged value is `TaggedValue::TypeDefault(td)`, recording only the
|
/// Constructs a `NodeInput::Value` whose tagged value is `TaggedValue::TypeDefault(ty)`, recording only the
|
||||||
/// type so the runtime materializes its default rather than baking a placeholder value into the saved document.
|
/// type so the runtime materializes its default rather than baking a placeholder value into the saved document.
|
||||||
pub fn type_default(td: core_types::TypeDescriptor, exposed: bool) -> Self {
|
pub fn type_default(ty: Type, exposed: bool) -> Self {
|
||||||
Self::value(TaggedValue::TypeDefault(td), exposed)
|
Self::value(TaggedValue::TypeDefault(ty.normalize_rank()), exposed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn import(import_type: Type, import_index: usize) -> Self {
|
pub const fn import(import_type: Type, import_index: usize) -> Self {
|
||||||
@@ -270,6 +282,7 @@ impl NodeInput {
|
|||||||
match self {
|
match self {
|
||||||
NodeInput::Node { .. } => unreachable!("ty() called on NodeInput::Node"),
|
NodeInput::Node { .. } => unreachable!("ty() called on NodeInput::Node"),
|
||||||
NodeInput::Value { tagged_value, .. } => tagged_value.ty(),
|
NodeInput::Value { tagged_value, .. } => tagged_value.ty(),
|
||||||
|
// Stored import types are normalized to their structural form once at document migration
|
||||||
NodeInput::Import { import_type, .. } => import_type.clone(),
|
NodeInput::Import { import_type, .. } => import_type.clone(),
|
||||||
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
|
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
|
||||||
NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"),
|
NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"),
|
||||||
@@ -723,7 +736,33 @@ impl ScopeChain<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Functions for compiling the network
|
/// Functions for compiling the network
|
||||||
|
/// Normalizes the ranked types an input can store: an `Import`'s type or a value's `TypeDefault` payload.
|
||||||
|
fn normalize_input_stored_type(input: &mut NodeInput) {
|
||||||
|
match input {
|
||||||
|
NodeInput::Import { import_type, .. } => *import_type = import_type.clone().normalize_rank(),
|
||||||
|
NodeInput::Value { tagged_value, .. } => {
|
||||||
|
if let TaggedValue::TypeDefault(ty) = &**tagged_value {
|
||||||
|
let normalized = ty.clone().normalize_rank();
|
||||||
|
if normalized != *ty {
|
||||||
|
*tagged_value = TaggedValue::TypeDefault(normalized).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NodeNetwork {
|
impl NodeNetwork {
|
||||||
|
/// Normalizes every stored type in the network (exports and each node's types) to the structural form, recursively.
|
||||||
|
pub fn normalize_stored_types(&mut self) {
|
||||||
|
for export in &mut self.exports {
|
||||||
|
normalize_input_stored_type(export);
|
||||||
|
}
|
||||||
|
for node in self.nodes.values_mut() {
|
||||||
|
node.normalize_stored_types();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace all references in the graph of a node ID with a new node ID defined by the function `f`.
|
/// Replace all references in the graph of a node ID with a new node ID defined by the function `f`.
|
||||||
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
|
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
|
||||||
self.exports.iter_mut().for_each(|output| {
|
self.exports.iter_mut().for_each(|output| {
|
||||||
@@ -995,7 +1034,7 @@ impl NodeNetwork {
|
|||||||
let (tagged_value, exposed) = match previous_export {
|
let (tagged_value, exposed) = match previous_export {
|
||||||
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
|
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
|
||||||
NodeInput::Reflection(reflect) => match reflect {
|
NodeInput::Reflection(reflect) => match reflect {
|
||||||
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false),
|
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(core_types::list::NodeIdPath::from(path.to_vec())).into(), false),
|
||||||
},
|
},
|
||||||
previous_export => {
|
previous_export => {
|
||||||
*export = previous_export;
|
*export = previous_export;
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ use super::DocumentNode;
|
|||||||
use crate::application_io::PlatformEditorApi;
|
use crate::application_io::PlatformEditorApi;
|
||||||
use crate::application_io::resource::Resource;
|
use crate::application_io::resource::Resource;
|
||||||
use crate::proto::{Any as DAny, FutureAny};
|
use crate::proto::{Any as DAny, FutureAny};
|
||||||
use brush_nodes::brush_stroke::BrushStroke;
|
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::List;
|
use core_types::list::{Item, List, NodeIdPath};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor};
|
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
pub use dyn_any::StaticType;
|
pub use dyn_any::StaticType;
|
||||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||||
use graphene_application_io::resource::ResourceHash;
|
use graphene_application_io::resource::ResourceHash;
|
||||||
|
use graphene_application_io::resource::ResourceId;
|
||||||
use graphic_types::raster_types::{CPU, Image, Raster};
|
use graphic_types::raster_types::{CPU, Image, Raster};
|
||||||
use graphic_types::vector_types::vector::style::GradientStops;
|
use graphic_types::vector_types::vector::misc::BoxCorners;
|
||||||
|
use graphic_types::vector_types::vector::style::DashPattern;
|
||||||
|
use graphic_types::vector_types::vector::style::Gradient;
|
||||||
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
||||||
use graphic_types::{Artboard, Graphic, Vector};
|
use graphic_types::{Artboard, Graphic, Vector};
|
||||||
use rendering::RenderMetadata;
|
use rendering::RenderMetadata;
|
||||||
@@ -28,15 +30,38 @@ use vector::VectorModification;
|
|||||||
|
|
||||||
pub struct TaggedValueTypeError;
|
pub struct TaggedValueTypeError;
|
||||||
|
|
||||||
/// List of types routed through [`TaggedValue::TypeDefault`] instead of another dedicated variant.
|
/// Item-cell element types routed through [`TaggedValue::TypeDefault`] instead of another dedicated variant, stored as the concrete `Item<T>` wire type.
|
||||||
/// Consumed by [`TaggedValue::from_type`] (which creates `TypeDefault` values) and [`TaggedValue::to_dynany`]/[`TaggedValue::to_any`] (which unwrap them into real default values).
|
/// Consumed by [`TaggedValue::from_type`] (which creates `TypeDefault` values) and [`TaggedValue::to_dynany`]/[`TaggedValue::to_any`] (which unwrap them into real default values).
|
||||||
macro_rules! for_each_type_default {
|
macro_rules! for_each_item_type_default {
|
||||||
|
($action:ident) => {
|
||||||
|
$action!(Vector);
|
||||||
|
$action!(f64);
|
||||||
|
$action!(Raster<CPU>);
|
||||||
|
$action!(Graphic);
|
||||||
|
$action!(Color);
|
||||||
|
$action!(Gradient);
|
||||||
|
$action!(Artboard);
|
||||||
|
$action!(String);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List element types routed through [`TaggedValue::TypeDefault`], stored as the structural [`Type::List`] form.
|
||||||
|
/// `List<f64>` is absent because it stores as `TaggedValue::F64Array`.
|
||||||
|
macro_rules! for_each_list_type_default {
|
||||||
|
($action:ident) => {
|
||||||
|
$action!(Graphic);
|
||||||
|
$action!(Artboard);
|
||||||
|
$action!(Raster<CPU>);
|
||||||
|
$action!(Vector);
|
||||||
|
$action!(String);
|
||||||
|
$action!(Color);
|
||||||
|
$action!(Gradient);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unranked types routed through [`TaggedValue::TypeDefault`], stored as their concrete type.
|
||||||
|
macro_rules! for_each_bare_type_default {
|
||||||
($action:ident) => {
|
($action:ident) => {
|
||||||
$action!(List<Graphic>);
|
|
||||||
$action!(List<Artboard>);
|
|
||||||
$action!(List<Raster<CPU>>);
|
|
||||||
$action!(List<Vector>);
|
|
||||||
$action!(List<String>);
|
|
||||||
$action!(DocumentNode);
|
$action!(DocumentNode);
|
||||||
$action!(Resource);
|
$action!(Resource);
|
||||||
};
|
};
|
||||||
@@ -54,22 +79,24 @@ macro_rules! tagged_value {
|
|||||||
// ===============
|
// ===============
|
||||||
None,
|
None,
|
||||||
/// Stores a type, from which its `Default::default()` value can be obtained, rather than storing an actual type's value.
|
/// Stores a type, from which its `Default::default()` value can be obtained, rather than storing an actual type's value.
|
||||||
/// Example: `TaggedValue::TypeDefault(descriptor!(String))` stores the type `String` but no specific string value.
|
/// Example: `TaggedValue::TypeDefault(concrete!(String))` stores the type `String` but no specific string value.
|
||||||
TypeDefault(TypeDescriptor),
|
/// (Old documents stored a bare `TypeDescriptor` payload, routed to this shape by `deserialize_tagged_value_with_legacy_migration`.)
|
||||||
|
TypeDefault(Type),
|
||||||
/// Stored compactly as a `Vec<f64>`, materializes as `List<f64>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
/// Stored compactly as a `Vec<f64>`, materializes as `List<f64>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
||||||
#[serde(deserialize_with = "core_types::misc::migrate_to_f64_array")] // TODO: Eventually remove this migration document upgrade code
|
#[serde(deserialize_with = "core_types::misc::migrate_to_f64_array")] // TODO: Eventually remove this migration document upgrade code
|
||||||
#[serde(alias = "F64Table", alias = "VecF64", alias = "VecF32", alias = "F64Array4")]
|
#[serde(alias = "F64Table", alias = "VecF64", alias = "VecF32", alias = "F64Array4")]
|
||||||
F64Array(Vec<f64>),
|
F64Array(Vec<f64>),
|
||||||
/// Stored compactly as an `Option<Color>`, materializes as `List<Color>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
/// A plain, always-present color. Aliases recover legacy on-disk shapes; a legacy `null` payload (the old "no color")
|
||||||
#[serde(deserialize_with = "core_types::misc::migrate_to_optional_color")] // TODO: Eventually remove this migration document upgrade code
|
/// is routed to [`TaggedValue::no_paint`] by `deserialize_tagged_value_with_legacy_migration`.
|
||||||
|
#[serde(deserialize_with = "core_types::misc::migrate_to_color")] // TODO: Eventually remove this migration document upgrade code
|
||||||
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
|
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
|
||||||
Color(Option<Color>),
|
Color(Color),
|
||||||
/// Stored compactly as a `GradientStops`, materializes as a single-row `List<GradientStops>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
/// Stored compactly as a `Gradient`, materializing as an `Item<Gradient>` at runtime. Aliases recover legacy on-disk shapes.
|
||||||
/// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `LegacyGradient` by `deserialize_tagged_value_with_legacy_migration`.)
|
/// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `LegacyGradient` by `deserialize_tagged_value_with_legacy_migration`.)
|
||||||
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
|
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient")] // TODO: Eventually remove this migration document upgrade code
|
||||||
#[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")]
|
#[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")]
|
||||||
Gradient(GradientStops),
|
Gradient(Gradient),
|
||||||
/// Stored compactly as a `Vec<BrushStroke>`, materializes as `List<BrushStroke>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
/// Stored compactly as a `Vec<BrushStroke>`, materializes as the single-value `Item<BrushTrace>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
|
||||||
#[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this migration document upgrade code
|
#[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this migration document upgrade code
|
||||||
#[serde(alias = "BrushStrokeTable")]
|
#[serde(alias = "BrushStrokeTable")]
|
||||||
BrushStrokes(Vec<BrushStroke>),
|
BrushStrokes(Vec<BrushStroke>),
|
||||||
@@ -82,10 +109,10 @@ macro_rules! tagged_value {
|
|||||||
// =======================
|
// =======================
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
RenderOutput(RenderOutput),
|
RenderOutput(RenderOutput),
|
||||||
/// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes a `List<NodeId>` at runtime via `to_dynany`/`to_any` during graph flattening.
|
/// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes an `Item<NodeIdPath>` at runtime via `to_dynany`/`to_any` during graph flattening, matching the ranked connectors it feeds.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
NodeIdPath(Vec<NodeId>),
|
NodeIdPath(NodeIdPath),
|
||||||
/// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(descriptor!(DocumentNode))`.
|
/// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(concrete!(DocumentNode))`.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
DocumentNode(DocumentNode),
|
DocumentNode(DocumentNode),
|
||||||
/// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`.
|
/// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`.
|
||||||
@@ -118,7 +145,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::ContextFeatures(features) => features.cache_hash(state),
|
Self::ContextFeatures(features) => features.cache_hash(state),
|
||||||
Self::RenderOutput(x) => x.cache_hash(state),
|
Self::RenderOutput(x) => x.cache_hash(state),
|
||||||
@@ -138,46 +165,58 @@ macro_rules! tagged_value {
|
|||||||
Self::None => Box::new(()),
|
Self::None => Box::new(()),
|
||||||
Self::TypeDefault(td) => {
|
Self::TypeDefault(td) => {
|
||||||
// Construct the actual default for types without a `TaggedValue` variant directly.
|
// Construct the actual default for types without a `TaggedValue` variant directly.
|
||||||
// Recursion through `from_type_or_none` below is safe only because `for_each_type_default!`
|
// Recursion through `from_type_or_none` below is safe only because the type-default lists
|
||||||
// exhaustively handles every type that `from_type` would route back to `TypeDefault`.
|
// exhaustively cover every type that `from_type` would route back to `TypeDefault`.
|
||||||
let name = td.name.as_ref();
|
match &td {
|
||||||
macro_rules! check {
|
Type::List(element) => {
|
||||||
($type_default:ty) => {
|
macro_rules! check {
|
||||||
if name == std::any::type_name::<$type_default>() { return Box::new(<$type_default>::default()); }
|
($type_default:ty) => {
|
||||||
};
|
if **element == concrete!($type_default) { return Box::new(List::<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_list_type_default!(check);
|
||||||
|
}
|
||||||
|
Type::Item(element) => {
|
||||||
|
macro_rules! check {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if **element == concrete!($type_default) { return Box::new(Item::<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_item_type_default!(check);
|
||||||
|
}
|
||||||
|
Type::Concrete(descriptor) => {
|
||||||
|
let name = descriptor.name.as_ref();
|
||||||
|
macro_rules! check_bare {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if name == std::any::type_name::<$type_default>() { return Box::new(<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_bare_type_default!(check_bare);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
for_each_type_default!(check);
|
Self::from_type_or_none(&td).to_dynany()
|
||||||
Self::from_type_or_none(&Type::Concrete(td)).to_dynany()
|
|
||||||
}
|
}
|
||||||
Self::F64Array(values) => {
|
Self::F64Array(values) => {
|
||||||
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||||
Box::new(list)
|
Box::new(list)
|
||||||
}
|
}
|
||||||
Self::Color(color) => {
|
Self::Color(color) => Box::new(Item::new_from_element(color)),
|
||||||
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
|
Self::Gradient(stops) => Box::new(Item::new_from_element(stops)),
|
||||||
Box::new(list)
|
Self::BrushStrokes(strokes) => Box::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
|
||||||
}
|
|
||||||
Self::Gradient(stops) => Box::new(List::<GradientStops>::new_from_element(stops)),
|
|
||||||
Self::BrushStrokes(strokes) => {
|
|
||||||
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
||||||
Box::new(list)
|
|
||||||
}
|
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( Self::$identifier(x) => Box::new(x), )*
|
$( Self::$identifier(x) => Box::new(Item::new_from_element(x)), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
Self::RenderOutput(x) => Box::new(x),
|
Self::RenderOutput(x) => Box::new(Item::new_from_element(x)),
|
||||||
Self::NodeIdPath(path) => {
|
Self::NodeIdPath(path) => Box::new(Item::new_from_element(path)),
|
||||||
let list: List<NodeId> = path.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
||||||
Box::new(list)
|
|
||||||
}
|
|
||||||
Self::DocumentNode(node) => Box::new(node),
|
Self::DocumentNode(node) => Box::new(node),
|
||||||
Self::ContextFeatures(features) => Box::new(features),
|
Self::ContextFeatures(features) => Box::new(Item::new_from_element(features)),
|
||||||
Self::EditorApi(x) => Box::new(x),
|
Self::EditorApi(x) => Box::new(x),
|
||||||
Self::ResourceHash(x) => Box::new(x),
|
Self::ResourceHash(x) => Box::new(Item::new_from_element(x)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,73 +229,88 @@ macro_rules! tagged_value {
|
|||||||
Self::None => Arc::new(()),
|
Self::None => Arc::new(()),
|
||||||
Self::TypeDefault(td) => {
|
Self::TypeDefault(td) => {
|
||||||
// Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`.
|
// Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`.
|
||||||
let name = td.name.as_ref();
|
match &td {
|
||||||
macro_rules! check {
|
Type::List(element) => {
|
||||||
($type_default:ty) => {
|
macro_rules! check {
|
||||||
if name == std::any::type_name::<$type_default>() { return Arc::new(<$type_default>::default()); }
|
($type_default:ty) => {
|
||||||
};
|
if **element == concrete!($type_default) { return Arc::new(List::<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_list_type_default!(check);
|
||||||
|
}
|
||||||
|
Type::Item(element) => {
|
||||||
|
macro_rules! check {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if **element == concrete!($type_default) { return Arc::new(Item::<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_item_type_default!(check);
|
||||||
|
}
|
||||||
|
Type::Concrete(descriptor) => {
|
||||||
|
let name = descriptor.name.as_ref();
|
||||||
|
macro_rules! check_bare {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if name == std::any::type_name::<$type_default>() { return Arc::new(<$type_default>::default()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_bare_type_default!(check_bare);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
for_each_type_default!(check);
|
Self::from_type_or_none(&td).to_any()
|
||||||
Self::from_type_or_none(&Type::Concrete(td)).to_any()
|
|
||||||
}
|
}
|
||||||
Self::F64Array(values) => {
|
Self::F64Array(values) => {
|
||||||
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||||
Arc::new(list)
|
Arc::new(list)
|
||||||
}
|
}
|
||||||
Self::Color(color) => {
|
Self::Color(color) => Arc::new(Item::new_from_element(color)),
|
||||||
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
|
Self::Gradient(stops) => Arc::new(Item::new_from_element(stops)),
|
||||||
Arc::new(list)
|
Self::BrushStrokes(strokes) => Arc::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
|
||||||
}
|
|
||||||
Self::Gradient(stops) => Arc::new(List::<GradientStops>::new_from_element(stops)),
|
|
||||||
Self::BrushStrokes(strokes) => {
|
|
||||||
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
||||||
Arc::new(list)
|
|
||||||
}
|
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( Self::$identifier(x) => Arc::new(x), )*
|
$( Self::$identifier(x) => Arc::new(Item::new_from_element(x)), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
Self::RenderOutput(x) => Arc::new(x),
|
Self::RenderOutput(x) => Arc::new(Item::new_from_element(x)),
|
||||||
Self::NodeIdPath(path) => {
|
Self::NodeIdPath(path) => Arc::new(Item::new_from_element(path)),
|
||||||
let list: List<NodeId> = path.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
||||||
Arc::new(list)
|
|
||||||
}
|
|
||||||
Self::DocumentNode(node) => Arc::new(node),
|
Self::DocumentNode(node) => Arc::new(node),
|
||||||
Self::ContextFeatures(features) => Arc::new(features),
|
Self::ContextFeatures(features) => Arc::new(Item::new_from_element(features)),
|
||||||
Self::EditorApi(x) => Arc::new(x),
|
Self::EditorApi(x) => Arc::new(x),
|
||||||
Self::ResourceHash(x) => Arc::new(x),
|
Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a core_types::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value
|
/// Creates the wire [`Type`] of the value inside the tagged value, with ranked types in their structural form.
|
||||||
pub fn ty(&self) -> Type {
|
pub fn ty(&self) -> Type {
|
||||||
match self {
|
let ty = match self {
|
||||||
// ===============
|
// ===============
|
||||||
// MANUAL VARIANTS
|
// MANUAL VARIANTS
|
||||||
// ===============
|
// ===============
|
||||||
Self::None => concrete!(()),
|
Self::None => concrete!(()),
|
||||||
Self::TypeDefault(td) => Type::Concrete(td.clone()),
|
Self::TypeDefault(td) => td.clone(),
|
||||||
Self::F64Array(_) => concrete!(List<f64>),
|
Self::F64Array(_) => list!(f64),
|
||||||
Self::Color(_) => concrete!(List<Color>),
|
Self::Color(_) => item!(Color),
|
||||||
Self::Gradient(_) => concrete!(List<GradientStops>),
|
Self::Gradient(_) => item!(Gradient),
|
||||||
Self::BrushStrokes(_) => concrete!(List<BrushStroke>),
|
Self::BrushStrokes(_) => item!(BrushTrace),
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( Self::$identifier(_) => concrete!($ty), )*
|
$( Self::$identifier(_) => item!($ty), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
Self::RenderOutput(_) => concrete!(RenderOutput),
|
Self::RenderOutput(_) => item!(RenderOutput),
|
||||||
Self::NodeIdPath(_) => concrete!(List<NodeId>),
|
Self::NodeIdPath(_) => item!(NodeIdPath),
|
||||||
Self::DocumentNode(_) => concrete!(DocumentNode),
|
Self::DocumentNode(_) => concrete!(DocumentNode),
|
||||||
Self::ContextFeatures(_) => concrete!(ContextFeatures),
|
Self::ContextFeatures(_) => item!(ContextFeatures),
|
||||||
Self::EditorApi(_) => concrete!(&PlatformEditorApi),
|
Self::EditorApi(_) => item!(&PlatformEditorApi),
|
||||||
Self::ResourceHash(_) => concrete!(ResourceHash),
|
Self::ResourceHash(_) => item!(ResourceHash),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Defensively converges any remaining name-encoded ranked type (e.g. an opaque macro capture) to the structural form
|
||||||
|
ty.normalize_rank()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to downcast the dynamic type to a tagged value
|
/// Attempts to downcast the dynamic type to a tagged value
|
||||||
@@ -273,10 +327,11 @@ macro_rules! tagged_value {
|
|||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
|
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
|
||||||
|
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(downcast::<Item<$ty>>(input).unwrap().into_element())), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(downcast::<Item<RenderOutput>>(input).unwrap().into_element())),
|
||||||
|
|
||||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||||
}
|
}
|
||||||
@@ -295,17 +350,18 @@ macro_rules! tagged_value {
|
|||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
|
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
|
||||||
|
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(Item::<$ty>::clone(input.downcast_ref().unwrap()).into_element())), )*
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))),
|
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(Item::<RenderOutput>::clone(input.downcast_ref().unwrap()).into_element())),
|
||||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))),
|
_ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a TaggedValue from the type, where that value is its type's `Default::default()`.
|
/// Returns a TaggedValue from the type, where that value is its type's `Default::default()`.
|
||||||
/// Dispatches by the type's name (the field that round-trips through serde) so it works for both
|
/// Dispatches by name for concrete types and structurally by element for ranked types, where the name
|
||||||
/// freshly constructed types and types deserialized from disk where the runtime `TypeId` is unavailable.
|
/// field is what round-trips through serde so it works even for types deserialized from disk.
|
||||||
pub fn from_type(input: &Type) -> Option<Self> {
|
pub fn from_type(input: &Type) -> Option<Self> {
|
||||||
match input {
|
match input {
|
||||||
Type::Generic(_) => None,
|
Type::Generic(_) => None,
|
||||||
@@ -314,23 +370,44 @@ macro_rules! tagged_value {
|
|||||||
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
|
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
|
||||||
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
|
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
|
||||||
if name == std::any::type_name::<()>() { return Some(TaggedValue::None) }
|
if name == std::any::type_name::<()>() { return Some(TaggedValue::None) }
|
||||||
// List-wrapped types need a single-item default with the element's default, not an empty list
|
if name == std::any::type_name::<Color>() { return Some(TaggedValue::Color(Color::default())) }
|
||||||
if name == std::any::type_name::<List<Color>>() { return Some(TaggedValue::Color(Some(Color::default()))) }
|
if name == std::any::type_name::<Gradient>() { return Some(TaggedValue::Gradient(Gradient::default())) }
|
||||||
if name == std::any::type_name::<List<GradientStops>>() { return Some(TaggedValue::Gradient(GradientStops::default())) }
|
|
||||||
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
|
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
|
||||||
if name == std::any::type_name::<List<f64>>() { return Some(TaggedValue::F64Array(Vec::new())) }
|
if name == std::any::type_name::<BrushTrace>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
|
||||||
if name == std::any::type_name::<List<BrushStroke>>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
|
// Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time
|
||||||
// Types whose `TaggedValue` variant has been removed. They route through `TypeDefault` instead, with `to_dynany`/`to_any` constructing the actual default at execution time.
|
macro_rules! check_bare {
|
||||||
macro_rules! check {
|
|
||||||
($type_default:ty) => {
|
($type_default:ty) => {
|
||||||
if name == std::any::type_name::<$type_default>() { return Some(TaggedValue::TypeDefault(concrete_type.clone())); }
|
if name == std::any::type_name::<$type_default>() { return Some(TaggedValue::TypeDefault(input.clone())); }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for_each_type_default!(check);
|
for_each_bare_type_default!(check_bare);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Type::Fn(_, output) => TaggedValue::from_type(output),
|
Type::Fn(_, output) => TaggedValue::from_type(output),
|
||||||
Type::Future(output) => TaggedValue::from_type(output),
|
Type::Future(output) => TaggedValue::from_type(output),
|
||||||
|
// Element types with a dedicated variant use it directly (the variant's value is a rank-0 cell); the rest store the structural type
|
||||||
|
Type::Item(element) => TaggedValue::from_type(element).or_else(|| {
|
||||||
|
macro_rules! check {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_item_type_default!(check);
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
// Structural lists match by element; `List<f64>` stays the dedicated `F64Array` variant
|
||||||
|
Type::List(element) => {
|
||||||
|
if **element == concrete!(f64) {
|
||||||
|
return Some(TaggedValue::F64Array(Vec::new()));
|
||||||
|
}
|
||||||
|
macro_rules! check {
|
||||||
|
($type_default:ty) => {
|
||||||
|
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_list_type_default!(check);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +421,7 @@ macro_rules! tagged_value {
|
|||||||
// MANUAL VARIANTS
|
// MANUAL VARIANTS
|
||||||
// ===============
|
// ===============
|
||||||
Self::None => "()".to_string(),
|
Self::None => "()".to_string(),
|
||||||
Self::TypeDefault(td) => format!("TypeDefault({})", td.name),
|
Self::TypeDefault(td) => format!("TypeDefault({td})"),
|
||||||
Self::F64Array(values) => format!("F64Array({values:?})"),
|
Self::F64Array(values) => format!("F64Array({values:?})"),
|
||||||
Self::Color(color) => format!("Color({color:?})"),
|
Self::Color(color) => format!("Color({color:?})"),
|
||||||
Self::Gradient(stops) => format!("Gradient({stops:?})"),
|
Self::Gradient(stops) => format!("Gradient({stops:?})"),
|
||||||
@@ -402,19 +479,19 @@ tagged_value! {
|
|||||||
DVec2(DVec2),
|
DVec2(DVec2),
|
||||||
#[serde(alias = "Affine2")]
|
#[serde(alias = "Affine2")]
|
||||||
DAffine2(DAffine2),
|
DAffine2(DAffine2),
|
||||||
OptionalDAffine2(Option<DAffine2>),
|
|
||||||
#[serde(alias = "FillGradient")]
|
|
||||||
LegacyGradient(graphic_types::migrations::legacy::Gradient),
|
|
||||||
Font(Font),
|
Font(Font),
|
||||||
Footprint(Footprint),
|
Footprint(Footprint),
|
||||||
VectorModification(Box<VectorModification>),
|
VectorModification(Box<VectorModification>),
|
||||||
ImageData(Image<Color>),
|
ImageData(Image<Color>),
|
||||||
Resource(graphene_application_io::resource::ResourceId),
|
Resource(ResourceId),
|
||||||
|
// Legacy
|
||||||
|
#[serde(alias = "OptionalDAffine2")]
|
||||||
|
LegacyOptionalDAffine2(Option<DAffine2>),
|
||||||
|
#[serde(alias = "FillGradient")]
|
||||||
|
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
||||||
// ==========
|
// ==========
|
||||||
// ENUM TYPES
|
// ENUM TYPES
|
||||||
// ==========
|
// ==========
|
||||||
#[serde(alias = "Fill")]
|
|
||||||
LegacyFill(graphic_types::migrations::legacy::Fill),
|
|
||||||
BlendMode(core_types::blending::BlendMode),
|
BlendMode(core_types::blending::BlendMode),
|
||||||
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
||||||
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
||||||
@@ -444,6 +521,8 @@ tagged_value! {
|
|||||||
StrokeJoin(vector::style::StrokeJoin),
|
StrokeJoin(vector::style::StrokeJoin),
|
||||||
StrokeAlign(vector::style::StrokeAlign),
|
StrokeAlign(vector::style::StrokeAlign),
|
||||||
PaintOrder(vector::style::PaintOrder),
|
PaintOrder(vector::style::PaintOrder),
|
||||||
|
DashPattern(vector::style::DashPattern),
|
||||||
|
BoxCorners(vector::misc::BoxCorners),
|
||||||
GradientType(vector::style::GradientType),
|
GradientType(vector::style::GradientType),
|
||||||
GradientSpreadMethod(vector::style::GradientSpreadMethod),
|
GradientSpreadMethod(vector::style::GradientSpreadMethod),
|
||||||
ReferencePoint(vector::ReferencePoint),
|
ReferencePoint(vector::ReferencePoint),
|
||||||
@@ -451,6 +530,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 {
|
||||||
@@ -512,11 +594,11 @@ impl TaggedValue {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_gradient(input: &str) -> Option<GradientStops> {
|
fn to_gradient(input: &str) -> Option<Gradient> {
|
||||||
// String syntax: (e.g. "000000ff, ff0000ff")
|
// String syntax: (e.g. "000000ff, ff0000ff")
|
||||||
let stops = input.split(',').filter_map(|s| to_color(s.trim())).collect::<Vec<_>>();
|
let stops = input.split(',').filter_map(|s| to_color(s.trim())).collect::<Vec<_>>();
|
||||||
if stops.len() == 1 {
|
if stops.len() == 1 {
|
||||||
Some(GradientStops::new(vec![
|
Some(Gradient::new(vec![
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -530,7 +612,7 @@ impl TaggedValue {
|
|||||||
]))
|
]))
|
||||||
} else if stops.len() >= 2 {
|
} else if stops.len() >= 2 {
|
||||||
let step = 1. / (stops.len() - 1) as f64;
|
let step = 1. / (stops.len() - 1) as f64;
|
||||||
Some(GradientStops::new(stops.into_iter().enumerate().map(|(i, color)| GradientStop {
|
Some(Gradient::new(stops.into_iter().enumerate().map(|(i, color)| GradientStop {
|
||||||
position: i as f64 * step,
|
position: i as f64 * step,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color,
|
color,
|
||||||
@@ -583,19 +665,21 @@ impl TaggedValue {
|
|||||||
() if ty == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
|
() if ty == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
|
||||||
() if ty == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
|
() if ty == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
|
||||||
() if ty == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
|
() if ty == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
|
||||||
// `Color` (not in a `List`) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants
|
() if ty == TypeId::of::<Color>() => to_color(string).map(TaggedValue::Color)?,
|
||||||
() if ty == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
// The Fill/Stroke paint wires carry `Graphic` or `Gradient` elements, so a paint default parses through the element recursion as a color or gradient literal
|
||||||
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
() if ty == TypeId::of::<Graphic>() => to_color(string).map(TaggedValue::Color)?,
|
||||||
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||||
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
|
||||||
() if ty == TypeId::of::<List<GradientStops>>() => 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::<BoxCorners>() => TaggedValue::BoxCorners(BoxCorners::from(string)),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
Some(ty)
|
Some(ty)
|
||||||
}
|
}
|
||||||
Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
|
Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
|
||||||
Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
|
Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
|
||||||
|
Type::Item(element) => TaggedValue::from_primitive_string(string, element),
|
||||||
|
Type::List(element) => TaggedValue::from_primitive_string(string, element),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,6 +689,16 @@ impl TaggedValue {
|
|||||||
_ => panic!("Passed value is not of type u32"),
|
_ => panic!("Passed value is not of type u32"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The stored form of a paint input's red-slash "no paint" choice: the `List<Graphic>` type default, materializing as an empty paint list.
|
||||||
|
pub fn no_paint() -> Self {
|
||||||
|
TaggedValue::TypeDefault(list!(Graphic))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this is the `List<Graphic>` type default created by [`Self::no_paint`] (and by disconnecting a paint wire).
|
||||||
|
pub fn is_no_paint(&self) -> bool {
|
||||||
|
matches!(self, TaggedValue::TypeDefault(td) if *td == list!(Graphic))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom deserializer hooked onto `NodeInput::Value::tagged_value` that intercepts removed-variant tags before delegating to `TaggedValue`'s standard derive.
|
/// Custom deserializer hooked onto `NodeInput::Value::tagged_value` that intercepts removed-variant tags before delegating to `TaggedValue`'s standard derive.
|
||||||
@@ -612,14 +706,16 @@ impl TaggedValue {
|
|||||||
/// Routes legacy variant names into modern variants, in typed Rust. Each legacy name is also matched against the historical `#[serde(alias = "...")]` spellings the deleted variant accepted, so old-shape inner payloads are caught:
|
/// Routes legacy variant names into modern variants, in typed Rust. Each legacy name is also matched against the historical `#[serde(alias = "...")]` spellings the deleted variant accepted, so old-shape inner payloads are caught:
|
||||||
///
|
///
|
||||||
/// - `BrushCache` → `TaggedValue::None` (purely runtime cache; no payload to preserve)
|
/// - `BrushCache` → `TaggedValue::None` (purely runtime cache; no payload to preserve)
|
||||||
/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(descriptor!(List<Graphic>))`
|
/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(list!(Graphic))`
|
||||||
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(descriptor!(List<Artboard>))`
|
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(list!(Artboard))`
|
||||||
/// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`):
|
/// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`):
|
||||||
/// - non-empty (the legacy `image` proto's input 1, where the inner `Raster<CPU>` serializes as the embedded `Image<Color>`) → `TaggedValue::ImageData(<inner Image<Color>>)`
|
/// - non-empty (the legacy `image` proto's input 1, where the inner `Raster<CPU>` serializes as the embedded `Image<Color>`) → `TaggedValue::ImageData(<inner Image<Color>>)`
|
||||||
/// - empty → `TaggedValue::TypeDefault(descriptor!(List<Raster<CPU>>))`
|
/// - empty → `TaggedValue::TypeDefault(list!(Raster<CPU>))`
|
||||||
/// - `Vector` (or alias `VectorData`):
|
/// - `Vector` (or alias `VectorData`):
|
||||||
/// - non-empty → `TaggedValue::VectorModification(<built from first element>)` (the document_migration's Path pass disambiguates this between SVG-import legacy and a discardable modern baked value via the input's `exposed` flag)
|
/// - non-empty → `TaggedValue::VectorModification(<built from first element>)` (the document_migration's Path pass disambiguates this between SVG-import legacy and a discardable modern baked value via the input's `exposed` flag)
|
||||||
/// - empty → `TaggedValue::TypeDefault(descriptor!(List<Vector>))`
|
/// - empty → `TaggedValue::TypeDefault(list!(Vector))`
|
||||||
|
/// - `FillChoice` → `TaggedValue::Color` (solid), `TaggedValue::Gradient` (gradient), or `TaggedValue::no_paint()` (none)
|
||||||
|
/// - `TypeDefault` with the old bare-`TypeDescriptor` payload → the same variant wrapping a `Type` (name-encoded `List` normalized to structural)
|
||||||
///
|
///
|
||||||
/// All other tags (including ones with the modern shape) fall through to the standard derived `Deserialize` for `TaggedValue`.
|
/// All other tags (including ones with the modern shape) fall through to the standard derived `Deserialize` for `TaggedValue`.
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
@@ -634,8 +730,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
|
|||||||
{
|
{
|
||||||
match tag.as_str() {
|
match tag.as_str() {
|
||||||
"BrushCache" => return Ok(MemoHash::new(TaggedValue::None)),
|
"BrushCache" => return Ok(MemoHash::new(TaggedValue::None)),
|
||||||
"Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Graphic>)))),
|
"Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Graphic)))),
|
||||||
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Artboard>)))),
|
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Artboard)))),
|
||||||
"Raster" | "ImageFrame" | "RasterData" | "Image" => {
|
"Raster" | "ImageFrame" | "RasterData" | "Image" => {
|
||||||
let first_element = content
|
let first_element = content
|
||||||
.as_object()
|
.as_object()
|
||||||
@@ -646,7 +742,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
|
|||||||
let image: Image<Color> = serde_json::from_value(image_value.clone()).map_err(serde::de::Error::custom)?;
|
let image: Image<Color> = serde_json::from_value(image_value.clone()).map_err(serde::de::Error::custom)?;
|
||||||
return Ok(MemoHash::new(TaggedValue::ImageData(image)));
|
return Ok(MemoHash::new(TaggedValue::ImageData(image)));
|
||||||
}
|
}
|
||||||
return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Raster<CPU>>))));
|
return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Raster<CPU>))));
|
||||||
}
|
}
|
||||||
"Vector" | "VectorData" => {
|
"Vector" | "VectorData" => {
|
||||||
let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?;
|
let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?;
|
||||||
@@ -654,12 +750,42 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
|
|||||||
let modification = Box::new(VectorModification::create_from_vector(&vector));
|
let modification = Box::new(VectorModification::create_from_vector(&vector));
|
||||||
return Ok(MemoHash::new(TaggedValue::VectorModification(modification)));
|
return Ok(MemoHash::new(TaggedValue::VectorModification(modification)));
|
||||||
}
|
}
|
||||||
return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Vector>))));
|
return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Vector))));
|
||||||
}
|
}
|
||||||
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<GradientStops>`.
|
// The `TypeDefault` payload used to be a bare `TypeDescriptor`; it now carries a `Type`
|
||||||
// Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `GradientStops` has none of those (it has `position`/`midpoint`/`color`).
|
"TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("name")) => {
|
||||||
|
let descriptor: TypeDescriptor = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
|
||||||
|
return Ok(MemoHash::new(TaggedValue::TypeDefault(Type::Concrete(descriptor).normalize_rank())));
|
||||||
|
}
|
||||||
|
// The `Color` tag used to carry `Option<Color>`, where a `null` payload (or an empty legacy color table) was the red-slash "no paint" choice
|
||||||
|
"Color" | "ColorTable" | "OptionalColor" | "ColorNotInTable"
|
||||||
|
if content.is_null()
|
||||||
|
|| content
|
||||||
|
.as_object()
|
||||||
|
.and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances")))
|
||||||
|
.and_then(|e| e.as_array())
|
||||||
|
.is_some_and(|colors| colors.is_empty()) =>
|
||||||
|
{
|
||||||
|
return Ok(MemoHash::new(TaggedValue::no_paint()));
|
||||||
|
}
|
||||||
|
// The removed `FillChoice` variant decomposes into the plain paint values
|
||||||
|
"FillChoice" => {
|
||||||
|
if let Some(payload) = content.as_object() {
|
||||||
|
if let Some(solid) = payload.get("Solid") {
|
||||||
|
let color: Color = serde_json::from_value(solid.clone()).map_err(serde::de::Error::custom)?;
|
||||||
|
return Ok(MemoHash::new(TaggedValue::Color(color)));
|
||||||
|
}
|
||||||
|
if let Some(gradient) = payload.get("Gradient") {
|
||||||
|
let gradient: Gradient = serde_json::from_value(gradient.clone()).map_err(serde::de::Error::custom)?;
|
||||||
|
return Ok(MemoHash::new(TaggedValue::Gradient(gradient)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(MemoHash::new(TaggedValue::no_paint()));
|
||||||
|
}
|
||||||
|
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<Gradient>`.
|
||||||
|
// Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `Gradient` has none of those (it has `position`/`midpoint`/`color`).
|
||||||
"Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => {
|
"Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => {
|
||||||
let gradient: graphic_types::migrations::legacy::Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
|
let gradient: graphic_types::migrations::legacy::LegacyGradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
|
||||||
return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
|
return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -707,7 +833,7 @@ impl<'i, T: 'i + AsRef<U> + Sync + Send, U: 'i + StaticType + Sync + Send> Node<
|
|||||||
type Output = FutureAny<'i>;
|
type Output = FutureAny<'i>;
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn eval(&'i self, _: DAny<'i>) -> Self::Output {
|
fn eval(&'i self, _: DAny<'i>) -> Self::Output {
|
||||||
Box::pin(async move { Box::new(self.0.as_ref()) as DAny<'i> })
|
Box::pin(async move { Box::new(Item::new_from_element(self.0.as_ref())) as DAny<'i> })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,26 +904,26 @@ impl CacheHash for RenderOutput {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod typedefault_dispatch {
|
mod typedefault_dispatch {
|
||||||
use super::*;
|
use super::*;
|
||||||
use core_types::descriptor;
|
use core_types::{concrete, item, list};
|
||||||
|
|
||||||
/// Round-trips every type listed in [`for_each_type_default`] through `TaggedValue::TypeDefault → to_dynany / to_any` and asserts the resulting concrete type matches the descriptor.
|
/// Round-trips every type in the type-default lists through `TaggedValue::TypeDefault → to_dynany / to_any` and asserts the resulting concrete type matches the stored type.
|
||||||
///
|
///
|
||||||
/// This guards against the only way to break the recursion invariant in the unwrap functions: someone hand-rolling a `TypeDefault`-yielding case in `from_type` (or the macro's expansion in one of the unwrap sites silently failing to match a name). If it fails, the message points at the specific type and the structural reason.
|
/// This guards against the only way to break the recursion invariant in the unwrap functions: someone hand-rolling a `TypeDefault`-yielding case in `from_type` (or the macro's expansion in one of the unwrap sites silently failing to match a name). If it fails, the message points at the specific type and the structural reason.
|
||||||
#[test]
|
#[test]
|
||||||
fn typedefault_dispatch_terminates() {
|
fn typedefault_dispatch_terminates() {
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($type_default:ty) => {{
|
($type_default:ty, $stored:expr) => {{
|
||||||
let descriptor = descriptor!($type_default);
|
let ty: Type = $stored;
|
||||||
let expected_type_id = std::any::TypeId::of::<$type_default>();
|
let expected_type_id = std::any::TypeId::of::<$type_default>();
|
||||||
let dyn_value = TaggedValue::TypeDefault(descriptor.clone()).to_dynany();
|
let dyn_value = TaggedValue::TypeDefault(ty.clone()).to_dynany();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DynAny::type_id(&*dyn_value),
|
DynAny::type_id(&*dyn_value),
|
||||||
expected_type_id,
|
expected_type_id,
|
||||||
"`to_dynany(TypeDefault({0}))` did not produce a `{0}` — `for_each_type_default!` lists this type but the unwrap site doesn't handle it. Without a match, `to_dynany` falls back to `from_type_or_none`, which returns `TypeDefault({0})` again and recurses forever.",
|
"`to_dynany(TypeDefault({0}))` did not produce a `{0}` — the type-default lists cover this type but the unwrap site doesn't handle it. Without a match, `to_dynany` falls back to `from_type_or_none`, which returns `TypeDefault({0})` again and recurses forever.",
|
||||||
std::any::type_name::<$type_default>(),
|
std::any::type_name::<$type_default>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let arc_value = TaggedValue::TypeDefault(descriptor).to_any();
|
let arc_value = TaggedValue::TypeDefault(ty).to_any();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(*arc_value).type_id(),
|
(*arc_value).type_id(),
|
||||||
expected_type_id,
|
expected_type_id,
|
||||||
@@ -806,6 +932,57 @@ mod typedefault_dispatch {
|
|||||||
);
|
);
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
for_each_type_default!(check);
|
macro_rules! check_item {
|
||||||
|
($element:ty) => {
|
||||||
|
check!(Item<$element>, item!($element));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
macro_rules! check_list {
|
||||||
|
($element:ty) => {
|
||||||
|
check!(List<$element>, list!($element));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
macro_rules! check_bare {
|
||||||
|
($type_default:ty) => {
|
||||||
|
check!($type_default, concrete!($type_default));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for_each_item_type_default!(check_item);
|
||||||
|
for_each_list_type_default!(check_list);
|
||||||
|
for_each_bare_type_default!(check_bare);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod paint_default_parsing {
|
||||||
|
use super::*;
|
||||||
|
use core_types::{item, list};
|
||||||
|
|
||||||
|
/// A Fill/Stroke paint wire carries `Graphic` elements, so its `Color::BLACK` default must parse through the
|
||||||
|
/// element recursion 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", &list!(Graphic)),
|
||||||
|
black,
|
||||||
|
"a `List<Graphic>` paint wire should resolve its color default"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TaggedValue::from_primitive_string("Color::BLACK", &item!(Graphic)),
|
||||||
|
black,
|
||||||
|
"an `Item<Graphic>` paint wire should resolve its color default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[test]
|
||||||
|
fn empty_legacy_color_table_deserializes_to_no_paint() {
|
||||||
|
for payload in [r#"{"ColorTable": {"instances": []}}"#, r#"{"ColorTable": {"element": []}}"#, r#"{"Color": null}"#] {
|
||||||
|
let mut deserializer = serde_json::Deserializer::from_str(payload);
|
||||||
|
let value = deserialize_tagged_value_with_legacy_migration(&mut deserializer).expect("The legacy payload should deserialize");
|
||||||
|
assert!(value.is_no_paint(), "The legacy payload `{payload}` should migrate to the no-paint choice");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ extern crate log;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate core_types;
|
extern crate core_types;
|
||||||
|
|
||||||
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, descriptor, generic};
|
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, descriptor, generic, item, list};
|
||||||
|
|
||||||
pub mod application_io;
|
pub mod application_io;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ impl ProtoNetwork {
|
|||||||
let we_introduce_new_deps = !combined_deps.contains(new_deps);
|
let we_introduce_new_deps = !combined_deps.contains(new_deps);
|
||||||
|
|
||||||
// For diverging branches, we can add a cache node for all branches which don't reqire all dependencies
|
// For diverging branches, we can add a cache node for all branches which don't reqire all dependencies
|
||||||
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies.into_iter()) {
|
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies) {
|
||||||
if let Some(new_id) = new_id {
|
if let Some(new_id) = new_id {
|
||||||
*child_node = new_id;
|
*child_node = new_id;
|
||||||
} else if we_introduce_new_deps || deps != combined_deps {
|
} else if we_introduce_new_deps || deps != combined_deps {
|
||||||
@@ -634,6 +634,31 @@ pub struct TypingContext {
|
|||||||
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
|
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
|
||||||
inferred: HashMap<NodeId, NodeIOTypes>,
|
inferred: HashMap<NodeId, NodeIOTypes>,
|
||||||
constructor: HashMap<NodeId, NodeConstructor>,
|
constructor: HashMap<NodeId, NodeConstructor>,
|
||||||
|
promotions: HashMap<NodeId, Vec<(usize, Promotion)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rank adapter which type resolution marks for insertion between a wire and a connector whose ranks differ,
|
||||||
|
/// carrying the element type the adapter is registered under.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Promotion {
|
||||||
|
/// Raises an `Item<X>` wire onto a `List<X>` connector as a one-element list.
|
||||||
|
ItemToList(Type),
|
||||||
|
/// Bundles a whole `List<X>` wire into one opaque `Item<Bundle<X>>` cell.
|
||||||
|
Bundle(Type),
|
||||||
|
/// Unbundles an `Item<Bundle<X>>` wire back into the whole `List<X>`.
|
||||||
|
Unbundle(Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Promotion {
|
||||||
|
/// The registry identifier of the adapter node monomorphized for this promotion's element type.
|
||||||
|
pub fn adapter_identifier(&self) -> ProtoNodeIdentifier {
|
||||||
|
let (adapter_name, element) = match self {
|
||||||
|
Self::ItemToList(element) => ("graphene_core::ops::ItemToListNode", element),
|
||||||
|
Self::Bundle(element) => ("graphene_core::ops::BundleNode", element),
|
||||||
|
Self::Unbundle(element) => ("graphene_core::ops::UnbundleNode", element),
|
||||||
|
};
|
||||||
|
ProtoNodeIdentifier::with_owned_string(format!("{adapter_name}<{}>", element.identifier_name()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypingContext {
|
impl TypingContext {
|
||||||
@@ -658,9 +683,20 @@ impl TypingContext {
|
|||||||
|
|
||||||
pub fn remove_inference(&mut self, node_id: NodeId) -> Option<NodeIOTypes> {
|
pub fn remove_inference(&mut self, node_id: NodeId) -> Option<NodeIOTypes> {
|
||||||
self.constructor.remove(&node_id);
|
self.constructor.remove(&node_id);
|
||||||
|
self.promotions.remove(&node_id);
|
||||||
self.inferred.remove(&node_id)
|
self.inferred.remove(&node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the input positions of a node which type resolution marked for rank promotion, with each position's adapter.
|
||||||
|
pub fn promotions(&self, node_id: NodeId) -> Option<&Vec<(usize, Promotion)>> {
|
||||||
|
self.promotions.get(&node_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up the sole constructor registered under an adapter identifier, such as an Item -> List promotion adapter.
|
||||||
|
pub fn adapter_constructor(&self, identifier: &ProtoNodeIdentifier) -> Option<NodeConstructor> {
|
||||||
|
self.lookup.get(identifier).and_then(|implementations| implementations.values().next().copied())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the node constructor for a given node id.
|
/// Returns the node constructor for a given node id.
|
||||||
pub fn constructor(&self, node_id: NodeId) -> Option<NodeConstructor> {
|
pub fn constructor(&self, node_id: NodeId) -> Option<NodeConstructor> {
|
||||||
self.constructor.get(&node_id).copied()
|
self.constructor.get(&node_id).copied()
|
||||||
@@ -731,6 +767,9 @@ impl TypingContext {
|
|||||||
// Graphite doesn't have subtyping currently, but it used to have it, and may do so again, so we make sure to compare types in this way to make things easier.
|
// Graphite doesn't have subtyping currently, but it used to have it, and may do so again, so we make sure to compare types in this way to make things easier.
|
||||||
// More details explained here: <https://github.com/GraphiteEditor/Graphite/issues/1741>
|
// More details explained here: <https://github.com/GraphiteEditor/Graphite/issues/1741>
|
||||||
(Type::Fn(in1, out1), Type::Fn(in2, out2)) => valid_type(out2, out1) && valid_type(in1, in2),
|
(Type::Fn(in1, out1), Type::Fn(in2, out2)) => valid_type(out2, out1) && valid_type(in1, in2),
|
||||||
|
// Ranked wrappers of the same rank are compared element-wise
|
||||||
|
(Type::Item(element1), Type::Item(element2)) => valid_type(element1, element2),
|
||||||
|
(Type::List(element1), Type::List(element2)) => valid_type(element1, element2),
|
||||||
// If either the proposed input or the allowed input are generic, we allow the substitution (meaning this is a valid subtype).
|
// If either the proposed input or the allowed input are generic, we allow the substitution (meaning this is a valid subtype).
|
||||||
// TODO: Add proper generic counting which is not based on the name
|
// TODO: Add proper generic counting which is not based on the name
|
||||||
(Type::Generic(_), _) | (_, Type::Generic(_)) => true,
|
(Type::Generic(_), _) | (_, Type::Generic(_)) => true,
|
||||||
@@ -768,6 +807,75 @@ impl TypingContext {
|
|||||||
|
|
||||||
match valid_impls.as_slice() {
|
match valid_impls.as_slice() {
|
||||||
[] => {
|
[] => {
|
||||||
|
// Retry allowing a wire whose rank differs from its connector, satisfied at construction time by an inserted promotion adapter.
|
||||||
|
// Every rank relation is matched structurally; only the `Bundle` layer inside an `Item` cell remains name-encoded.
|
||||||
|
fn promotable_adapter(from: &Type, to: &Type) -> Option<Promotion> {
|
||||||
|
fn concrete_name(ty: &Type) -> Option<&str> {
|
||||||
|
match ty {
|
||||||
|
Type::Concrete(descriptor) => Some(&descriptor.name),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Type::Fn(from_input, from_output), Type::Fn(to_input, to_output)) = (from, to) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if !valid_type(from_input, to_input) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_value = from_output.nested_type();
|
||||||
|
let to_value = to_output.nested_type();
|
||||||
|
|
||||||
|
match (from_value, to_value) {
|
||||||
|
// An `Item<X>` wire may feed a `List<X>` connector via a singleton raise
|
||||||
|
(Type::Item(from_element), Type::List(element)) if valid_type(from_element, element) => Some(Promotion::ItemToList((**element).clone())),
|
||||||
|
|
||||||
|
// A `List<X>` wire may feed an `Item<Bundle<X>>` connector by bundling the whole list into one opaque cell
|
||||||
|
(Type::List(element), Type::Item(_)) if to_value.bundle_element_name().is_some_and(|name| Some(name) == concrete_name(element)) => Some(Promotion::Bundle((**element).clone())),
|
||||||
|
|
||||||
|
// An `Item<Bundle<X>>` wire may feed a `List<X>` connector by unbundling it back into the whole list
|
||||||
|
(Type::Item(_), Type::List(element)) if from_value.bundle_element_name().is_some_and(|name| Some(name) == concrete_name(element)) => {
|
||||||
|
Some(Promotion::Unbundle((**element).clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut promotable_matches = impls
|
||||||
|
.keys()
|
||||||
|
.filter(|node_io| collect_generics(node_io).is_empty() && valid_type(&node_io.call_argument, call_argument) && inputs.len() == node_io.inputs.len())
|
||||||
|
.filter_map(|node_io| {
|
||||||
|
let mut required_promotions = Vec::new();
|
||||||
|
for (index, (provided, expected)) in inputs.iter().zip(node_io.inputs.iter()).enumerate() {
|
||||||
|
if valid_type(provided, expected) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
required_promotions.push((index, promotable_adapter(provided, expected)?));
|
||||||
|
}
|
||||||
|
Some((node_io, required_promotions))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Prefer the variant needing the fewest promotions, so an Item wire resolves the all-Item variant instead of raising every ranked connector to reach the mapped variant; a tie stays ambiguous.
|
||||||
|
promotable_matches.sort_by_key(|(_, required_promotions)| required_promotions.len());
|
||||||
|
let minimum_promotions = promotable_matches.first().map(|(_, required_promotions)| required_promotions.len());
|
||||||
|
let tied_at_minimum = promotable_matches
|
||||||
|
.iter()
|
||||||
|
.take_while(|(_, required_promotions)| Some(required_promotions.len()) == minimum_promotions)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if tied_at_minimum == 1 {
|
||||||
|
let (node_io, required_promotions) = promotable_matches.remove(0);
|
||||||
|
let node_io = node_io.clone();
|
||||||
|
|
||||||
|
self.inferred.insert(node_id, node_io.clone());
|
||||||
|
self.constructor.insert(node_id, impls[&node_io]);
|
||||||
|
self.promotions.insert(node_id, required_promotions);
|
||||||
|
return Ok(node_io);
|
||||||
|
}
|
||||||
|
|
||||||
let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0);
|
let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0);
|
||||||
let mut best_errors = usize::MAX;
|
let mut best_errors = usize::MAX;
|
||||||
let mut error_inputs = Vec::new();
|
let mut error_inputs = Vec::new();
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub async fn export_document(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the graph
|
// Execute the graph
|
||||||
let result = executor.execute(render_config).await?;
|
let result = executor.execute(render_config.into_context()).await?;
|
||||||
|
|
||||||
// Handle the result based on output type
|
// Handle the result based on output type
|
||||||
match result {
|
match result {
|
||||||
@@ -195,7 +195,7 @@ pub async fn export_gif(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the graph for this frame
|
// Execute the graph for this frame
|
||||||
let result = executor.execute(render_config).await?;
|
let result = executor.execute(render_config.into_context()).await?;
|
||||||
|
|
||||||
// Extract RGBA data from result
|
// Extract RGBA data from result
|
||||||
let (data, img_width, img_height) = match result {
|
let (data, img_width, img_height) = match result {
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ use graphene_std::application_io::RenderConfig;
|
|||||||
|
|
||||||
fn subsequent_evaluations(c: &mut Criterion) {
|
fn subsequent_evaluations(c: &mut Criterion) {
|
||||||
let mut group = c.benchmark_group("Subsequent Evaluations");
|
let mut group = c.benchmark_group("Subsequent Evaluations");
|
||||||
let context = RenderConfig::default();
|
let context = RenderConfig::default().into_context();
|
||||||
bench_for_each_demo(&mut group, |name, g| {
|
bench_for_each_demo(&mut group, |name, g| {
|
||||||
let (executor, _) = setup_network(name);
|
let (executor, _) = setup_network(name);
|
||||||
|
let context = context.clone();
|
||||||
g.bench_function(name, |b| {
|
g.bench_function(name, |b| {
|
||||||
b.iter(|| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context))).unwrap())
|
b.iter(|| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context.clone()))).unwrap())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
group.finish();
|
group.finish();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ fn setup_run_cached(name: &str) -> DynamicExecutor {
|
|||||||
let (executor, _) = setup_network(name);
|
let (executor, _) = setup_network(name);
|
||||||
|
|
||||||
// Warm up the cache by running once
|
// Warm up the cache by running once
|
||||||
let context = RenderConfig::default();
|
let context = RenderConfig::default().into_context();
|
||||||
let _ = futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), context));
|
let _ = futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), context));
|
||||||
|
|
||||||
executor
|
executor
|
||||||
@@ -19,7 +19,7 @@ fn setup_run_cached(name: &str) -> DynamicExecutor {
|
|||||||
#[library_benchmark]
|
#[library_benchmark]
|
||||||
#[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_cached)]
|
#[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_cached)]
|
||||||
pub fn run_cached(executor: DynamicExecutor) -> DynamicExecutor {
|
pub fn run_cached(executor: DynamicExecutor) -> DynamicExecutor {
|
||||||
let context = RenderConfig::default();
|
let context = RenderConfig::default().into_context();
|
||||||
black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap());
|
black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap());
|
||||||
|
|
||||||
// Return the executor so its teardown happens outside the measured section
|
// Return the executor so its teardown happens outside the measured section
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ use graphene_std::application_io::RenderConfig;
|
|||||||
|
|
||||||
fn run_once(c: &mut Criterion) {
|
fn run_once(c: &mut Criterion) {
|
||||||
let mut group = c.benchmark_group("Run Once");
|
let mut group = c.benchmark_group("Run Once");
|
||||||
let context = RenderConfig::default();
|
let context = RenderConfig::default().into_context();
|
||||||
bench_for_each_demo(&mut group, |name, g| {
|
bench_for_each_demo(&mut group, |name, g| {
|
||||||
|
let context = context.clone();
|
||||||
g.bench_function(name, |b| {
|
g.bench_function(name, |b| {
|
||||||
b.iter_batched(
|
b.iter_batched(
|
||||||
|| setup_network(name),
|
|| setup_network(name),
|
||||||
|(executor, _)| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context))).unwrap(),
|
|(executor, _)| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context.clone()))).unwrap(),
|
||||||
criterion::BatchSize::SmallInput,
|
criterion::BatchSize::SmallInput,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ fn setup_run_once(name: &str) -> DynamicExecutor {
|
|||||||
#[library_benchmark]
|
#[library_benchmark]
|
||||||
#[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_once)]
|
#[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_once)]
|
||||||
pub fn run_once(executor: DynamicExecutor) -> DynamicExecutor {
|
pub fn run_once(executor: DynamicExecutor) -> DynamicExecutor {
|
||||||
let context = application_io::RenderConfig::default();
|
let context = application_io::RenderConfig::default().into_context();
|
||||||
black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap());
|
black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap());
|
||||||
|
|
||||||
// Return the executor so its teardown happens outside the measured section
|
// Return the executor so its teardown happens outside the measured section
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ impl DynamicExecutor {
|
|||||||
|
|
||||||
impl<I> Executor<I, TaggedValue> for &DynamicExecutor
|
impl<I> Executor<I, TaggedValue> for &DynamicExecutor
|
||||||
where
|
where
|
||||||
I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe,
|
I: StaticType + 'static + Send + Sync,
|
||||||
{
|
{
|
||||||
fn execute(&self, input: I) -> LocalFuture<'_, Result<TaggedValue, Box<dyn Error>>> {
|
fn execute(&self, input: I) -> LocalFuture<'_, Result<TaggedValue, Box<dyn Error>>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -413,7 +413,19 @@ impl BorrowTree {
|
|||||||
ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"),
|
ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"),
|
||||||
ConstructionArgs::Nodes(ids) => {
|
ConstructionArgs::Nodes(ids) => {
|
||||||
let ids = ids.to_vec();
|
let ids = ids.to_vec();
|
||||||
let construction_nodes = self.node_deps(&ids);
|
let mut construction_nodes = self.node_deps(&ids);
|
||||||
|
|
||||||
|
// Wrap arguments the typing pass marked for rank promotion with their adapter node
|
||||||
|
if let Some(promotions) = typing_context.promotions(id) {
|
||||||
|
for (argument_index, promotion) in promotions {
|
||||||
|
let adapter_constructor = typing_context
|
||||||
|
.adapter_constructor(&promotion.adapter_identifier())
|
||||||
|
.ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
|
||||||
|
let adapter = adapter_constructor(vec![construction_nodes[*argument_index].clone()]).await;
|
||||||
|
construction_nodes[*argument_index] = NodeContainer::new(adapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
|
let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
|
||||||
let node = constructor(construction_nodes).await;
|
let node = constructor(construction_nodes).await;
|
||||||
let node = NodeContainer::new(node);
|
let node = NodeContainer::new(node);
|
||||||
@@ -430,19 +442,4 @@ impl BorrowTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test;
|
||||||
use super::*;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn push_node_sync() {
|
|
||||||
let mut tree = BorrowTree::default();
|
|
||||||
let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).into()), vec![]);
|
|
||||||
let context = TypingContext::default();
|
|
||||||
let future = tree.push_node(NodeId(0), val_1_protonode, &context);
|
|
||||||
futures::executor::block_on(future).unwrap();
|
|
||||||
let _node = tree.get(NodeId(0)).unwrap();
|
|
||||||
let result = futures::executor::block_on(tree.eval(NodeId(0), ()));
|
|
||||||
assert_eq!(result, Some(2u32));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,624 @@
|
|||||||
|
use super::*;
|
||||||
|
use core_types::Context;
|
||||||
|
use core_types::list::{Item, List};
|
||||||
|
use core_types::{item, list};
|
||||||
|
use graph_craft::ProtoNodeIdentifier;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::vector::Vector;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_node_sync() {
|
||||||
|
let mut tree = BorrowTree::default();
|
||||||
|
let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).into()), vec![]);
|
||||||
|
let context = TypingContext::default();
|
||||||
|
let future = tree.push_node(NodeId(0), val_1_protonode, &context);
|
||||||
|
futures::executor::block_on(future).unwrap();
|
||||||
|
let _node = tree.get(NodeId(0)).unwrap();
|
||||||
|
let result: Option<Item<u32>> = futures::executor::block_on(tree.eval(NodeId(0), ()));
|
||||||
|
assert_eq!(result.map(|item| *item.element()), Some(2_u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a two-node network feeding the given value into Bounding Box, whose primary input registers both `Item<Vector>` and `List<Vector>` wire variants.
|
||||||
|
fn bounding_box_network(content: TaggedValue) -> ProtoNetwork {
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut bounding_box_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
bounding_box_node.identifier = ProtoNodeIdentifier::new("core_types::vector::BoundingBoxNode");
|
||||||
|
|
||||||
|
ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), bounding_box_node)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_bounding_box_network(content: TaggedValue) -> BorrowTree {
|
||||||
|
let network = bounding_box_network(content);
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("The network should resolve against exactly one registered wire variant");
|
||||||
|
futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The resolved variant's constructor should instantiate")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn item_wire_variant_resolves_and_executes() {
|
||||||
|
let tree = compile_bounding_box_network(TaggedValue::TypeDefault(item!(Vector)));
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context.clone()));
|
||||||
|
assert!(result.is_some(), "The Item wire variant should downcast and execute end-to-end");
|
||||||
|
|
||||||
|
let wrong_type: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert!(wrong_type.is_none(), "An Item wire should not downcast as a List");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn item_wire_promotes_to_list_connector() {
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(item!(f64)).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
// Box Corners takes a `List<f64>` primary, so feeding it an `Item<f64>` wire exercises the singleton raise
|
||||||
|
let mut box_corners_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
box_corners_node.identifier = graphene_std::vector::generator_nodes::box_corners::IDENTIFIER;
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), box_corners_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("An Item wire should resolve a List connector via promotion");
|
||||||
|
assert!(typing_context.promotions(NodeId(1)).is_some(), "The typing pass should record the promotion");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapter should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<graphene_std::vector::misc::BoxCorners>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert!(result.is_some(), "The promoted wire should execute end-to-end");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The layer content path: a rank-0 content wire enters Wrap Graphic's `List` connector by singleton raise, and the
|
||||||
|
// wrapped `Item<Graphic>` raises again at Extend's `List` connector, so layers accept rank-0 chains without new machinery
|
||||||
|
#[test]
|
||||||
|
fn rank_0_content_promotes_through_the_layer_coercion_path() {
|
||||||
|
let content_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(item!(Vector)).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut wrap_graphic_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
wrap_graphic_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::WrapGraphicNode");
|
||||||
|
|
||||||
|
let base_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(2)]);
|
||||||
|
|
||||||
|
let mut extend_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]);
|
||||||
|
extend_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ExtendNode");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![(NodeId(0), content_node), (NodeId(1), wrap_graphic_node), (NodeId(2), base_node), (NodeId(3), extend_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A rank-0 content wire should resolve the layer coercion path via promotion");
|
||||||
|
assert!(typing_context.promotions(NodeId(1)).is_some(), "The rank-0 content should be raised at Wrap Graphic's List connector");
|
||||||
|
assert!(typing_context.promotions(NodeId(3)).is_some(), "The wrapped Item<Graphic> should be raised at Extend's List connector");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapters should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<graphene_std::Graphic>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||||
|
let stack = result.expect("The layer coercion path should execute end-to-end");
|
||||||
|
assert_eq!(stack.len(), 1, "The rank-0 content should contribute exactly one graphic to the stack");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a network feeding the given content plus an f64 distance value into Offset Points, whose distance input is ranked `Item<f64>`.
|
||||||
|
fn offset_points_network(content: TaggedValue) -> ProtoNetwork {
|
||||||
|
let content_node = ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]);
|
||||||
|
let distance_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(10.).into()), vec![NodeId(1)]);
|
||||||
|
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
|
||||||
|
|
||||||
|
let mut offset_points_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
offset_points_node.identifier = ProtoNodeIdentifier::new("core_types::vector::OffsetPointsNode");
|
||||||
|
|
||||||
|
ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![(NodeId(0), content_node), (NodeId(1), distance_node), (NodeId(2), input_adapter_node), (NodeId(3), offset_points_node)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_rank_connectors_resolve_via_promotion() {
|
||||||
|
let network = offset_points_network(TaggedValue::TypeDefault(list!(Vector)));
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context
|
||||||
|
.update(&network)
|
||||||
|
.expect("A List primary with an Item parameter should resolve the mapped variant via promotion");
|
||||||
|
assert!(typing_context.promotions(NodeId(3)).is_some(), "The Item distance should be marked for promotion");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Construction should wrap the promoted argument");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||||
|
assert!(result.is_some(), "The zipped mapped variant should execute end-to-end");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_item_connectors_resolve_without_promotion() {
|
||||||
|
let network = offset_points_network(TaggedValue::TypeDefault(item!(Vector)));
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("All-Item connectors should resolve the rank-0 variant exactly");
|
||||||
|
assert!(typing_context.promotions(NodeId(3)).is_none(), "No promotion should be needed at rank 0");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The rank-0 variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||||
|
assert!(result.is_some(), "The rank-0 variant should execute and stay rank 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a Transform network: content (node 0) plus four parameter values, each promoted onto Item wires as the preprocessor would.
|
||||||
|
fn transform_network(content: TaggedValue, rotation: TaggedValue) -> ProtoNetwork {
|
||||||
|
let mut nodes = vec![(NodeId(0), ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]))];
|
||||||
|
|
||||||
|
let parameters = [
|
||||||
|
(TaggedValue::DVec2(glam::DVec2::new(5., 0.)), "DVec2"),
|
||||||
|
(rotation, "f64"),
|
||||||
|
(TaggedValue::DVec2(glam::DVec2::ONE), "DVec2"),
|
||||||
|
(TaggedValue::DVec2(glam::DVec2::ZERO), "DVec2"),
|
||||||
|
];
|
||||||
|
let mut transform_inputs = vec![NodeId(0)];
|
||||||
|
let mut next_id = 1;
|
||||||
|
for (value, element) in parameters {
|
||||||
|
let value_id = NodeId(next_id);
|
||||||
|
let input_adapter_id = NodeId(next_id + 1);
|
||||||
|
next_id += 2;
|
||||||
|
|
||||||
|
nodes.push((value_id, ProtoNode::value(ConstructionArgs::Value(value.into()), vec![value_id])));
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![value_id]), vec![input_adapter_id]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::with_owned_string(format!("input_adapter<{element}>"));
|
||||||
|
nodes.push((input_adapter_id, input_adapter_node));
|
||||||
|
transform_inputs.push(input_adapter_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = NodeId(next_id);
|
||||||
|
let mut transform_node = ProtoNode::value(ConstructionArgs::Nodes(transform_inputs), vec![output]);
|
||||||
|
transform_node.identifier = graphene_std::transform_nodes::transform::IDENTIFIER;
|
||||||
|
nodes.push((output, transform_node));
|
||||||
|
|
||||||
|
ProtoNetwork { inputs: vec![], output, nodes }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_composes_onto_item_wire() {
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
|
let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64(0.));
|
||||||
|
let output = network.output;
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("Transform should resolve its rank-0 variant");
|
||||||
|
assert!(typing_context.promotions(output).is_none(), "All-Item connectors should need no promotion");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Transform's rank-0 variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(output, context));
|
||||||
|
let item = result.expect("A rank-0 chain through Transform should stay rank 0");
|
||||||
|
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM);
|
||||||
|
assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_broadcasts_item_content_across_a_framed_parameter() {
|
||||||
|
use glam::DAffine2;
|
||||||
|
|
||||||
|
let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64Array(vec![0., 90.]));
|
||||||
|
let output = network.output;
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context
|
||||||
|
.update(&network)
|
||||||
|
.expect("A framed rotation should resolve the mapped variant via promotion of the other connectors");
|
||||||
|
assert!(typing_context.promotions(output).is_some(), "The Item-typed connectors should be raised into the frame");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(output, context));
|
||||||
|
let list = result.expect("The broadcast should produce a List");
|
||||||
|
assert_eq!(list.len(), 2, "One output item per frame slot");
|
||||||
|
|
||||||
|
let first: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 0);
|
||||||
|
let second: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 1);
|
||||||
|
assert!((first.matrix2.col(0).y - 0.).abs() < 1e-10, "Slot 0 should be unrotated");
|
||||||
|
assert!((second.matrix2.col(0).y - 1.).abs() < 1e-10, "Slot 1 should be rotated 90 degrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generator_frames_over_a_list_parameter() {
|
||||||
|
// A `()` generator (Circle) fed a `List<f64>` radius should frame into one circle per slot
|
||||||
|
let primary = ProtoNode::value(ConstructionArgs::Value(TaggedValue::None.into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let radii = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![10., 20., 30.]).into()), vec![NodeId(1)]);
|
||||||
|
let mut radius_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||||
|
radius_adapter.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
|
||||||
|
|
||||||
|
let mut circle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
circle_node.identifier = graphene_std::vector_nodes::circle::IDENTIFIER;
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![(NodeId(0), primary), (NodeId(1), radii), (NodeId(2), radius_adapter), (NodeId(3), circle_node)],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A List<f64> radius should resolve Circle's mapped generator variant");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped generator variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||||
|
let list = result.expect("The generator frame should produce a List<Vector>");
|
||||||
|
assert_eq!(list.len(), 3, "One circle per radius slot");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the compiler's cache chain (child, then Memoize, then Context Modification) around a value, as `insert_context_nullification_node` does.
|
||||||
|
fn nullification_chain_network(value: TaggedValue) -> ProtoNetwork {
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(value.into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut memoize_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
memoize_node.identifier = graphene_core::memo::memoize::IDENTIFIER;
|
||||||
|
|
||||||
|
let features_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextFeatures(Default::default()).into()), vec![NodeId(2)]);
|
||||||
|
|
||||||
|
let mut nullification_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
nullification_node.identifier = graphene_core::context_modification::context_modification::IDENTIFIER;
|
||||||
|
|
||||||
|
ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), memoize_node), (NodeId(2), features_node), (NodeId(3), nullification_node)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nullification_chain_resolves_for_ranked_enum_wires() {
|
||||||
|
use graphene_std::vector::style::StrokeAlign;
|
||||||
|
|
||||||
|
// The Item form, as a wrapped input adapter's output presents to the chain
|
||||||
|
let network = nullification_chain_network(TaggedValue::TypeDefault(item!(StrokeAlign)));
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("An Item<StrokeAlign> wire should resolve through the compiler's cache chain");
|
||||||
|
|
||||||
|
// The List form, as a whole-list enum wire presents to the chain
|
||||||
|
let network = nullification_chain_network(TaggedValue::TypeDefault(list!(StrokeAlign)));
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A List<StrokeAlign> wire should resolve through the compiler's cache chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_wires_materialize_as_items_at_resolution() {
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
|
let values = [
|
||||||
|
TaggedValue::DAffine2(DAffine2::IDENTITY),
|
||||||
|
TaggedValue::DVec2(DVec2::new(7., 0.)),
|
||||||
|
TaggedValue::F64(0.),
|
||||||
|
TaggedValue::DVec2(DVec2::ONE),
|
||||||
|
TaggedValue::DVec2(DVec2::ZERO),
|
||||||
|
];
|
||||||
|
let mut nodes: Vec<_> = values
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| (NodeId(index as u64), ProtoNode::value(ConstructionArgs::Value(value.into()), vec![NodeId(index as u64)])))
|
||||||
|
.collect();
|
||||||
|
let mut transform_node = ProtoNode::value(ConstructionArgs::Nodes((0..5).map(NodeId).collect()), vec![NodeId(5)]);
|
||||||
|
transform_node.identifier = graphene_std::transform_nodes::transform::IDENTIFIER;
|
||||||
|
nodes.push((NodeId(5), transform_node));
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(5),
|
||||||
|
nodes,
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("Value wires should materialize as Items and resolve the all-Item variant");
|
||||||
|
assert!(typing_context.promotions(NodeId(5)).is_none(), "Already-Item value wires should need no promotion");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The all-Item variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<DAffine2>> = futures::executor::block_on(tree.eval(NodeId(5), context));
|
||||||
|
let item = result.expect("A value matrix should flow through Transform as an Item");
|
||||||
|
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM);
|
||||||
|
assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A position's Item wire converts through the vector input adapter into a single-anchor path, which the ItemToList promotion can then raise at a List connector
|
||||||
|
#[test]
|
||||||
|
fn position_value_converts_through_the_vector_input_adapter() {
|
||||||
|
let position_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::DVec2(glam::DVec2::new(3., 4.)).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Vector>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), position_node), (NodeId(1), input_adapter_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("An Item<DVec2> wire should resolve the adapter's element conversion row");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The conversion constructor should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert!(result.is_some(), "The position should arrive as an Item<Vector> single-anchor path");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A `List` wire feeding a `ListDyn` connector erases its element type through the input adapter's `Into` row
|
||||||
|
#[test]
|
||||||
|
fn list_wire_erases_through_the_list_dyn_input_adapter() {
|
||||||
|
use core_types::list::ListDyn;
|
||||||
|
|
||||||
|
let list_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2., 3.]).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<ListDyn>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), list_node), (NodeId(1), input_adapter_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A List<f64> wire should resolve the ListDyn erasure row");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The erasure constructor should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<ListDyn> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
let erased = result.expect("The erased list should arrive as a ListDyn");
|
||||||
|
assert_eq!(erased.len(), 3, "The erased list should keep its row count");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_wire_passes_through_the_input_adapter_as_item() {
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(3.).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), input_adapter_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("An f64 value's Item wire should resolve the adapter's passthrough row");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<f64>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert_eq!(result.map(|item| *item.element()), Some(3.), "The value should arrive as an Item");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path Modify's ranked modification parameter: a `Box<VectorModification>` value rides the `Item` wire through its input adapter,
|
||||||
|
// exercising the nested-generic identifier round-trip between the registered `stringify!` name and the preprocessor's simplified name
|
||||||
|
#[test]
|
||||||
|
fn modification_value_rides_the_item_wire_through_its_input_adapter() {
|
||||||
|
use graphene_std::vector::VectorModification;
|
||||||
|
|
||||||
|
let modification = TaggedValue::VectorModification(Default::default());
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(modification.into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Box<VectorModification>>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), input_adapter_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A modification value's Item wire should resolve the adapter's passthrough row");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<Box<VectorModification>>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert!(result.is_some(), "The modification should arrive as an Item");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Write Attribute value input: a value's Item wire boxes its element into a type-erased attribute value through the input adapter
|
||||||
|
#[test]
|
||||||
|
fn item_wire_boxes_into_the_attribute_value_connector() {
|
||||||
|
use graphene_std::list::AttributeValueDyn;
|
||||||
|
|
||||||
|
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(3.).into()), vec![NodeId(0)]);
|
||||||
|
let mut attribute_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
attribute_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<AttributeValueDyn>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(1),
|
||||||
|
nodes: vec![(NodeId(0), value_node), (NodeId(1), attribute_adapter_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("An Item<f64> wire should resolve the attribute value boxing row");
|
||||||
|
assert!(typing_context.promotions(NodeId(1)).is_none(), "The already-Item value wire should need no promotion");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The boxing constructor should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<Item<AttributeValueDyn>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
let boxed = result.expect("The boxed attribute value should arrive as an Item");
|
||||||
|
assert_eq!(
|
||||||
|
boxed.element().0.as_any().downcast_ref::<f64>(),
|
||||||
|
Some(&3.),
|
||||||
|
"The stored value should be the bare element, not the whole Item"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_wire_variant_resolves_and_executes() {
|
||||||
|
let tree = compile_bounding_box_network(TaggedValue::TypeDefault(list!(Vector)));
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context.clone()));
|
||||||
|
assert!(result.is_some(), "The mapped List wire variant should downcast and execute end-to-end");
|
||||||
|
|
||||||
|
let wrong_type: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||||
|
assert!(wrong_type.is_none(), "A List wire should not downcast as an Item");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expander_flattens_under_the_frame() {
|
||||||
|
// A string value's Item wire feeds String Split's expander primary; its parameters ride Item wires through their input adapters
|
||||||
|
let string_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("a,b".into()).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let delimiter_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(",".into()).into()), vec![NodeId(1)]);
|
||||||
|
let mut delimiter_input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||||
|
delimiter_input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<String>");
|
||||||
|
|
||||||
|
let escaping_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(false).into()), vec![NodeId(3)]);
|
||||||
|
let mut escaping_input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
|
||||||
|
escaping_input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<bool>");
|
||||||
|
|
||||||
|
let output = NodeId(5);
|
||||||
|
let mut string_split_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2), NodeId(4)]), vec![output]);
|
||||||
|
string_split_node.identifier = graphene_std::text_nodes::string_split::IDENTIFIER;
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output,
|
||||||
|
nodes: vec![
|
||||||
|
(NodeId(0), string_node),
|
||||||
|
(NodeId(1), delimiter_node),
|
||||||
|
(NodeId(2), delimiter_input_adapter_node),
|
||||||
|
(NodeId(3), escaping_node),
|
||||||
|
(NodeId(4), escaping_input_adapter_node),
|
||||||
|
(output, string_split_node),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context
|
||||||
|
.update(&network)
|
||||||
|
.expect("All-Item connectors should resolve the expander's direct `Item -> List` variant");
|
||||||
|
assert!(typing_context.promotions(output).is_none(), "No promotion should be needed when every connector is already an Item");
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The expander variant should instantiate");
|
||||||
|
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<String>> = futures::executor::block_on(tree.eval(output, context));
|
||||||
|
let list = result.expect("An Item-wired expander should produce a List");
|
||||||
|
assert_eq!(list.len(), 2, "Splitting \"a,b\" on the comma should expand into two rows");
|
||||||
|
let substrings: Vec<_> = list.iter_element_values().map(|s| s.as_str()).collect();
|
||||||
|
assert_eq!(substrings, ["a", "b"], "The rows should hold the split substrings");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whole_list_switches_as_one_bundle() {
|
||||||
|
// One bool selecting between two whole `List<f64>` stacks: each branch bundles into a rank-0 cell, and the result unbundles back to the flat stack
|
||||||
|
let condition_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(0)]);
|
||||||
|
let if_true_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2., 3.]).into()), vec![NodeId(1)]);
|
||||||
|
let if_false_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![4., 5.]).into()), vec![NodeId(2)]);
|
||||||
|
|
||||||
|
let mut switch_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
switch_node.identifier = ProtoNodeIdentifier::new("math_nodes::SwitchNode");
|
||||||
|
|
||||||
|
let mut unbundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
|
||||||
|
unbundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::UnbundleNode<f64>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(4),
|
||||||
|
nodes: vec![
|
||||||
|
(NodeId(0), condition_node),
|
||||||
|
(NodeId(1), if_true_node),
|
||||||
|
(NodeId(2), if_false_node),
|
||||||
|
(NodeId(3), switch_node),
|
||||||
|
(NodeId(4), unbundle_node),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context
|
||||||
|
.update(&network)
|
||||||
|
.expect("A List<f64> branch should resolve the Item<Bundle<f64>> row via the bundle wrap");
|
||||||
|
|
||||||
|
let promotions = typing_context.promotions(NodeId(3)).expect("The condition wrap and both branch bundles should be recorded");
|
||||||
|
let branch_bundles = promotions
|
||||||
|
.iter()
|
||||||
|
.filter(|(index, adapter)| *index != 0 && matches!(adapter, graph_craft::proto::Promotion::Bundle(_)))
|
||||||
|
.count();
|
||||||
|
assert_eq!(branch_bundles, 2, "Both branches should bundle their whole list into one opaque cell");
|
||||||
|
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate");
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<f64>> = futures::executor::block_on(tree.eval(NodeId(4), context));
|
||||||
|
let list = result.expect("The whole stack should round-trip through the bundle switch back to a flat List<f64>");
|
||||||
|
let values: Vec<f64> = list.iter_element_values().copied().collect();
|
||||||
|
assert_eq!(values, [1., 2., 3.], "The taken branch's whole list should come through unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bundle_unbundles_into_a_list_connector() {
|
||||||
|
// A bundled wire (sourced here from a BundleNode, as a Switch branch produces one) feeding Extend's whole-`List` base connector
|
||||||
|
let stack_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(0)]);
|
||||||
|
|
||||||
|
let mut bundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||||
|
bundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::BundleNode<Graphic>");
|
||||||
|
|
||||||
|
let new_layers_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(2)]);
|
||||||
|
|
||||||
|
let mut extend_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
extend_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ExtendNode");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![(NodeId(0), stack_node), (NodeId(1), bundle_node), (NodeId(2), new_layers_node), (NodeId(3), extend_node)],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context.update(&network).expect("A bundled wire should feed Extend's List<Graphic> connector via the unbundle");
|
||||||
|
|
||||||
|
let promotions = typing_context.promotions(NodeId(3)).expect("Extend's bundled base should be marked for unbundling");
|
||||||
|
assert!(
|
||||||
|
promotions.iter().any(|(index, adapter)| *index == 0 && matches!(adapter, graph_craft::proto::Promotion::Unbundle(_))),
|
||||||
|
"The base connector should unbundle the whole list"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The unbundle adapter should instantiate");
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<graphene_std::Graphic>> = futures::executor::block_on(tree.eval(NodeId(3), context));
|
||||||
|
assert!(result.is_some(), "The unbundled stack should flow into Extend as a List<Graphic>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_whole_list_of_scalars_switches_as_one_bundle() {
|
||||||
|
// A single bool selecting between two whole `List<f64>` values, covering a primitive element type and confirming the selected list survives intact
|
||||||
|
let condition_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(0)]);
|
||||||
|
let if_true_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2.]).into()), vec![NodeId(1)]);
|
||||||
|
let if_false_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![3., 4., 5.]).into()), vec![NodeId(2)]);
|
||||||
|
|
||||||
|
let mut switch_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1), NodeId(2)]), vec![NodeId(3)]);
|
||||||
|
switch_node.identifier = ProtoNodeIdentifier::new("math_nodes::SwitchNode");
|
||||||
|
|
||||||
|
let mut unbundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
|
||||||
|
unbundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::UnbundleNode<f64>");
|
||||||
|
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(4),
|
||||||
|
nodes: vec![
|
||||||
|
(NodeId(0), condition_node),
|
||||||
|
(NodeId(1), if_true_node),
|
||||||
|
(NodeId(2), if_false_node),
|
||||||
|
(NodeId(3), switch_node),
|
||||||
|
(NodeId(4), unbundle_node),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||||
|
typing_context
|
||||||
|
.update(&network)
|
||||||
|
.expect("A List<f64> branch should resolve the Item<Bundle<f64>> row via the bundle wrap");
|
||||||
|
|
||||||
|
let promotions = typing_context.promotions(NodeId(3)).expect("The condition wrap and both branch bundles should be recorded");
|
||||||
|
let branch_bundles = promotions
|
||||||
|
.iter()
|
||||||
|
.filter(|(index, adapter)| *index != 0 && matches!(adapter, graph_craft::proto::Promotion::Bundle(_)))
|
||||||
|
.count();
|
||||||
|
assert_eq!(branch_bundles, 2, "Both scalar-list branches should bundle into one opaque cell");
|
||||||
|
|
||||||
|
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate");
|
||||||
|
let context: Context = None;
|
||||||
|
let result: Option<List<f64>> = futures::executor::block_on(tree.eval(NodeId(4), context));
|
||||||
|
let list = result.expect("The whole scalar list should round-trip through the bundle switch");
|
||||||
|
assert_eq!(list.len(), 2, "The true branch's whole list should be selected and preserved intact");
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::application_io::PlatformEditorApi;
|
use graph_craft::application_io::PlatformEditorApi;
|
||||||
use graph_craft::document::DocumentNode;
|
use graph_craft::application_io::resource::Resource;
|
||||||
use graph_craft::document::value::RenderOutput;
|
use graph_craft::document::value::RenderOutput;
|
||||||
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||||
use graphene_std::any::DynAnyNode;
|
use graphene_std::any::DynAnyNode;
|
||||||
use graphene_std::application_io::Texture;
|
use graphene_std::brush::brush_stroke::BrushTrace;
|
||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::gradient::GradientStops;
|
use graphene_std::gradient::Gradient;
|
||||||
use graphene_std::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
|
use graphene_std::list::{AttributeValueDyn, Bundle, Item, List, ListDyn, NodeIdPath};
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use graphene_std::platform_application_io::canvas_utils::CanvasHandle;
|
use graphene_std::platform_application_io::canvas_utils::CanvasHandle;
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
@@ -17,103 +17,21 @@ use graphene_std::raster::color::Color;
|
|||||||
use graphene_std::raster::*;
|
use graphene_std::raster::*;
|
||||||
use graphene_std::raster::{CPU, Raster};
|
use graphene_std::raster::{CPU, Raster};
|
||||||
use graphene_std::render_node::RenderIntermediate;
|
use graphene_std::render_node::RenderIntermediate;
|
||||||
use graphene_std::transform::Footprint;
|
use graphene_std::text_nodes::StringCapitalization;
|
||||||
use graphene_std::uuid::NodeId;
|
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType};
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::misc::{BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns};
|
||||||
|
use graphene_std::vector::style::{DashPattern, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
use graphene_std::vector::{Vector, VectorModification};
|
||||||
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
||||||
use node_registry_macros::{async_node, convert_node, into_node};
|
use node_registry_macros::async_node;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
use wgpu_executor::WgpuExecutor;
|
|
||||||
|
|
||||||
// TODO: turn into hashmap
|
// TODO: turn into hashmap
|
||||||
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
|
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
|
||||||
let mut node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
let mut node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
// ==========
|
|
||||||
// INTO 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>),
|
|
||||||
// Type-erased attribute conversions for the `Attach Attribute` node, so it monomorphizes only over the destination `List` type.
|
|
||||||
convert_node!(from: List<Artboard>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<Graphic>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<Vector>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<Color>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<GradientStops>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<f64>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<bool>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<String>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<DAffine2>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<BlendMode>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<graphene_std::vector::style::GradientType>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<graphene_std::vector::style::GradientSpreadMethod>, to: AttributeDyn),
|
|
||||||
convert_node!(from: List<Artboard>, to: ListDyn),
|
|
||||||
convert_node!(from: List<Graphic>, to: ListDyn),
|
|
||||||
convert_node!(from: List<Vector>, to: ListDyn),
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: ListDyn),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: ListDyn),
|
|
||||||
convert_node!(from: List<Color>, to: ListDyn),
|
|
||||||
convert_node!(from: List<GradientStops>, to: ListDyn),
|
|
||||||
convert_node!(from: List<f64>, to: ListDyn),
|
|
||||||
convert_node!(from: List<bool>, to: ListDyn),
|
|
||||||
convert_node!(from: List<String>, to: ListDyn),
|
|
||||||
convert_node!(from: List<u8>, to: ListDyn),
|
|
||||||
convert_node!(from: List<NodeId>, to: ListDyn),
|
|
||||||
convert_node!(from: List<DAffine2>, to: ListDyn),
|
|
||||||
convert_node!(from: List<BlendMode>, to: ListDyn),
|
|
||||||
convert_node!(from: List<graphene_std::vector::style::GradientType>, to: ListDyn),
|
|
||||||
convert_node!(from: List<graphene_std::vector::style::GradientSpreadMethod>, to: ListDyn),
|
|
||||||
// Type-erased attribute value conversions for the `Write Attribute` node, so it monomorphizes only over the destination `List` type.
|
|
||||||
convert_node!(from: f64, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: u32, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: u64, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: bool, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: String, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: DVec2, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: DAffine2, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: Color, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: BlendMode, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: graphene_std::vector::style::GradientType, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: graphene_std::vector::style::GradientSpreadMethod, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<String>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<NodeId>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<Color>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<GradientStops>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<Vector>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: AttributeValueDyn),
|
|
||||||
convert_node!(from: List<Graphic>, to: AttributeValueDyn),
|
|
||||||
// into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
into_node!(from: &PlatformEditorApi, to: &WgpuExecutor),
|
|
||||||
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: bool, to: String),
|
|
||||||
convert_node!(from: DVec2, to: String),
|
|
||||||
convert_node!(from: IVec2, to: String),
|
|
||||||
convert_node!(from: DAffine2, to: String),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>, converter: &WgpuExecutor),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<CPU>>, to: List<Raster<GPU>>, converter: &WgpuExecutor),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>, converter: &WgpuExecutor),
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
convert_node!(from: List<Raster<GPU>>, to: List<Raster<CPU>>, converter: &WgpuExecutor),
|
|
||||||
// =============
|
// =============
|
||||||
// MONITOR NODES
|
// MONITOR NODES
|
||||||
// =============
|
// =============
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Artboard>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Artboard>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Graphic>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Graphic>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Vector>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Vector>]),
|
||||||
@@ -121,207 +39,547 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Color>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Color>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientStops>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Gradient>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Image<Color>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Artboard>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Graphic>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Vector>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Raster<CPU>>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]),
|
#[cfg(feature = "gpu")]
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Raster<GPU>>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Color>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Gradient>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<DashPattern>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BoxCorners>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<String>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Texture]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<f64>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<f32>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<DAffine2>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<Footprint>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<DVec2>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<bool>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<u32>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<u64>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BlendMode>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<String>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<String>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<NodeId>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<NodeIdPath>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<f64>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<f64>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<u8>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<f32>]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<u32>]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<u64>]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<DVec2>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<bool>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<bool>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<DAffine2>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<DAffine2>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientType>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientType>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientSpreadMethod>]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<GradientSpreadMethod>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeDyn]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeValueDyn]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DocumentNode]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
|
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
|
|
||||||
// Context nullification
|
// Context nullification
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<RenderIntermediate>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<RenderOutput>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]),
|
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<CanvasHandle>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<Option<&wgpu_executor::WgpuExecutor>>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextFeatures]),
|
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<wgpu_executor::WgpuPipelineCache>, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
// ==========
|
// ==========
|
||||||
// MEMO NODES
|
// MEMO NODES
|
||||||
// ==========
|
// ==========
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Artboard>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Artboard>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Graphic>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Graphic>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Vector>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Vector>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Color>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Color>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Gradient>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientStops>]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<String>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<String>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<NodeId>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<NodeIdPath>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<f64>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<f64>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<u8>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<DVec2>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<bool>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<bool>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<DAffine2>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<DAffine2>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientType>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientType>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientSpreadMethod>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<GradientSpreadMethod>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeDyn]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Artboard>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Graphic>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Vector>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Raster<CPU>>]),
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Raster<GPU>>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Color>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Gradient>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<DashPattern>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<BoxCorners>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<String>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<f64>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<f32>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<DAffine2>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Footprint>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<DVec2>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<bool>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<u32>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<u64>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<BlendMode>]),
|
||||||
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ListDyn]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ListDyn]),
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<CanvasHandle>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f64]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<RenderOutput>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f32]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u32]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u64]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DVec2]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => String]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DAffine2]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]),
|
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<Color>]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<RenderIntermediate>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Graphic]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Option<&wgpu_executor::WgpuExecutor>>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),
|
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<wgpu_executor::WgpuPipelineCache>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::NoiseType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::FractalType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularDistanceFunction]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularReturnType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::DomainWarpType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RelativeAbsolute]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>]),
|
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]),
|
|
||||||
];
|
];
|
||||||
// =============
|
// The per-connector input adapter, registered per element type: an `Item` or `List` wire passes through unchanged.
|
||||||
// CONVERT NODES
|
// The `name` arm registers an `Into`-based whole-wire shift under the given identifier, serving the `ListDyn` erasure rows.
|
||||||
// =============
|
macro_rules! input_adapter_node {
|
||||||
node_types.extend(
|
(element: $element:ty) => {{
|
||||||
[
|
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
convert_node!(from: f32, to: numbers),
|
input_adapter_node!(passthrough: Item<$element>, element: $element),
|
||||||
convert_node!(from: f64, to: numbers),
|
input_adapter_node!(passthrough: List<$element>, element: $element),
|
||||||
convert_node!(from: i8, to: numbers),
|
];
|
||||||
convert_node!(from: u8, to: numbers),
|
entries
|
||||||
convert_node!(from: u16, to: numbers),
|
}};
|
||||||
convert_node!(from: i16, to: numbers),
|
(passthrough: $ty:ty, element: $element:ty) => {
|
||||||
convert_node!(from: i32, to: numbers),
|
(
|
||||||
convert_node!(from: u32, to: numbers),
|
ProtoNodeIdentifier::new(concat!["input_adapter<", stringify!($element), ">"]),
|
||||||
convert_node!(from: i64, to: numbers),
|
|mut args| {
|
||||||
convert_node!(from: u64, to: numbers),
|
Box::pin(async move {
|
||||||
convert_node!(from: i128, to: numbers),
|
let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::downcast_node::<Context, $ty>(args.pop().unwrap()));
|
||||||
convert_node!(from: u128, to: numbers),
|
let any: DynAnyNode<Context, $ty, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
convert_node!(from: isize, to: numbers),
|
Box::new(any) as TypeErasedBox
|
||||||
convert_node!(from: usize, to: numbers),
|
})
|
||||||
convert_node!(from: numbers, to: DVec2),
|
},
|
||||||
convert_node!(from: numbers, to: String),
|
{
|
||||||
]
|
let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $ty> + Send>>>::new());
|
||||||
.into_iter()
|
let params = vec![fn_type_fut!(Context, $ty)];
|
||||||
.flatten(),
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
);
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(name: $name:literal, from: $from:ty, to: $to:ty, element: $element:ty) => {
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new(concat![$name, "<", stringify!($element), ">"]),
|
||||||
|
|mut args| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let node = graphene_core::ops::IntoNode::new(
|
||||||
|
graphene_std::any::downcast_node::<Context, $from>(args.pop().unwrap()),
|
||||||
|
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)),
|
||||||
|
);
|
||||||
|
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
|
Box::new(any) as TypeErasedBox
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let node = graphene_core::ops::IntoNode::new(
|
||||||
|
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
|
||||||
|
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)),
|
||||||
|
);
|
||||||
|
let params = vec![fn_type_fut!(Context, $from)];
|
||||||
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// A conversion row registered under the same `input_adapter<$element>` identifier so a convertible-but-not-identical
|
||||||
|
// ranked wire can feed an `Item<$element>` connector, converting each element via `Into`
|
||||||
|
macro_rules! input_adapter_row {
|
||||||
|
(from_element: $from:ty, element: $element:ty) => {{
|
||||||
|
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
input_adapter_row!(node: IntoItemNode, from: Item<$from>, to: Item<$element>, element: $element),
|
||||||
|
input_adapter_row!(node: IntoListNode, from: List<$from>, to: List<$element>, element: $element),
|
||||||
|
];
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
(node: $node:ident, from: $from:ty, to: $to:ty, element: $element:ty) => {
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new(concat!["input_adapter<", stringify!($element), ">"]),
|
||||||
|
|mut args| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let node = graphene_core::ops::$node::new(
|
||||||
|
graphene_std::any::downcast_node::<Context, $from>(args.pop().unwrap()),
|
||||||
|
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)),
|
||||||
|
);
|
||||||
|
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
|
Box::new(any) as TypeErasedBox
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let node = graphene_core::ops::$node::new(
|
||||||
|
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
|
||||||
|
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)),
|
||||||
|
);
|
||||||
|
let params = vec![fn_type_fut!(Context, $from)];
|
||||||
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// The promotion adapter inserted by type resolution when an Item wire feeds a List connector: the singleton raise
|
||||||
|
macro_rules! item_to_list_node {
|
||||||
|
(element: $element:ty) => {
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ItemToListNode<", stringify!($element), ">"]),
|
||||||
|
|mut args| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::downcast_node::<Context, Item<$element>>(args.pop().unwrap()));
|
||||||
|
let any: DynAnyNode<Context, List<$element>, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
|
Box::new(any) as TypeErasedBox
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::PanicNode::<
|
||||||
|
Context,
|
||||||
|
core::pin::Pin<Box<dyn core::future::Future<Output = Item<$element>> + Send>>,
|
||||||
|
>::new());
|
||||||
|
let params = vec![fn_type_fut!(Context, Item<$element>)];
|
||||||
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// A whole-List bundle adapter inserted by type resolution when a List wire feeds an Item<Bundle<X>> connector
|
||||||
|
macro_rules! bundle_node {
|
||||||
|
(element: $element:ty) => {
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new(concat!["graphene_core::ops::BundleNode<", stringify!($element), ">"]),
|
||||||
|
|mut args| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let node = graphene_core::ops::BundleNode::new(graphene_std::any::downcast_node::<Context, List<$element>>(args.pop().unwrap()));
|
||||||
|
let any: DynAnyNode<Context, Item<Bundle<$element>>, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
|
Box::new(any) as TypeErasedBox
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let node = graphene_core::ops::BundleNode::new(graphene_std::any::PanicNode::<
|
||||||
|
Context,
|
||||||
|
core::pin::Pin<Box<dyn core::future::Future<Output = List<$element>> + Send>>,
|
||||||
|
>::new());
|
||||||
|
let params = vec![fn_type_fut!(Context, List<$element>)];
|
||||||
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// The reverse unbundle adapter inserted by type resolution when an Item<Bundle<X>> wire feeds a List connector
|
||||||
|
macro_rules! unbundle_node {
|
||||||
|
(element: $element:ty) => {
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new(concat!["graphene_core::ops::UnbundleNode<", stringify!($element), ">"]),
|
||||||
|
|mut args| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::downcast_node::<Context, Item<Bundle<$element>>>(args.pop().unwrap()));
|
||||||
|
let any: DynAnyNode<Context, List<$element>, _> = graphene_std::any::DynAnyNode::new(node);
|
||||||
|
Box::new(any) as TypeErasedBox
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::PanicNode::<
|
||||||
|
Context,
|
||||||
|
core::pin::Pin<Box<dyn core::future::Future<Output = Item<Bundle<$element>>> + Send>>,
|
||||||
|
>::new());
|
||||||
|
let params = vec![fn_type_fut!(Context, Item<Bundle<$element>>)];
|
||||||
|
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
||||||
|
node_io
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// ==================
|
||||||
|
// RANK ADAPTER NODES
|
||||||
|
// ==================
|
||||||
|
// Registers the rank adapters (input_adapter, ItemToListNode) for each element type
|
||||||
|
macro_rules! rank_adapter_nodes {
|
||||||
|
($($element:ty),* $(,)?) => {{
|
||||||
|
let mut entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = Vec::new();
|
||||||
|
$(
|
||||||
|
entries.extend(input_adapter_node!(element: $element));
|
||||||
|
entries.push(item_to_list_node!(element: $element));
|
||||||
|
)*
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
node_types.extend(rank_adapter_nodes!(
|
||||||
|
Vector,
|
||||||
|
Raster<CPU>,
|
||||||
|
Graphic,
|
||||||
|
Color,
|
||||||
|
Gradient,
|
||||||
|
String,
|
||||||
|
f64,
|
||||||
|
f32,
|
||||||
|
u64,
|
||||||
|
DVec2,
|
||||||
|
DAffine2,
|
||||||
|
bool,
|
||||||
|
u32,
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
|
BlendMode,
|
||||||
|
MergeByDistanceAlgorithm,
|
||||||
|
ExtrudeJoiningAlgorithm,
|
||||||
|
StrokeJoin,
|
||||||
|
StrokeAlign,
|
||||||
|
StrokeCap,
|
||||||
|
PaintOrder,
|
||||||
|
PointSpacingType,
|
||||||
|
StringCapitalization,
|
||||||
|
GradientType,
|
||||||
|
GradientSpreadMethod,
|
||||||
|
LuminanceCalculation,
|
||||||
|
RedGreenBlue,
|
||||||
|
RedGreenBlueAlpha,
|
||||||
|
RelativeAbsolute,
|
||||||
|
SelectiveColorChoice,
|
||||||
|
DashPattern,
|
||||||
|
BoxCorners,
|
||||||
|
NodeIdPath,
|
||||||
|
BrushTrace,
|
||||||
|
Box<VectorModification>,
|
||||||
|
XY,
|
||||||
|
ScaleType,
|
||||||
|
Footprint,
|
||||||
|
ReferencePoint,
|
||||||
|
CentroidType,
|
||||||
|
BooleanOperation,
|
||||||
|
InterpolationDistribution,
|
||||||
|
RowsOrColumns,
|
||||||
|
Artboard,
|
||||||
|
Resource,
|
||||||
|
));
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
node_types.extend(rank_adapter_nodes!(Raster<GPU>));
|
||||||
|
// Type-erased rows for the `ListDyn` connectors (`Read Attribute`, `List Length`): any `List` wire erases its element type
|
||||||
|
macro_rules! list_dyn_rows {
|
||||||
|
($($element:ty),* $(,)?) => {{
|
||||||
|
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
$(input_adapter_node!(name: "input_adapter", from: List<$element>, to: ListDyn, element: ListDyn),)*
|
||||||
|
];
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
node_types.push(input_adapter_node!(passthrough: ListDyn, element: ListDyn));
|
||||||
|
node_types.extend(list_dyn_rows!(
|
||||||
|
Artboard,
|
||||||
|
Graphic,
|
||||||
|
Vector,
|
||||||
|
Raster<CPU>,
|
||||||
|
Color,
|
||||||
|
Gradient,
|
||||||
|
f64,
|
||||||
|
bool,
|
||||||
|
String,
|
||||||
|
DVec2,
|
||||||
|
DAffine2,
|
||||||
|
BlendMode,
|
||||||
|
GradientType,
|
||||||
|
GradientSpreadMethod
|
||||||
|
));
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
node_types.extend(list_dyn_rows!(Raster<GPU>));
|
||||||
|
// Registers the whole-List bundle adapters for each value type whose entire list may be selected or carried as one opaque cell
|
||||||
|
macro_rules! bundle_adapter_nodes {
|
||||||
|
($($element:ty),* $(,)?) => {{
|
||||||
|
let mut entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = Vec::new();
|
||||||
|
$(
|
||||||
|
entries.push(bundle_node!(element: $element));
|
||||||
|
entries.push(unbundle_node!(element: $element));
|
||||||
|
)*
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
node_types.extend(bundle_adapter_nodes!(
|
||||||
|
bool,
|
||||||
|
f32,
|
||||||
|
f64,
|
||||||
|
u32,
|
||||||
|
u64,
|
||||||
|
DVec2,
|
||||||
|
DAffine2,
|
||||||
|
Graphic,
|
||||||
|
Raster<CPU>,
|
||||||
|
Vector,
|
||||||
|
String,
|
||||||
|
Color,
|
||||||
|
Gradient,
|
||||||
|
Artboard,
|
||||||
|
));
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
node_types.extend(bundle_adapter_nodes!(Raster<GPU>));
|
||||||
|
// The compiler wraps a ranked connector's broadcast sibling in a Memoize plus context nullification pair, and the editor's test
|
||||||
|
// instrumentation monitors any wire, so all three must pass through every ranked element type the hand lists above omit.
|
||||||
|
// The identifiers are passed explicitly because `stringify!` mangles whitespace when the path tokens come through this nested macro layer.
|
||||||
|
macro_rules! cache_chain_nodes {
|
||||||
|
(each: $value:ty) => {{
|
||||||
|
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
async_node!(identifier: graphene_core::memo::memoize::IDENTIFIER, graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => $value]),
|
||||||
|
async_node!(identifier: graphene_core::memo::monitor::IDENTIFIER, graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => $value]),
|
||||||
|
async_node!(identifier: graphene_core::context_modification::context_modification::IDENTIFIER, graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => $value, Context => Item<graphene_std::ContextFeatures>]),
|
||||||
|
];
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
($($element:ty),* $(,)?) => {{
|
||||||
|
let mut entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = Vec::new();
|
||||||
|
$(
|
||||||
|
entries.extend(cache_chain_nodes!(each: Item<$element>));
|
||||||
|
entries.extend(cache_chain_nodes!(each: List<$element>));
|
||||||
|
)*
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
node_types.extend(cache_chain_nodes!(
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
|
BlendMode,
|
||||||
|
StrokeJoin,
|
||||||
|
StrokeAlign,
|
||||||
|
StrokeCap,
|
||||||
|
PaintOrder,
|
||||||
|
GradientType,
|
||||||
|
GradientSpreadMethod,
|
||||||
|
DashPattern,
|
||||||
|
BoxCorners,
|
||||||
|
MergeByDistanceAlgorithm,
|
||||||
|
ExtrudeJoiningAlgorithm,
|
||||||
|
PointSpacingType,
|
||||||
|
StringCapitalization,
|
||||||
|
LuminanceCalculation,
|
||||||
|
RedGreenBlue,
|
||||||
|
RedGreenBlueAlpha,
|
||||||
|
RelativeAbsolute,
|
||||||
|
SelectiveColorChoice,
|
||||||
|
BrushTrace,
|
||||||
|
XY,
|
||||||
|
ScaleType,
|
||||||
|
ReferencePoint,
|
||||||
|
CentroidType,
|
||||||
|
BooleanOperation,
|
||||||
|
NoiseType,
|
||||||
|
FractalType,
|
||||||
|
CellularDistanceFunction,
|
||||||
|
CellularReturnType,
|
||||||
|
DomainWarpType,
|
||||||
|
graphene_std::animation::RealTimeMode,
|
||||||
|
graphene_std::vector::misc::GridType,
|
||||||
|
graphene_std::vector::misc::ArcType,
|
||||||
|
graphene_std::vector::misc::SpiralType,
|
||||||
|
graphene_std::text::TextAlign,
|
||||||
|
graphene_std::vector::QRCodeErrorCorrectionLevel,
|
||||||
|
Box<VectorModification>,
|
||||||
|
graphene_std::text::Font,
|
||||||
|
InterpolationDistribution,
|
||||||
|
RowsOrColumns,
|
||||||
|
Resource,
|
||||||
|
));
|
||||||
|
// A position wire may feed a ranked vector connector, each position becoming a single-anchor vector
|
||||||
|
node_types.extend(input_adapter_row!(from_element: DVec2, element: Vector));
|
||||||
|
// A string wire may feed the ranked `Item<DashPattern>` dash connector by parsing each element into a dash pattern
|
||||||
|
node_types.extend(input_adapter_row!(from_element: String, element: DashPattern));
|
||||||
|
// A number wire may feed the ranked `Item<DashPattern>` dash connector, each number broadcasting element-wise as a one-length pattern
|
||||||
|
node_types.extend(input_adapter_row!(from_element: f64, element: DashPattern));
|
||||||
|
// A string wire may feed the ranked `Item<BoxCorners>` connector by parsing each element into a set of corner values
|
||||||
|
node_types.extend(input_adapter_row!(from_element: String, element: BoxCorners));
|
||||||
|
// A number wire may feed the ranked `Item<BoxCorners>` connector, each number becoming a uniform radius for all four corners
|
||||||
|
node_types.extend(input_adapter_row!(from_element: f64, element: BoxCorners));
|
||||||
|
// Numeric wires cast between element types at a ranked connector, as `Convert` does for bare numeric wires
|
||||||
|
macro_rules! numeric_convert_node {
|
||||||
|
(from_element: $from:ty, element: $element:ty) => {{
|
||||||
|
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
input_adapter_row!(node: ConvertItemNode, from: Item<$from>, to: Item<$element>, element: $element),
|
||||||
|
input_adapter_row!(node: ConvertListNode, from: List<$from>, to: List<$element>, element: $element),
|
||||||
|
];
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
macro_rules! numeric_convert_star {
|
||||||
|
(from: $from:ty, to: [$($to:ty),*]) => {{
|
||||||
|
let mut entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = Vec::new();
|
||||||
|
$(entries.extend(numeric_convert_node!(from_element: $from, element: $to));)*
|
||||||
|
entries
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
node_types.extend(numeric_convert_star!(from: f64, to: [f32, u32, u64, i32, i64]));
|
||||||
|
node_types.extend(numeric_convert_star!(from: f32, to: [f64, u32, u64, i32, i64]));
|
||||||
|
node_types.extend(numeric_convert_star!(from: u32, to: [f64, f32, u64, i32, i64]));
|
||||||
|
node_types.extend(numeric_convert_star!(from: u64, to: [f64, f32, u32, i32, i64]));
|
||||||
|
node_types.extend(numeric_convert_star!(from: i32, to: [f64, f32, u32, u64, i64]));
|
||||||
|
node_types.extend(numeric_convert_star!(from: i64, to: [f64, f32, u32, u64, i32]));
|
||||||
|
// The sanctioned attribute value conversions: an Item wire's elements box per cell, while a List wire boxes whole as one value
|
||||||
|
macro_rules! attribute_value_node {
|
||||||
|
(Item<$element:ty>) => {
|
||||||
|
input_adapter_row!(node: ItemToAttributeValueNode, from: Item<$element>, to: Item<AttributeValueDyn>, element: AttributeValueDyn)
|
||||||
|
};
|
||||||
|
(List<$element:ty>) => {
|
||||||
|
input_adapter_row!(node: ListToAttributeValueNode, from: List<$element>, to: Item<AttributeValueDyn>, element: AttributeValueDyn)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let attribute_value_rows: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
attribute_value_node!(Item<f64>),
|
||||||
|
attribute_value_node!(Item<u32>),
|
||||||
|
attribute_value_node!(Item<u64>),
|
||||||
|
attribute_value_node!(Item<bool>),
|
||||||
|
attribute_value_node!(Item<String>),
|
||||||
|
attribute_value_node!(Item<DVec2>),
|
||||||
|
attribute_value_node!(Item<DAffine2>),
|
||||||
|
attribute_value_node!(Item<Color>),
|
||||||
|
attribute_value_node!(Item<BlendMode>),
|
||||||
|
attribute_value_node!(Item<GradientType>),
|
||||||
|
attribute_value_node!(Item<GradientSpreadMethod>),
|
||||||
|
attribute_value_node!(Item<NodeIdPath>),
|
||||||
|
attribute_value_node!(List<String>),
|
||||||
|
attribute_value_node!(List<Color>),
|
||||||
|
attribute_value_node!(List<Gradient>),
|
||||||
|
attribute_value_node!(List<Vector>),
|
||||||
|
attribute_value_node!(List<Raster<CPU>>),
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
attribute_value_node!(List<Raster<GPU>>),
|
||||||
|
attribute_value_node!(List<Graphic>),
|
||||||
|
];
|
||||||
|
node_types.extend(attribute_value_rows);
|
||||||
|
// Whole-List Transform companion rows: a rank-1 content wire composes the matrix onto every item
|
||||||
|
macro_rules! transform_list_node {
|
||||||
|
(element: $element:ty) => {
|
||||||
|
async_node!(
|
||||||
|
identifier: ProtoNodeIdentifier::new("transform_nodes::transform_nodes::TransformNode"),
|
||||||
|
graphene_std::transform_nodes::TransformListNode<_, _, _, _, _>,
|
||||||
|
input: Context,
|
||||||
|
fn_params: [Context => List<$element>, Context => Item<DVec2>, Context => Item<f64>, Context => Item<DVec2>, Context => Item<DVec2>]
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let transform_list_rows: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||||
|
transform_list_node!(element: Graphic),
|
||||||
|
transform_list_node!(element: String),
|
||||||
|
transform_list_node!(element: Vector),
|
||||||
|
transform_list_node!(element: Raster<CPU>),
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
transform_list_node!(element: Raster<GPU>),
|
||||||
|
transform_list_node!(element: Color),
|
||||||
|
transform_list_node!(element: Gradient),
|
||||||
|
];
|
||||||
|
node_types.extend(transform_list_rows);
|
||||||
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
||||||
|
|
||||||
|
// Rank normalization at this merge is the single convergence point for structurally-built and name-encoded `List` types,
|
||||||
|
// covering the sources which cannot construct them structurally (reflected return values and opaque macro captures)
|
||||||
for (id, entry) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() {
|
for (id, entry) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() {
|
||||||
for (constructor, types) in entry.iter() {
|
for (constructor, types) in entry.iter() {
|
||||||
map.entry(id.clone()).or_default().insert(types.clone(), *constructor);
|
map.entry(id.clone()).or_default().insert(types.clone().normalize_rank(), *constructor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,14 +589,18 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
// This might be caused by the stringify! macro
|
// This might be caused by the stringify! macro
|
||||||
let mut new_name = id.as_str().replace('\n', " ");
|
let mut new_name = id.as_str().replace('\n', " ");
|
||||||
|
|
||||||
// Remove struct generics for all nodes except for the IntoNode and ConvertNode
|
// Remove struct generics for all nodes except the adapter identifiers, whose element suffix distinguishes their rows
|
||||||
if !(new_name.contains("IntoNode") || new_name.contains("ConvertNode"))
|
let element_suffixed_adapter = new_name.starts_with("input_adapter<")
|
||||||
&& let Some((path, _generics)) = new_name.split_once("<")
|
|| new_name.starts_with("graphene_core::ops::ItemToListNode<")
|
||||||
{
|
|| new_name.starts_with("graphene_core::ops::BundleNode<")
|
||||||
|
|| new_name.starts_with("graphene_core::ops::UnbundleNode<");
|
||||||
|
if !element_suffixed_adapter && let Some((path, _generics)) = new_name.split_once("<") {
|
||||||
new_name = path.to_string();
|
new_name = path.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
map.entry(ProtoNodeIdentifier::with_owned_string(new_name)).or_default().insert(types.clone(), node_constructor);
|
map.entry(ProtoNodeIdentifier::with_owned_string(new_name))
|
||||||
|
.or_default()
|
||||||
|
.insert(types.clone().normalize_rank(), node_constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
map
|
||||||
@@ -357,8 +619,11 @@ mod node_registry_macros {
|
|||||||
async_node!($path, input: $input, fn_params: [ $(() => $type),*])
|
async_node!($path, input: $input, fn_params: [ $(() => $type),*])
|
||||||
};
|
};
|
||||||
($path:ty, input: $input:ty, fn_params: [$($arg:ty => $type:ty),*]) => {
|
($path:ty, input: $input:ty, fn_params: [$($arg:ty => $type:ty),*]) => {
|
||||||
|
async_node!(identifier: ProtoNodeIdentifier::new(stringify!($path)), $path, input: $input, fn_params: [$($arg => $type),*])
|
||||||
|
};
|
||||||
|
(identifier: $identifier:expr, $path:ty, input: $input:ty, fn_params: [$($arg:ty => $type:ty),*]) => {
|
||||||
(
|
(
|
||||||
ProtoNodeIdentifier::new(stringify!($path)),
|
$identifier,
|
||||||
|mut args| {
|
|mut args| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
args.reverse();
|
args.reverse();
|
||||||
@@ -380,105 +645,5 @@ mod node_registry_macros {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! into_node {
|
|
||||||
(from: $from:ty, to: $to:ty) => {
|
|
||||||
(
|
|
||||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
|
|
||||||
|mut args| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let node = graphene_std::ops::IntoNode::new(
|
|
||||||
graphene_std::any::downcast_node::<Context, $from>(args.pop().unwrap()),
|
|
||||||
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)),
|
|
||||||
);
|
|
||||||
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node);
|
|
||||||
Box::new(any) as TypeErasedBox
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let node = graphene_std::ops::IntoNode::new(
|
|
||||||
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
|
|
||||||
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)),
|
|
||||||
);
|
|
||||||
let params = vec![fn_type_fut!(Context, $from)];
|
|
||||||
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
|
||||||
node_io
|
|
||||||
},
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
macro_rules! convert_node {
|
|
||||||
(from: $from:ty, to: numbers) => {{
|
|
||||||
let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
|
||||||
convert_node!(from: $from, to: f32),
|
|
||||||
convert_node!(from: $from, to: f64),
|
|
||||||
convert_node!(from: $from, to: i8),
|
|
||||||
convert_node!(from: $from, to: u8),
|
|
||||||
convert_node!(from: $from, to: u16),
|
|
||||||
convert_node!(from: $from, to: i16),
|
|
||||||
convert_node!(from: $from, to: i32),
|
|
||||||
convert_node!(from: $from, to: u32),
|
|
||||||
convert_node!(from: $from, to: i64),
|
|
||||||
convert_node!(from: $from, to: u64),
|
|
||||||
convert_node!(from: $from, to: i128),
|
|
||||||
convert_node!(from: $from, to: u128),
|
|
||||||
convert_node!(from: $from, to: isize),
|
|
||||||
convert_node!(from: $from, to: usize),
|
|
||||||
];
|
|
||||||
x
|
|
||||||
}};
|
|
||||||
(from: numbers, to: $to:ty) => {{
|
|
||||||
let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
|
||||||
convert_node!(from: f32, to: $to),
|
|
||||||
convert_node!(from: f64, to: $to),
|
|
||||||
convert_node!(from: i8, to: $to),
|
|
||||||
convert_node!(from: u8, to: $to),
|
|
||||||
convert_node!(from: u16, to: $to),
|
|
||||||
convert_node!(from: i16, to: $to),
|
|
||||||
convert_node!(from: i32, to: $to),
|
|
||||||
convert_node!(from: u32, to: $to),
|
|
||||||
convert_node!(from: i64, to: $to),
|
|
||||||
convert_node!(from: u64, to: $to),
|
|
||||||
convert_node!(from: i128, to: $to),
|
|
||||||
convert_node!(from: u128, to: $to),
|
|
||||||
convert_node!(from: isize, to: $to),
|
|
||||||
convert_node!(from: usize, to: $to),
|
|
||||||
];
|
|
||||||
x
|
|
||||||
}};
|
|
||||||
(from: $from:ty, to: $to:ty) => {
|
|
||||||
convert_node!(from: $from, to: $to, converter: ())
|
|
||||||
};
|
|
||||||
(from: $from:ty, to: $to:ty, converter: $convert:ty) => {
|
|
||||||
(
|
|
||||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
|
|
||||||
|mut args| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut args = args.drain(..);
|
|
||||||
let node = graphene_std::ops::ConvertNode::new(
|
|
||||||
graphene_std::any::downcast_node::<Context, $from>(args.next().expect("Convert node did not get first argument")),
|
|
||||||
graphene_std::any::downcast_node::<Context, $convert>(args.next().expect("Convert node did not get converter argument")),
|
|
||||||
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>))
|
|
||||||
);
|
|
||||||
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node);
|
|
||||||
Box::new(any) as TypeErasedBox
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let node = graphene_std::ops::ConvertNode::new(
|
|
||||||
|
|
||||||
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
|
|
||||||
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $convert> + Send>>>::new(),
|
|
||||||
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>))
|
|
||||||
);
|
|
||||||
let params = vec![fn_type_fut!(Context, $from), fn_type_fut!(Context, $convert)];
|
|
||||||
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
|
|
||||||
node_io
|
|
||||||
},
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) use async_node;
|
pub(crate) use async_node;
|
||||||
pub(crate) use convert_node;
|
|
||||||
pub(crate) use into_node;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use graph_craft::application_io::PlatformEditorApi;
|
use graph_craft::application_io::PlatformEditorApi;
|
||||||
use graph_craft::concrete;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
|
||||||
use graph_craft::generic;
|
use graph_craft::generic;
|
||||||
|
use graph_craft::{concrete, item};
|
||||||
use graphene_std::Context;
|
use graphene_std::Context;
|
||||||
use graphene_std::ContextFeatures;
|
use graphene_std::ContextFeatures;
|
||||||
use graphene_std::uuid::NodeId;
|
use graphene_std::uuid::NodeId;
|
||||||
@@ -84,12 +84,12 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
call_argument: concrete!(graphene_std::application_io::RenderConfig),
|
call_argument: concrete!(Context),
|
||||||
inputs: vec![NodeInput::node(NodeId(4), 0)],
|
inputs: vec![NodeInput::node(NodeId(4), 0)],
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
|
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
|
||||||
context_features: graphene_std::ContextDependencies {
|
context_features: graphene_std::ContextDependencies {
|
||||||
// We add the extract index annotation here to force the compiler to add a context nullification node before this node so the render context is properly nullified so the render cache node can do its's work
|
// INDEX is never read; it forces a nullification and cache pair onto the `data` parameter's wire so the render cache keys on a stable context
|
||||||
extract: ContextFeatures::INDEX,
|
extract: ContextFeatures::VARARGS | ContextFeatures::INDEX,
|
||||||
inject: ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER_POSITION | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
|
inject: ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER_POSITION | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -114,7 +114,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))];
|
let scope_injections = vec![("editor-api".to_string(), (NodeId(2), item!(&PlatformEditorApi)))];
|
||||||
|
|
||||||
NodeNetwork {
|
NodeNetwork {
|
||||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||||
|
|||||||
@@ -16,6 +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, 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,8 @@
|
|||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
|
use core_types::{Context, OwnedContextImpl};
|
||||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
use graphene_hash::CacheHash;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ptr::addr_of;
|
use std::ptr::addr_of;
|
||||||
@@ -54,7 +56,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)]
|
#[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]
|
||||||
@@ -62,14 +64,14 @@ pub enum ExportFormat {
|
|||||||
Raster,
|
Raster,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
|
#[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)]
|
#[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,
|
||||||
@@ -83,6 +85,13 @@ pub struct RenderConfig {
|
|||||||
pub for_eyedropper: bool,
|
pub for_eyedropper: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RenderConfig {
|
||||||
|
/// Wraps this render configuration as the sole vararg of a fresh context, the call argument of a compiled network's boundary node.
|
||||||
|
pub fn into_context(self) -> Context<'static> {
|
||||||
|
OwnedContextImpl::default().with_vararg(Box::new(self)).into_context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Logger;
|
struct Logger;
|
||||||
|
|
||||||
impl NodeGraphUpdateSender for Logger {
|
impl NodeGraphUpdateSender for Logger {
|
||||||
@@ -127,7 +136,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub trait BoundingBox {
|
|||||||
/// Returns the bounding box to use when sizing this value's thumbnail in the Layers panel.
|
/// Returns the bounding box to use when sizing this value's thumbnail in the Layers panel.
|
||||||
///
|
///
|
||||||
/// Diverges from `bounding_box` for types where the rendering bounds wouldn't make a useful thumbnail frame.
|
/// Diverges from `bounding_box` for types where the rendering bounds wouldn't make a useful thumbnail frame.
|
||||||
/// For instance, `GradientStops` is `Infinite` for rendering but returns the line's AABB here, so a `List<Graphic>`
|
/// For instance, `Gradient` is `Infinite` for rendering but returns the line's AABB here, so a `List<Graphic>`
|
||||||
/// group of a gradient and a vector frames around the vector's geometry rather than infinity.
|
/// group of a gradient and a vector frames around the vector's geometry rather than infinity.
|
||||||
/// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a
|
/// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a
|
||||||
/// small fallback rectangle at the end if no finite bounds remain after combining.
|
/// small fallback rectangle at the end if no finite bounds remain after combining.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::bounds::{BoundingBox, RenderBoundingBox};
|
use crate::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use crate::math::quad::Quad;
|
use crate::math::quad::Quad;
|
||||||
use crate::transform::ApplyTransform;
|
use crate::transform::ApplyTransform;
|
||||||
use dyn_any::{StaticType, StaticTypeSized};
|
use crate::uuid::NodeId;
|
||||||
|
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graphene_hash::CacheHash;
|
use graphene_hash::CacheHash;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@@ -22,7 +23,7 @@ pub const ATTR_OPACITY: &str = "opacity";
|
|||||||
pub const ATTR_OPACITY_FILL: &str = "opacity_fill";
|
pub const ATTR_OPACITY_FILL: &str = "opacity_fill";
|
||||||
/// `bool` for whether an item inherits the alpha of the content beneath it (clipping mask).
|
/// `bool` for whether an item inherits the alpha of the content beneath it (clipping mask).
|
||||||
pub const ATTR_CLIPPING_MASK: &str = "clipping_mask";
|
pub const ATTR_CLIPPING_MASK: &str = "clipping_mask";
|
||||||
/// `List<NodeId>` path from the root network to the layer node owning this item.
|
/// `NodeIdPath` path from the root network to the layer node owning this item.
|
||||||
/// Used by editor tools to route clicks/selection back to the originating layer.
|
/// Used by editor tools to route clicks/selection back to the originating layer.
|
||||||
pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path";
|
pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path";
|
||||||
/// `List<Graphic>` snapshot of the upstream content that fed into a destructive merge
|
/// `List<Graphic>` snapshot of the upstream content that fed into a destructive merge
|
||||||
@@ -79,6 +80,54 @@ pub const ATTR_LETTER_TILT: &str = "letter_tilt";
|
|||||||
/// Text item's `TextAlign` horizontal alignment of lines within the block.
|
/// Text item's `TextAlign` horizontal alignment of lines within the block.
|
||||||
pub const ATTR_TEXT_ALIGN: &str = "text_align";
|
pub const ATTR_TEXT_ALIGN: &str = "text_align";
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// TYPE: NodeIdPath
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
/// A single path of `NodeId`s locating a node (or its owning layer) within the nested document graph.
|
||||||
|
/// Wraps a `List<NodeId>` so it flows as one rank-0 value (`Item<NodeIdPath>`) rather than a rank-1
|
||||||
|
/// `List<NodeId>` that the element-wise machinery would wrongly zip over per ID.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, CacheHash, DynAny)]
|
||||||
|
pub struct NodeIdPath(pub List<NodeId>);
|
||||||
|
|
||||||
|
impl From<Vec<NodeId>> for NodeIdPath {
|
||||||
|
fn from(ids: Vec<NodeId>) -> Self {
|
||||||
|
Self(ids.into_iter().map(Item::new_from_element).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================
|
||||||
|
// TYPE: Bundle
|
||||||
|
// ================
|
||||||
|
|
||||||
|
/// A whole `List<T>` treated as one rank-0 value (`Item<Bundle<T>>`) rather than a rank-1 `List<T>`.
|
||||||
|
/// Bundling a collection lets it pass through a connector that selects or carries the entire collection as one opaque
|
||||||
|
/// cell (such as a Switch branch), instead of the element-wise machinery zipping over it per element.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Bundle<T>(pub List<T>);
|
||||||
|
|
||||||
|
impl<T> Default for Bundle<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(List::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: CacheHash> CacheHash for Bundle<T> {
|
||||||
|
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.0.cache_hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<List<T>> for Bundle<T> {
|
||||||
|
fn from(list: List<T>) -> Self {
|
||||||
|
Self(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T: StaticTypeSized> StaticType for Bundle<T> {
|
||||||
|
type Static = Bundle<T::Static>;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================
|
// ===========================
|
||||||
// Implicit attribute defaults
|
// Implicit attribute defaults
|
||||||
// ===========================
|
// ===========================
|
||||||
@@ -125,6 +174,13 @@ pub trait AnyAttributeValue: std::any::Any + Send + Sync {
|
|||||||
/// Returns a debug-formatted string representation of this value.
|
/// Returns a debug-formatted string representation of this value.
|
||||||
fn display_string(&self) -> String;
|
fn display_string(&self) -> String;
|
||||||
|
|
||||||
|
/// Hashes this value into the given hasher (object-safe wrapper around `CacheHash`).
|
||||||
|
fn cache_hash_dyn(&self, state: &mut dyn core::hash::Hasher);
|
||||||
|
|
||||||
|
/// Compares this value to another for value-by-value equality (object-safe wrapper around `PartialEq`).
|
||||||
|
/// Returns `false` if the underlying types differ.
|
||||||
|
fn eq_dyn(&self, other: &dyn AnyAttributeValue) -> bool;
|
||||||
|
|
||||||
/// Wraps this scalar value into a new attribute, preceded by `preceding_defaults` implicit defaults for `key`.
|
/// Wraps this scalar value into a new attribute, preceded by `preceding_defaults` implicit defaults for `key`.
|
||||||
fn into_attribute(self: Box<Self>, key: &str, preceding_defaults: usize) -> Box<dyn AnyAttribute>;
|
fn into_attribute(self: Box<Self>, key: &str, preceding_defaults: usize) -> Box<dyn AnyAttribute>;
|
||||||
}
|
}
|
||||||
@@ -155,6 +211,17 @@ impl<T: Clone + Send + Sync + Default + Sized + Debug + PartialEq + CacheHash +
|
|||||||
format!("{:?}", self)
|
format!("{:?}", self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hashes this value into the given hasher (object-safe wrapper around `CacheHash`).
|
||||||
|
fn cache_hash_dyn(&self, state: &mut dyn core::hash::Hasher) {
|
||||||
|
self.cache_hash(&mut DynHasher(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares this value to another for value-by-value equality (object-safe wrapper around `PartialEq`).
|
||||||
|
/// Returns `false` if the underlying types differ.
|
||||||
|
fn eq_dyn(&self, other: &dyn AnyAttributeValue) -> bool {
|
||||||
|
other.as_any().downcast_ref::<Self>().is_some_and(|other| self == other)
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps this scalar value into a new attribute, preceded by `preceding_defaults` implicit defaults for `key`.
|
/// Wraps this scalar value into a new attribute, preceded by `preceding_defaults` implicit defaults for `key`.
|
||||||
fn into_attribute(self: Box<Self>, key: &str, preceding_defaults: usize) -> Box<dyn AnyAttribute> {
|
fn into_attribute(self: Box<Self>, key: &str, preceding_defaults: usize) -> Box<dyn AnyAttribute> {
|
||||||
let mut attribute: Box<dyn AnyAttribute> = Box::new(Attribute::<T>(Vec::with_capacity(preceding_defaults + 1)));
|
let mut attribute: Box<dyn AnyAttribute> = Box::new(Attribute::<T>(Vec::with_capacity(preceding_defaults + 1)));
|
||||||
@@ -361,83 +428,12 @@ impl<T: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
|
||||||
// AttributeDyn
|
|
||||||
// ============
|
|
||||||
|
|
||||||
/// Type-erased list of attribute values, used as a node graph parameter type.
|
|
||||||
/// Lets a node accept any `List<U>` source via the auto-inserted `Convert<AttributeDyn, ()>`
|
|
||||||
/// without monomorphizing over `U` (so the cartesian product of `(content T, source U)` collapses to just `T`).
|
|
||||||
pub struct AttributeDyn(pub Box<dyn AnyAttribute>);
|
|
||||||
|
|
||||||
impl AttributeDyn {
|
|
||||||
/// Number of values in this attribute.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this attribute has zero values.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.0.len() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a new attribute matching `target_len` items, taking values from this attribute (wrapping if shorter, truncating if longer).
|
|
||||||
pub fn cloned_to_length(&self, key: &str, target_len: usize) -> Box<dyn AnyAttribute> {
|
|
||||||
let mut result = self.0.new_with_defaults(0);
|
|
||||||
let source_len = self.0.len();
|
|
||||||
if source_len == 0 {
|
|
||||||
pad_with_implicit_default(key, &mut result, target_len);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
for i in 0..target_len {
|
|
||||||
let value = self.0.clone_value(i % source_len).expect("source_len > 0");
|
|
||||||
result.push(value);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for AttributeDyn {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone_box())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AttributeDyn {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Box::new(Attribute::<bool>(Vec::new())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for AttributeDyn {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "AttributeDyn(len: {})", self.0.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for AttributeDyn {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.0.eq_dyn(&*other.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CacheHash for AttributeDyn {
|
|
||||||
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.0.cache_hash_dyn(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl StaticType for AttributeDyn {
|
|
||||||
type Static = Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// AttributeValueDyn
|
// AttributeValueDyn
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/// Type-erased single attribute value, used as a node graph parameter type.
|
/// Type-erased single attribute value, used as a node graph parameter type.
|
||||||
/// Lets a node accept a value of any concrete type via the auto-inserted `Convert<AttributeValueDyn, ()>`
|
/// Lets a node accept a value of any valid concrete type via the auto-inserted input adapter conversion without monomorphizing over the value type.
|
||||||
/// without monomorphizing over the value type.
|
|
||||||
pub struct AttributeValueDyn(pub Box<dyn AnyAttributeValue>);
|
pub struct AttributeValueDyn(pub Box<dyn AnyAttributeValue>);
|
||||||
|
|
||||||
impl Clone for AttributeValueDyn {
|
impl Clone for AttributeValueDyn {
|
||||||
@@ -576,6 +572,17 @@ impl Debug for ItemAttributeValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ItemAttributeValues {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0.len() == other.0.len()
|
||||||
|
&& self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.zip(&other.0)
|
||||||
|
.all(|((self_key, self_value), (other_key, other_value))| self_key == other_key && self_value.eq_dyn(other_value.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ItemAttributeValues {
|
impl ItemAttributeValues {
|
||||||
/// Creates an empty set of attributes.
|
/// Creates an empty set of attributes.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -646,6 +653,11 @@ impl ItemAttributeValues {
|
|||||||
self.0.iter().map(|(key, _)| key.as_str())
|
self.0.iter().map(|(key, _)| key.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
@@ -1065,14 +1077,6 @@ impl<T> List<T> {
|
|||||||
self.attributes.set_value(key, index, value);
|
self.attributes.set_value(key, index, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces (or adds) an attribute from a type-erased source. The source is wrapped or truncated to match this list's item count.
|
|
||||||
pub fn set_attribute_dyn(&mut self, key: impl Into<String>, source: AttributeDyn) {
|
|
||||||
let key = key.into();
|
|
||||||
self.attributes.attributes.retain(|(k, _)| k != &key);
|
|
||||||
let new_attribute = source.cloned_to_length(&key, self.element.len());
|
|
||||||
self.attributes.attributes.push((key, new_attribute));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a single type-erased attribute value at the given index, creating the attribute from the value's underlying type if it doesn't exist (padded with defaults to match the list's length).
|
/// Sets a single type-erased attribute value at the given index, creating the attribute from the value's underlying type if it doesn't exist (padded with defaults to match the list's length).
|
||||||
/// Falls back to default if the value's type doesn't match an existing attribute.
|
/// Falls back to default if the value's type doesn't match an existing attribute.
|
||||||
pub fn set_attribute_value_dyn(&mut self, key: impl Into<String>, index: usize, value: AttributeValueDyn) {
|
pub fn set_attribute_value_dyn(&mut self, key: impl Into<String>, index: usize, value: AttributeValueDyn) {
|
||||||
@@ -1280,7 +1284,7 @@ impl<T> FromIterator<Item<T>> for List<T> {
|
|||||||
/// An owned item containing an element of type `T` and a set of type-erased scalar attributes.
|
/// An owned item containing an element of type `T` and a set of type-erased scalar attributes.
|
||||||
///
|
///
|
||||||
/// Used to build individual items before pushing them into a [`List`], or when consuming items out of a list via [`IntoIterator`].
|
/// Used to build individual items before pushing them into a [`List`], or when consuming items out of a list via [`IntoIterator`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Item<T> {
|
pub struct Item<T> {
|
||||||
element: T,
|
element: T,
|
||||||
attributes: ItemAttributeValues,
|
attributes: ItemAttributeValues,
|
||||||
@@ -1292,9 +1296,15 @@ impl<T: Default> Default for Item<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq> PartialEq for Item<T> {
|
impl<T: CacheHash> CacheHash for Item<T> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.element == other.element
|
self.element.cache_hash(state);
|
||||||
|
|
||||||
|
// Hash every attribute (key + value) so attribute changes invalidate downstream caches, mirroring `List`
|
||||||
|
for (key, attribute) in &self.attributes.0 {
|
||||||
|
std::hash::Hash::hash(key.as_str(), state);
|
||||||
|
attribute.cache_hash_dyn(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1386,6 +1396,42 @@ impl<T> Item<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Item<T> {
|
||||||
|
fn from(element: T) -> Self {
|
||||||
|
Self::new_from_element(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Item<T>> for List<T> {
|
||||||
|
fn from(item: Item<T>) -> Self {
|
||||||
|
Self::new_from_item(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for List<T> {
|
||||||
|
fn from(element: T) -> Self {
|
||||||
|
Self::new_from_element(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ApplyTransform for Item<T> {
|
||||||
|
/// Right-multiplies the modification into the item's transform attribute.
|
||||||
|
fn apply_transform(&mut self, modification: &DAffine2) {
|
||||||
|
let transform = self.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
*transform *= *modification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Left-multiplies the modification into the item's transform attribute.
|
||||||
|
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
||||||
|
let transform = self.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
*transform = *modification * *transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T: StaticTypeSized> StaticType for Item<T> {
|
||||||
|
type Static = Item<T::Static>;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// ItemIter<T>
|
// ItemIter<T>
|
||||||
// ===========
|
// ===========
|
||||||
|
|||||||
@@ -61,6 +61,27 @@ impl Clampable for DVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement for ranked wires (element-wise clamping across the frame)
|
||||||
|
use crate::list::{Item, List};
|
||||||
|
impl<T: Clampable> Clampable for Item<T> {
|
||||||
|
fn clamp_hard_min(self, min: f64) -> Self {
|
||||||
|
let (element, attributes) = self.into_parts();
|
||||||
|
Item::from_parts(element.clamp_hard_min(min), attributes)
|
||||||
|
}
|
||||||
|
fn clamp_hard_max(self, max: f64) -> Self {
|
||||||
|
let (element, attributes) = self.into_parts();
|
||||||
|
Item::from_parts(element.clamp_hard_max(max), attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Clampable> Clampable for List<T> {
|
||||||
|
fn clamp_hard_min(self, min: f64) -> Self {
|
||||||
|
self.into_iter().map(|item| item.clamp_hard_min(min)).collect()
|
||||||
|
}
|
||||||
|
fn clamp_hard_max(self, max: f64) -> Self {
|
||||||
|
self.into_iter().map(|item| item.clamp_hard_max(max)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct LegacyTable<T> {
|
struct LegacyTable<T> {
|
||||||
@@ -69,7 +90,7 @@ struct LegacyTable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
pub fn migrate_to_optional_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<no_std_types::color::Color>, D::Error> {
|
pub fn migrate_to_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<no_std_types::color::Color, D::Error> {
|
||||||
use no_std_types::color::Color;
|
use no_std_types::color::Color;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@@ -81,8 +102,8 @@ pub fn migrate_to_optional_color<'de, D: serde::Deserializer<'de>>(deserializer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(match ColorFormat::deserialize(deserializer)? {
|
Ok(match ColorFormat::deserialize(deserializer)? {
|
||||||
ColorFormat::OptionalColor(color) => color,
|
ColorFormat::OptionalColor(color) => color.unwrap_or(Color::TRANSPARENT),
|
||||||
ColorFormat::List(list) => list.element.into_iter().next(),
|
ColorFormat::List(list) => list.element.into_iter().next().unwrap_or(Color::TRANSPARENT),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::Node;
|
use crate::Node;
|
||||||
use crate::list::{Attribute, AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
|
|
||||||
use crate::transform::Footprint;
|
use crate::transform::Footprint;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphene_hash::CacheHash;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
@@ -47,79 +45,12 @@ pub trait Convert<T, C>: Sized {
|
|||||||
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
|
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ToString + Send> Convert<String, ()> for T {
|
|
||||||
/// Converts this type into a `String` using its `ToString` implementation.
|
|
||||||
#[inline]
|
|
||||||
async fn convert(self, _: Footprint, _converter: ()) -> String {
|
|
||||||
self.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ListConvert<U> {
|
|
||||||
fn convert_item(self) -> U;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> List<U> {
|
|
||||||
let list: List<U> = self
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| {
|
|
||||||
let (element, attributes) = row.into_parts();
|
|
||||||
Item::from_parts(element.convert_item(), attributes)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps each row's element into a type-erased attribute. Lets nodes that accept a source attribute
|
|
||||||
/// from any `List<U>` express their signature as `AttributeDyn` and avoid monomorphizing
|
|
||||||
/// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input.
|
|
||||||
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeDyn, ()> for List<T> {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> AttributeDyn {
|
|
||||||
let values: Vec<T> = self.into_iter().map(|row| row.into_element()).collect();
|
|
||||||
AttributeDyn(Box::new(Attribute(values)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps a value into a type-erased attribute value. Lets nodes that take a per-item value source
|
|
||||||
/// (such as `write_attribute`'s value-producing input) be generic over the destination list type
|
|
||||||
/// alone, with the compiler-inserted convert handling each concrete value type at the wire level.
|
|
||||||
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeValueDyn, ()> for T {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn {
|
|
||||||
AttributeValueDyn(Box::new(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Erases a `List<T>`'s element type, exposing only its attributes and row count. Lets nodes that
|
|
||||||
/// only need attribute access (such as the `read_attribute_*` family) take a single `ListDyn` input
|
|
||||||
/// instead of monomorphizing over every possible carrier list type.
|
|
||||||
impl<T: Send> Convert<ListDyn, ()> for List<T> {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> ListDyn {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Convert<DVec2, ()> for DVec2 {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> DVec2 {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a position into a vector path composed of a single anchor point
|
|
||||||
impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
|
|
||||||
async fn convert(self, _: Footprint, _: ()) -> List<T> {
|
|
||||||
List::new_from_item(Item::new_from_element(T::from_anchor_position(self)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
|
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
|
||||||
macro_rules! impl_convert {
|
macro_rules! impl_convert {
|
||||||
($from:ty, $to:ty) => {
|
($from:ty, $to:ty) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -52,8 +52,48 @@ macro_rules! generic {
|
|||||||
($type:ty) => {{ $crate::Type::Generic($crate::Cow::Borrowed(stringify!($type))) }};
|
($type:ty) => {{ $crate::Type::Generic($crate::Cow::Borrowed(stringify!($type))) }};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the [`Type`] of an `Item` holding the given element type, e.g. `item!(f64)` is the type of an `Item<f64>`.
|
||||||
|
/// The two-argument form tags the element descriptor with an alias, preserving the source spelling for widget dispatch.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! item {
|
||||||
|
(Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Item(Box::new($crate::item!($inner)))
|
||||||
|
};
|
||||||
|
($element:ty) => {
|
||||||
|
$crate::Type::Item(Box::new($crate::concrete!($element)))
|
||||||
|
};
|
||||||
|
($element:ty, $alias:ty) => {
|
||||||
|
$crate::Type::Item(Box::new($crate::concrete!($element, $alias)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the [`Type`] of a `List` holding the given element type, e.g. `list!(f64)` is the type of a `List<f64>`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! list {
|
||||||
|
(List<$inner:ty>) => {
|
||||||
|
$crate::Type::List(Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
($element:ty) => {
|
||||||
|
$crate::Type::List(Box::new($crate::concrete!($element)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `List<...>`/`Item<...>` rules must appear before the generic `$type:ty` rules, and in each macro that sees the literal tokens,
|
||||||
|
// because a type captured as `ty` becomes opaque to any inner macro's ranked pattern
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! future {
|
macro_rules! future {
|
||||||
|
(List<$inner:ty>) => {
|
||||||
|
$crate::Type::Future(Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
(List<$inner:ty>, $name:ty) => {
|
||||||
|
$crate::Type::Future(Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
(Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Future(Box::new($crate::item!($inner)))
|
||||||
|
};
|
||||||
|
(Item<$inner:ty>, $name:ty) => {
|
||||||
|
$crate::Type::Future(Box::new($crate::item!($inner, $name)))
|
||||||
|
};
|
||||||
($type:ty) => {{ $crate::Type::Future(Box::new(concrete!($type))) }};
|
($type:ty) => {{ $crate::Type::Future(Box::new(concrete!($type))) }};
|
||||||
($type:ty, $name:ty) => {
|
($type:ty, $name:ty) => {
|
||||||
$crate::Type::Future(Box::new(concrete!($type, $name)))
|
$crate::Type::Future(Box::new(concrete!($type, $name)))
|
||||||
@@ -62,9 +102,27 @@ macro_rules! future {
|
|||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fn_type {
|
macro_rules! fn_type {
|
||||||
|
(List<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
(Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::item!($inner)))
|
||||||
|
};
|
||||||
($type:ty) => {
|
($type:ty) => {
|
||||||
$crate::Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
|
||||||
};
|
};
|
||||||
|
($in_type:ty, List<$inner:ty>, alias: $outname:ty) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
($in_type:ty, List<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::list!($inner)))
|
||||||
|
};
|
||||||
|
($in_type:ty, Item<$inner:ty>, alias: $outname:ty) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::item!($inner, $inner)))
|
||||||
|
};
|
||||||
|
($in_type:ty, Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::item!($inner)))
|
||||||
|
};
|
||||||
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
||||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type, $outname)))
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type, $outname)))
|
||||||
};
|
};
|
||||||
@@ -74,9 +132,27 @@ macro_rules! fn_type {
|
|||||||
}
|
}
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fn_type_fut {
|
macro_rules! fn_type_fut {
|
||||||
|
(List<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
|
||||||
|
};
|
||||||
|
(Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::item!($inner)))))
|
||||||
|
};
|
||||||
($type:ty) => {
|
($type:ty) => {
|
||||||
$crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type)))
|
$crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type)))
|
||||||
};
|
};
|
||||||
|
($in_type:ty, List<$inner:ty>, alias: $outname:ty) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
|
||||||
|
};
|
||||||
|
($in_type:ty, List<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::list!($inner)))))
|
||||||
|
};
|
||||||
|
($in_type:ty, Item<$inner:ty>, alias: $outname:ty) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::item!($inner, $inner)))))
|
||||||
|
};
|
||||||
|
($in_type:ty, Item<$inner:ty>) => {
|
||||||
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::item!($inner)))))
|
||||||
|
};
|
||||||
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
||||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type, $outname)))
|
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type, $outname)))
|
||||||
};
|
};
|
||||||
@@ -99,6 +175,15 @@ impl NodeIOTypes {
|
|||||||
Self { call_argument, return_value, inputs }
|
Self { call_argument, return_value, inputs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies [`Type::normalize_rank`] to every type in the signature.
|
||||||
|
pub fn normalize_rank(self) -> Self {
|
||||||
|
Self {
|
||||||
|
call_argument: self.call_argument.normalize_rank(),
|
||||||
|
return_value: self.return_value.normalize_rank(),
|
||||||
|
inputs: self.inputs.into_iter().map(Type::normalize_rank).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn empty() -> Self {
|
pub const fn empty() -> Self {
|
||||||
let tds1 = TypeDescriptor {
|
let tds1 = TypeDescriptor {
|
||||||
id: None,
|
id: None,
|
||||||
@@ -235,6 +320,10 @@ pub enum Type {
|
|||||||
Fn(Box<Type>, Box<Type>),
|
Fn(Box<Type>, Box<Type>),
|
||||||
/// Represents a future which promises to return the inner type.
|
/// Represents a future which promises to return the inner type.
|
||||||
Future(Box<Type>),
|
Future(Box<Type>),
|
||||||
|
/// Represents a recursive [Type] allowing nested levels of types to represent the type of an Item<T>.
|
||||||
|
Item(Box<Type>),
|
||||||
|
/// Represents a list of this recursive [Type] allowing nested levels of types to represent the type of a List<T>.
|
||||||
|
List(Box<Type>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Type {
|
impl Default for Type {
|
||||||
@@ -308,6 +397,8 @@ impl Type {
|
|||||||
Self::Concrete(ty) => Some(ty.size),
|
Self::Concrete(ty) => Some(ty.size),
|
||||||
Self::Fn(_, _) => None,
|
Self::Fn(_, _) => None,
|
||||||
Self::Future(_) => None,
|
Self::Future(_) => None,
|
||||||
|
Self::Item(_) => None,
|
||||||
|
Self::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +408,8 @@ impl Type {
|
|||||||
Self::Concrete(ty) => Some(ty.align),
|
Self::Concrete(ty) => Some(ty.align),
|
||||||
Self::Fn(_, _) => None,
|
Self::Fn(_, _) => None,
|
||||||
Self::Future(_) => None,
|
Self::Future(_) => None,
|
||||||
|
Self::Item(_) => None,
|
||||||
|
Self::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +419,8 @@ impl Type {
|
|||||||
Self::Concrete(_) => self,
|
Self::Concrete(_) => self,
|
||||||
Self::Fn(_, output) => output.nested_type(),
|
Self::Fn(_, output) => output.nested_type(),
|
||||||
Self::Future(output) => output.nested_type(),
|
Self::Future(output) => output.nested_type(),
|
||||||
|
Self::Item(_) => self,
|
||||||
|
Self::List(_) => self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +433,8 @@ impl Type {
|
|||||||
Self::Concrete(_) => None,
|
Self::Concrete(_) => None,
|
||||||
Self::Fn(_, output) => output.replace_nested(f),
|
Self::Fn(_, output) => output.replace_nested(f),
|
||||||
Self::Future(output) => output.replace_nested(f),
|
Self::Future(output) => output.replace_nested(f),
|
||||||
|
Self::Item(_) => None,
|
||||||
|
Self::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +444,62 @@ impl Type {
|
|||||||
Type::Concrete(ty) => simplify_identifier_name(&ty.name),
|
Type::Concrete(ty) => simplify_identifier_name(&ty.name),
|
||||||
Type::Fn(call_arg, return_value) => format!("{} called with {}", return_value.identifier_name(), call_arg.identifier_name()),
|
Type::Fn(call_arg, return_value) => format!("{} called with {}", return_value.identifier_name(), call_arg.identifier_name()),
|
||||||
Type::Future(ty) => ty.identifier_name(),
|
Type::Future(ty) => ty.identifier_name(),
|
||||||
|
Type::Item(element) => element.identifier_name(),
|
||||||
|
Type::List(element) => format!("{}[]", element.identifier_name()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the [`Type`] of a `List` holding elements of the given type, the expression-position counterpart of [`list!`].
|
||||||
|
pub fn list_of(element: Type) -> Type {
|
||||||
|
Type::List(Box::new(element))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element type if this is a rank-1 `List` wire type.
|
||||||
|
pub fn list_element(&self) -> Option<&Type> {
|
||||||
|
match self {
|
||||||
|
Type::List(element) => Some(element),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element name if this is the type of an `Item<Bundle<X>>` cell carrying a whole list, e.g. `f64` from `Item<Bundle<f64>>`.
|
||||||
|
/// The `Bundle` layer stays name-encoded inside the structural `Item` since it has no structural variant.
|
||||||
|
pub fn bundle_element_name(&self) -> Option<&str> {
|
||||||
|
let Type::Item(element) = self else { return None };
|
||||||
|
let Type::Concrete(descriptor) = element.as_ref() else { return None };
|
||||||
|
descriptor.name.strip_prefix("core_types::list::Bundle<")?.strip_suffix('>')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a name-encoded `List` or `Item` concrete type into its structural form, recursively.
|
||||||
|
/// Structurally-built types pass through unchanged, so sources which cannot construct ranked types
|
||||||
|
/// (reflection and opaque macro captures) converge with macro-built ones at this single point.
|
||||||
|
pub fn normalize_rank(self) -> Type {
|
||||||
|
fn parse_element(element_name: &str) -> Type {
|
||||||
|
let element = Type::Concrete(TypeDescriptor {
|
||||||
|
id: None,
|
||||||
|
name: Cow::Owned(element_name.to_string()),
|
||||||
|
alias: None,
|
||||||
|
size: 0,
|
||||||
|
align: 0,
|
||||||
|
});
|
||||||
|
element.normalize_rank()
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Type::Concrete(descriptor) => {
|
||||||
|
if let Some(element_name) = descriptor.name.strip_prefix("core_types::list::List<").and_then(|rest| rest.strip_suffix('>')) {
|
||||||
|
return Type::List(Box::new(parse_element(element_name)));
|
||||||
|
}
|
||||||
|
if let Some(element_name) = descriptor.name.strip_prefix("core_types::list::Item<").and_then(|rest| rest.strip_suffix('>')) {
|
||||||
|
return Type::Item(Box::new(parse_element(element_name)));
|
||||||
|
}
|
||||||
|
Type::Concrete(descriptor)
|
||||||
|
}
|
||||||
|
Type::Fn(input, output) => Type::Fn(Box::new(input.normalize_rank()), Box::new(output.normalize_rank())),
|
||||||
|
Type::Future(inner) => Type::Future(Box::new(inner.normalize_rank())),
|
||||||
|
Type::Item(element) => Type::Item(Box::new(element.normalize_rank())),
|
||||||
|
Type::List(element) => Type::List(Box::new(element.normalize_rank())),
|
||||||
|
Type::Generic(_) => self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,12 +524,13 @@ pub fn make_type_user_readable(ty: &str) -> String {
|
|||||||
.replace("UVec2", "Vec2")
|
.replace("UVec2", "Vec2")
|
||||||
.replace("&str", "String");
|
.replace("&str", "String");
|
||||||
|
|
||||||
rewrite_list_as_array_brackets(&ty)
|
rewrite_ranked_type_wrappers(&ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rewrites `List<T>` as `T[]`. Handles nesting (e.g. `List<List<Vector>>` becomes `Vector[][]`).
|
/// Rewrites `List<T>` and the whole-collection `Bundle<T>` as `T[]`, and unwraps `Item<T>` to `T`, so ranked wires read as their element type.
|
||||||
/// Respects word boundaries so unrelated identifiers that happen to end in `List` are not affected.
|
/// Handles nesting (e.g. `List<List<Vector>>` becomes `Vector[][]`).
|
||||||
fn rewrite_list_as_array_brackets(input: &str) -> String {
|
/// Respects word boundaries so unrelated identifiers that happen to end in `List` or `Item` are not affected.
|
||||||
|
fn rewrite_ranked_type_wrappers(input: &str) -> String {
|
||||||
let bytes = input.as_bytes();
|
let bytes = input.as_bytes();
|
||||||
let mut result = String::with_capacity(input.len());
|
let mut result = String::with_capacity(input.len());
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -387,12 +541,31 @@ fn rewrite_list_as_array_brackets(input: &str) -> String {
|
|||||||
let inner_start = i + b"List<".len();
|
let inner_start = i + b"List<".len();
|
||||||
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
|
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
|
||||||
let inner = &input[inner_start..close];
|
let inner = &input[inner_start..close];
|
||||||
result.push_str(&rewrite_list_as_array_brackets(inner));
|
result.push_str(&rewrite_ranked_type_wrappers(inner));
|
||||||
result.push_str("[]");
|
result.push_str("[]");
|
||||||
i = close + 1;
|
i = close + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if at_word_boundary && bytes[i..].starts_with(b"Bundle<") {
|
||||||
|
let inner_start = i + b"Bundle<".len();
|
||||||
|
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
|
||||||
|
let inner = &input[inner_start..close];
|
||||||
|
result.push_str(&rewrite_ranked_type_wrappers(inner));
|
||||||
|
result.push_str("[]");
|
||||||
|
i = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if at_word_boundary && bytes[i..].starts_with(b"Item<") {
|
||||||
|
let inner_start = i + b"Item<".len();
|
||||||
|
if let Some(close) = find_matching_angle_bracket(bytes, inner_start) {
|
||||||
|
let inner = &input[inner_start..close];
|
||||||
|
result.push_str(&rewrite_ranked_type_wrappers(inner));
|
||||||
|
i = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if bytes[i].is_ascii() {
|
if bytes[i].is_ascii() {
|
||||||
result.push(bytes[i] as char);
|
result.push(bytes[i] as char);
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -441,6 +614,8 @@ impl std::fmt::Display for Type {
|
|||||||
Type::Concrete(ty) => write!(f, "{ty}"),
|
Type::Concrete(ty) => write!(f, "{ty}"),
|
||||||
Type::Fn(_, return_value) => write!(f, "{return_value}"),
|
Type::Fn(_, return_value) => write!(f, "{return_value}"),
|
||||||
Type::Future(ty) => write!(f, "{ty}"),
|
Type::Future(ty) => write!(f, "{ty}"),
|
||||||
|
Type::Item(element) => write!(f, "{element}"),
|
||||||
|
Type::List(element) => write!(f, "{element}[]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::Node;
|
use crate::list::Item;
|
||||||
|
use crate::registry::DynFuture;
|
||||||
|
use crate::{Node, WasmNotSend};
|
||||||
use std::cell::{Cell, RefCell, RefMut};
|
use std::cell::{Cell, RefCell, RefMut};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
@@ -110,6 +112,26 @@ impl<T: Clone> From<T> for ClonedNode<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yields a precomputed `Item<T>` as a ready future, ignoring its input context.
|
||||||
|
/// Generated list-content variants feed each already-evaluated content slot through this so the kernel's own
|
||||||
|
/// context modifications become no-ops (the slot was evaluated once, up front, at the ambient footprint).
|
||||||
|
pub struct PrecomputedItemNode<T>(pub Item<T>);
|
||||||
|
|
||||||
|
impl<'i, T: Clone + WasmNotSend + 'i, I: 'i> Node<'i, I> for PrecomputedItemNode<T> {
|
||||||
|
type Output = DynFuture<'i, Item<T>>;
|
||||||
|
#[inline(always)]
|
||||||
|
fn eval(&'i self, _input: I) -> Self::Output {
|
||||||
|
let item = self.0.clone();
|
||||||
|
Box::pin(async move { item })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PrecomputedItemNode<T> {
|
||||||
|
pub const fn new(item: Item<T>) -> Self {
|
||||||
|
Self(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
/// The DebugClonedNode logs every time it is evaluated.
|
/// The DebugClonedNode logs every time it is evaluated.
|
||||||
/// This is useful for debugging.
|
/// This is useful for debugging.
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ impl_via_hash! {
|
|||||||
bool, char,
|
bool, char,
|
||||||
u8, u16, u32, u64, u128, usize,
|
u8, u16, u32, u64, u128, usize,
|
||||||
i8, i16, i32, i64, i128, isize,
|
i8, i16, i32, i64, i128, isize,
|
||||||
|
core::time::Duration,
|
||||||
// glam integer vector types have Hash
|
// glam integer vector types have Hash
|
||||||
glam::UVec2, glam::UVec3, glam::UVec4,
|
glam::UVec2, glam::UVec3, glam::UVec4,
|
||||||
glam::IVec2, glam::IVec3, glam::IVec4,
|
glam::IVec2, glam::IVec3, glam::IVec4,
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::graphene_hash::CacheHash;
|
use core_types::graphene_hash::CacheHash;
|
||||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, ItemAttributeValues, List};
|
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, NodeIdPath};
|
||||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
use core_types::ops::FromAnchorPosition;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
pub use vector_types::Vector;
|
pub use vector_types::Vector;
|
||||||
|
|
||||||
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
||||||
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
|
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
|
||||||
pub enum Graphic {
|
pub enum Graphic {
|
||||||
|
/// The absence of graphical content, like CSS's `none` keyword: painting it produces nothing.
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
Graphic(List<Graphic>),
|
Graphic(List<Graphic>),
|
||||||
Vector(List<Vector>),
|
Vector(List<Vector>),
|
||||||
RasterCPU(List<Raster<CPU>>),
|
RasterCPU(List<Raster<CPU>>),
|
||||||
RasterGPU(List<Raster<GPU>>),
|
RasterGPU(List<Raster<GPU>>),
|
||||||
Color(List<Color>),
|
Color(List<Color>),
|
||||||
Gradient(List<GradientStops>),
|
Gradient(List<Gradient>),
|
||||||
Text(List<String>),
|
Text(List<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Graphic {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Graphic(List::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Graphic
|
// Graphic
|
||||||
impl From<List<Graphic>> for Graphic {
|
impl From<List<Graphic>> for Graphic {
|
||||||
fn from(graphic: List<Graphic>) -> Self {
|
fn from(graphic: List<Graphic>) -> Self {
|
||||||
@@ -91,14 +87,14 @@ impl From<List<Color>> for Graphic {
|
|||||||
// Note: List conversions handled by blanket impl in gcore
|
// Note: List conversions handled by blanket impl in gcore
|
||||||
// Note: List<Color> -> Option<Color> is in gcore (Color is defined there)
|
// Note: List<Color> -> Option<Color> is in gcore (Color is defined there)
|
||||||
|
|
||||||
// GradientStops
|
// Gradient
|
||||||
impl From<GradientStops> for Graphic {
|
impl From<Gradient> for Graphic {
|
||||||
fn from(gradient: GradientStops) -> Self {
|
fn from(gradient: Gradient) -> Self {
|
||||||
Graphic::Gradient(List::new_from_element(gradient))
|
Graphic::Gradient(List::new_from_element(gradient))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<List<GradientStops>> for Graphic {
|
impl From<List<Gradient>> for Graphic {
|
||||||
fn from(gradient: List<GradientStops>) -> Self {
|
fn from(gradient: List<Gradient>) -> Self {
|
||||||
Graphic::Gradient(gradient)
|
Graphic::Gradient(gradient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,9 +122,9 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
||||||
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
||||||
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
||||||
let parent_has_layer_path = current_graphic_item.attribute::<List<NodeId>>(ATTR_EDITOR_LAYER_PATH).is_some();
|
let parent_has_layer_path = current_graphic_item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).is_some();
|
||||||
|
|
||||||
let layer_path: List<NodeId> = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
|
let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
|
||||||
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
|
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
|
||||||
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
||||||
@@ -232,6 +228,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
|
|||||||
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
|
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
|
||||||
for graphic in graphics.iter_element_values_mut() {
|
for graphic in graphics.iter_element_values_mut() {
|
||||||
match graphic {
|
match graphic {
|
||||||
|
Graphic::None => {}
|
||||||
Graphic::Graphic(list) => bake_list_transform(list, transform),
|
Graphic::Graphic(list) => bake_list_transform(list, transform),
|
||||||
Graphic::Vector(list) => bake_list_transform(list, transform),
|
Graphic::Vector(list) => bake_list_transform(list, transform),
|
||||||
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
|
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
|
||||||
@@ -274,7 +271,7 @@ impl TryFromGraphic for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for GradientStops {
|
impl TryFromGraphic for Gradient {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::Gradient(t) = graphic { Some(t) } else { None }
|
if let Graphic::Gradient(t) = graphic { Some(t) } else { None }
|
||||||
}
|
}
|
||||||
@@ -307,11 +304,11 @@ impl IntoGraphicList for List<Graphic> {
|
|||||||
|
|
||||||
impl IntoGraphicList for List<Vector> {
|
impl IntoGraphicList for List<Vector> {
|
||||||
fn into_graphic_list(self) -> List<Graphic> {
|
fn into_graphic_list(self) -> List<Graphic> {
|
||||||
// Propagate `editor:layer_path` from item 0 onto the wrapper Graphic item so a subsequent
|
// Propagate the `editor:layer_path` column (if present) from item 0 onto the wrapper Graphic item so a
|
||||||
// `flatten_graphic_list` doesn't overwrite the inner Vector's stamp with an empty value
|
// subsequent `flatten_graphic_list` doesn't drop the inner Vector's layer stamp
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0);
|
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned();
|
||||||
let mut graphic_list = List::new_from_element(Graphic::Vector(self));
|
let mut graphic_list = List::new_from_element(Graphic::Vector(self));
|
||||||
if !layer_path.is_empty() {
|
if let Some(layer_path) = layer_path {
|
||||||
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
||||||
}
|
}
|
||||||
graphic_list
|
graphic_list
|
||||||
@@ -336,7 +333,7 @@ impl IntoGraphicList for List<Color> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoGraphicList for List<GradientStops> {
|
impl IntoGraphicList for List<Gradient> {
|
||||||
fn into_graphic_list(self) -> List<Graphic> {
|
fn into_graphic_list(self) -> List<Graphic> {
|
||||||
List::new_from_element(Graphic::Gradient(self))
|
List::new_from_element(Graphic::Gradient(self))
|
||||||
}
|
}
|
||||||
@@ -344,32 +341,32 @@ impl IntoGraphicList for List<GradientStops> {
|
|||||||
|
|
||||||
impl IntoGraphicList for List<String> {
|
impl IntoGraphicList for List<String> {
|
||||||
fn into_graphic_list(self) -> List<Graphic> {
|
fn into_graphic_list(self) -> List<Graphic> {
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0);
|
let layer_path = self.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).cloned();
|
||||||
let mut graphic_list = List::new_from_element(Graphic::Text(self));
|
let mut graphic_list = List::new_from_element(Graphic::Text(self));
|
||||||
if !layer_path.is_empty() {
|
if let Some(layer_path) = layer_path {
|
||||||
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
||||||
}
|
}
|
||||||
graphic_list
|
graphic_list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoGraphicList for DAffine2 {
|
impl IntoGraphicList for Item<DAffine2> {
|
||||||
fn into_graphic_list(self) -> List<Graphic> {
|
fn into_graphic_list(self) -> List<Graphic> {
|
||||||
List::new_from_element(Graphic::default())
|
List::new_from_element(Graphic::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DAffine2
|
// DAffine2
|
||||||
impl From<DAffine2> for Graphic {
|
impl From<Item<DAffine2>> for Graphic {
|
||||||
fn from(_: DAffine2) -> Self {
|
fn from(_: Item<DAffine2>) -> Self {
|
||||||
Graphic::default()
|
Graphic::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DVec2
|
// DVec2
|
||||||
impl From<DVec2> for Graphic {
|
impl From<Item<DVec2>> for Graphic {
|
||||||
fn from(position: DVec2) -> Self {
|
fn from(position: Item<DVec2>) -> Self {
|
||||||
Graphic::Vector(List::new_from_element(Vector::from_anchor_position(position)))
|
Graphic::Vector(List::new_from_element(Vector::from_anchor_position(position.into_element())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Note: List conversions handled by blanket impl in gcore
|
// Note: List conversions handled by blanket impl in gcore
|
||||||
@@ -423,6 +420,7 @@ impl Graphic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => true,
|
||||||
Graphic::Vector(list) => all_clipped(list),
|
Graphic::Vector(list) => all_clipped(list),
|
||||||
Graphic::Graphic(list) => all_clipped(list),
|
Graphic::Graphic(list) => all_clipped(list),
|
||||||
Graphic::RasterCPU(list) => all_clipped(list),
|
Graphic::RasterCPU(list) => all_clipped(list),
|
||||||
@@ -452,6 +450,7 @@ impl Graphic {
|
|||||||
|
|
||||||
pub fn is_opaque(&self) -> bool {
|
pub fn is_opaque(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => false,
|
||||||
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
||||||
Graphic::Vector(list) => {
|
Graphic::Vector(list) => {
|
||||||
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
|
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
|
||||||
@@ -474,6 +473,7 @@ impl Graphic {
|
|||||||
|
|
||||||
pub fn is_fully_transparent(&self) -> bool {
|
pub fn is_fully_transparent(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => true,
|
||||||
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
||||||
Graphic::Vector(list) => (0..list.len()).all(|i| {
|
Graphic::Vector(list) => (0..list.len()).all(|i| {
|
||||||
let Some(vector) = list.element(i) else { return false };
|
let Some(vector) = list.element(i) else { return false };
|
||||||
@@ -501,9 +501,10 @@ impl Graphic {
|
|||||||
matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque()
|
matches!(self, Graphic::Color(_) | Graphic::Gradient(_)) && self.is_opaque()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this graphic's inner list is empty.
|
/// Returns true if this graphic contains no content.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => true,
|
||||||
Graphic::Graphic(list) => list.is_empty(),
|
Graphic::Graphic(list) => list.is_empty(),
|
||||||
Graphic::Vector(list) => list.is_empty(),
|
Graphic::Vector(list) => list.is_empty(),
|
||||||
Graphic::Color(list) => list.is_empty(),
|
Graphic::Color(list) => list.is_empty(),
|
||||||
@@ -518,6 +519,7 @@ impl Graphic {
|
|||||||
impl BoundingBox for Graphic {
|
impl BoundingBox for Graphic {
|
||||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => RenderBoundingBox::None,
|
||||||
Graphic::Vector(list) => list.bounding_box(transform, include_stroke),
|
Graphic::Vector(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke),
|
||||||
@@ -530,6 +532,7 @@ impl BoundingBox for Graphic {
|
|||||||
|
|
||||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => RenderBoundingBox::None,
|
||||||
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
@@ -541,25 +544,10 @@ impl BoundingBox for Graphic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListConvert<Graphic> for Vector {
|
|
||||||
fn convert_item(self) -> Graphic {
|
|
||||||
Graphic::Vector(List::new_from_element(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ListConvert<Graphic> for Raster<CPU> {
|
|
||||||
fn convert_item(self) -> Graphic {
|
|
||||||
Graphic::RasterCPU(List::new_from_element(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ListConvert<Graphic> for Raster<GPU> {
|
|
||||||
fn convert_item(self) -> Graphic {
|
|
||||||
Graphic::RasterGPU(List::new_from_element(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderComplexity for Graphic {
|
impl RenderComplexity for Graphic {
|
||||||
fn render_complexity(&self) -> usize {
|
fn render_complexity(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
|
Self::None => 0,
|
||||||
Self::Graphic(list) => list.render_complexity(),
|
Self::Graphic(list) => list.render_complexity(),
|
||||||
Self::Vector(list) => list.render_complexity(),
|
Self::Vector(list) => list.render_complexity(),
|
||||||
Self::RasterCPU(list) => list.render_complexity(),
|
Self::RasterCPU(list) => list.render_complexity(),
|
||||||
@@ -688,7 +676,7 @@ mod graphic_is_opaque_tests {
|
|||||||
Graphic::Color(List::new_from_element(color))
|
Graphic::Color(List::new_from_element(color))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradient_graphic(gradient: GradientStops) -> Graphic {
|
fn gradient_graphic(gradient: Gradient) -> Graphic {
|
||||||
let mut gradient_list = List::new_from_element(gradient);
|
let mut gradient_list = List::new_from_element(gradient);
|
||||||
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad);
|
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad);
|
||||||
Graphic::Gradient(gradient_list)
|
Graphic::Gradient(gradient_list)
|
||||||
@@ -716,7 +704,7 @@ mod graphic_is_opaque_tests {
|
|||||||
fn gradient_with_all_opaque_stops_is_opaque() {
|
fn gradient_with_all_opaque_stops_is_opaque() {
|
||||||
let color_1 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
let color_1 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||||
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||||
let gradient = GradientStops::new(vec![
|
let gradient = Gradient::new(vec![
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -736,7 +724,7 @@ mod graphic_is_opaque_tests {
|
|||||||
fn gradient_with_transparent_stop_is_not_opaque() {
|
fn gradient_with_transparent_stop_is_not_opaque() {
|
||||||
let color_1 = Color::from_rgbaf32(1., 0., 0., 0.5).unwrap();
|
let color_1 = Color::from_rgbaf32(1., 0., 0., 0.5).unwrap();
|
||||||
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
let color_2 = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||||
let gradient = GradientStops::new(vec![
|
let gradient = Gradient::new(vec![
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ pub mod migrations {
|
|||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke};
|
use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke};
|
||||||
use vector_types::{GradientStops, Vector, vector};
|
use vector_types::{Gradient, Vector, vector};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Gradient {
|
pub struct LegacyGradient {
|
||||||
pub stops: GradientStops,
|
pub stops: Gradient,
|
||||||
pub gradient_type: vector::style::GradientType,
|
pub gradient_type: vector::style::GradientType,
|
||||||
pub start: DVec2,
|
pub start: DVec2,
|
||||||
pub end: DVec2,
|
pub end: DVec2,
|
||||||
@@ -36,11 +36,11 @@ pub mod migrations {
|
|||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gradient {
|
impl LegacyGradient {
|
||||||
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
|
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
|
||||||
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
|
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
|
||||||
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
|
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
|
||||||
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
|
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> LegacyGradient {
|
||||||
let start = bounding_box.transform_point2(self.start);
|
let start = bounding_box.transform_point2(self.start);
|
||||||
let end = bounding_box.transform_point2(self.end);
|
let end = bounding_box.transform_point2(self.end);
|
||||||
let direction = end - start;
|
let direction = end - start;
|
||||||
@@ -63,7 +63,7 @@ pub mod migrations {
|
|||||||
DAffine2::IDENTITY
|
DAffine2::IDENTITY
|
||||||
};
|
};
|
||||||
|
|
||||||
Gradient {
|
LegacyGradient {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transform,
|
transform,
|
||||||
@@ -80,15 +80,15 @@ pub mod migrations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum Fill {
|
pub enum LegacyFill {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
Solid(Color),
|
Solid(Color),
|
||||||
Gradient(Gradient),
|
Gradient(LegacyGradient),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
||||||
/// recovers editable vector data. The fill/stroke paints are migrated from the the node inputs.
|
/// recovers editable vector data. The fill/stroke paints are migrated from the node inputs.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
||||||
pub(super) struct PathStyle {
|
pub(super) struct PathStyle {
|
||||||
@@ -162,7 +162,7 @@ pub mod migrations {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
|
.insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap());
|
||||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
||||||
|
|
||||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod blending;
|
|||||||
pub mod choice_type;
|
pub mod choice_type;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
pub mod list;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod shaders;
|
pub mod shaders;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//! A zero-cost stand-in for `core_types::list::Item` used when node kernels are compiled for the GPU.
|
||||||
|
//!
|
||||||
|
//! Shader node kernels compile twice: under `std` against the real attribute-carrying `Item`, and under
|
||||||
|
//! `no_std` (SPIR-V) against this transparent wrapper, imported as `Item`. Only the element-access surface is
|
||||||
|
//! provided, since rust-gpu cannot allocate and attributes have no per-pixel meaning; attribute use fails the
|
||||||
|
//! shader build. It is named distinctly from `Item` so a search for the canonical type finds only that one.
|
||||||
|
|
||||||
|
/// A rank-0 wire value holding a single element, mirroring the element-access API of the real `Item`.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct ShaderItem<T> {
|
||||||
|
element: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ShaderItem<T> {
|
||||||
|
/// Constructs an item with the given element.
|
||||||
|
pub fn new_from_element(element: T) -> Self {
|
||||||
|
Self { element }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a shared reference to this item's element.
|
||||||
|
pub fn element(&self) -> &T {
|
||||||
|
&self.element
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to this item's element.
|
||||||
|
pub fn element_mut(&mut self) -> &mut T {
|
||||||
|
&mut self.element
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this item and returns the owned element.
|
||||||
|
pub fn into_element(self) -> T {
|
||||||
|
self.element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for ShaderItem<T> {
|
||||||
|
fn from(element: T) -> Self {
|
||||||
|
Self::new_from_element(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use graphic_types::Graphic;
|
|||||||
use graphic_types::vector_types::gradient::GradientType;
|
use graphic_types::vector_types::gradient::GradientType;
|
||||||
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
use vector_types::gradient::GradientSpreadMethod;
|
use vector_types::gradient::GradientSpreadMethod;
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
@@ -76,7 +76,7 @@ impl RenderExt for List<Color> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderExt for List<GradientStops> {
|
impl RenderExt for List<Gradient> {
|
||||||
type Output = u64;
|
type Output = u64;
|
||||||
|
|
||||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||||
@@ -241,6 +241,7 @@ impl RenderExt for List<Graphic> {
|
|||||||
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target);
|
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target);
|
||||||
format!(r##" {paint_attr}="url(#{gradient_id})""##)
|
format!(r##" {paint_attr}="url(#{gradient_id})""##)
|
||||||
}
|
}
|
||||||
|
Some(Graphic::None) => format!(r#" {paint_attr}="none""#),
|
||||||
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => {
|
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => {
|
||||||
let bounds = if target == PaintTarget::Stroke {
|
let bounds = if target == PaintTarget::Stroke {
|
||||||
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
|
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use core_types::bounds::RenderBoundingBox;
|
|||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::consts::DEFAULT_FONT_SIZE;
|
use core_types::consts::DEFAULT_FONT_SIZE;
|
||||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
|
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, NodeIdPath};
|
||||||
use core_types::math::quad::Quad;
|
use core_types::math::quad::Quad;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
@@ -23,7 +23,7 @@ use graphene_hash::CacheHashWrapper;
|
|||||||
use graphene_resource::Resource;
|
use graphene_resource::Resource;
|
||||||
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
|
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
|
||||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
||||||
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
use graphic_types::vector_types::gradient::{Gradient, GradientType};
|
||||||
use graphic_types::vector_types::subpath::Subpath;
|
use graphic_types::vector_types::subpath::Subpath;
|
||||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||||
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
@@ -394,7 +394,7 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
fn create_peniko_gradient_brush(gradient_list: &List<Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
||||||
let stops = gradient_list.element(0)?;
|
let stops = gradient_list.element(0)?;
|
||||||
|
|
||||||
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||||
@@ -546,6 +546,7 @@ pub trait Render: BoundingBox + RenderComplexity {
|
|||||||
impl Render for Graphic {
|
impl Render for Graphic {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.render_svg(render, render_params),
|
Graphic::Graphic(list) => list.render_svg(render, render_params),
|
||||||
Graphic::Vector(list) => list.render_svg(render, render_params),
|
Graphic::Vector(list) => list.render_svg(render, render_params),
|
||||||
Graphic::RasterCPU(list) => list.render_svg(render, render_params),
|
Graphic::RasterCPU(list) => list.render_svg(render, render_params),
|
||||||
@@ -558,6 +559,7 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params),
|
Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||||
Graphic::Vector(list) => list.render_to_vello(scene, transform, context, render_params),
|
Graphic::Vector(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||||
Graphic::RasterCPU(list) => list.render_to_vello(scene, transform, context, render_params),
|
Graphic::RasterCPU(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||||
@@ -571,6 +573,7 @@ impl Render for Graphic {
|
|||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
||||||
if let Some(element_id) = element_id {
|
if let Some(element_id) = element_id {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => {}
|
||||||
Graphic::Graphic(_) => {
|
Graphic::Graphic(_) => {
|
||||||
metadata.upstream_footprints.insert(element_id, footprint);
|
metadata.upstream_footprints.insert(element_id, footprint);
|
||||||
}
|
}
|
||||||
@@ -578,7 +581,7 @@ impl Render for Graphic {
|
|||||||
metadata.upstream_footprints.insert(element_id, footprint);
|
metadata.upstream_footprints.insert(element_id, footprint);
|
||||||
// TODO: Find a way to handle more than the first item
|
// TODO: Find a way to handle more than the first item
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
let layer_path: List<NodeId> = list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0);
|
let layer_path: List<NodeId> = list.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
|
|
||||||
@@ -630,6 +633,7 @@ impl Render for Graphic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Graphic(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||||
Graphic::Vector(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Vector(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||||
Graphic::RasterCPU(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::RasterCPU(list) => list.collect_metadata(metadata, footprint, element_id),
|
||||||
@@ -642,6 +646,7 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Graphic(list) => list.add_upstream_click_targets(click_targets),
|
||||||
Graphic::Vector(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Vector(list) => list.add_upstream_click_targets(click_targets),
|
||||||
Graphic::RasterCPU(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::RasterCPU(list) => list.add_upstream_click_targets(click_targets),
|
||||||
@@ -654,6 +659,7 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Graphic(list) => list.add_upstream_outline_targets(outlines),
|
||||||
Graphic::Vector(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Vector(list) => list.add_upstream_outline_targets(outlines),
|
||||||
Graphic::RasterCPU(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::RasterCPU(list) => list.add_upstream_outline_targets(outlines),
|
||||||
@@ -666,6 +672,7 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
fn contains_artboard(&self) -> bool {
|
fn contains_artboard(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => false,
|
||||||
Graphic::Graphic(list) => list.contains_artboard(),
|
Graphic::Graphic(list) => list.contains_artboard(),
|
||||||
Graphic::Vector(list) => list.contains_artboard(),
|
Graphic::Vector(list) => list.contains_artboard(),
|
||||||
Graphic::RasterCPU(list) => list.contains_artboard(),
|
Graphic::RasterCPU(list) => list.contains_artboard(),
|
||||||
@@ -678,6 +685,7 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
|
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
|
||||||
match self {
|
match self {
|
||||||
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.new_ids_from_hash(reference),
|
Graphic::Graphic(list) => list.new_ids_from_hash(reference),
|
||||||
Graphic::Vector(list) => list.new_ids_from_hash(reference),
|
Graphic::Vector(list) => list.new_ids_from_hash(reference),
|
||||||
Graphic::RasterCPU(_) => (),
|
Graphic::RasterCPU(_) => (),
|
||||||
@@ -792,7 +800,7 @@ impl Render for List<Artboard> {
|
|||||||
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
|
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
|
||||||
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
|
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
|
||||||
|
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let element_id = layer_path.iter_element_values().next_back().copied();
|
let element_id = layer_path.iter_element_values().next_back().copied();
|
||||||
|
|
||||||
if let Some(element_id) = element_id {
|
if let Some(element_id) = element_id {
|
||||||
@@ -964,7 +972,7 @@ impl Render for List<Graphic> {
|
|||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
|
||||||
@@ -1046,9 +1054,9 @@ impl Render for List<Graphic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
|
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
|
||||||
let (elements, layers) = self.element_and_attribute_slices_mut::<List<NodeId>>(ATTR_EDITOR_LAYER_PATH);
|
let (elements, layers) = self.element_and_attribute_slices_mut::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH);
|
||||||
for (element, layer) in elements.iter_mut().zip(layers.iter()) {
|
for (element, layer) in elements.iter_mut().zip(layers.iter()) {
|
||||||
element.new_ids_from_hash(layer.iter_element_values().next_back().copied());
|
element.new_ids_from_hash(layer.0.iter_element_values().next_back().copied());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1348,6 +1356,7 @@ impl Render for List<Vector> {
|
|||||||
for paint_index in 0..fill_graphic.len() {
|
for paint_index in 0..fill_graphic.len() {
|
||||||
let Some(paint) = fill_graphic.element(paint_index) else { continue };
|
let Some(paint) = fill_graphic.element(paint_index) else { continue };
|
||||||
match paint {
|
match paint {
|
||||||
|
Graphic::None => continue,
|
||||||
Graphic::Color(list) => {
|
Graphic::Color(list) => {
|
||||||
let Some(color) = list.element(0) else { continue };
|
let Some(color) = list.element(0) else { continue };
|
||||||
|
|
||||||
@@ -1430,6 +1439,7 @@ impl Render for List<Vector> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match stroke_graphic {
|
match stroke_graphic {
|
||||||
|
Graphic::None => continue,
|
||||||
Graphic::Color(list) => {
|
Graphic::Color(list) => {
|
||||||
let Some(color) = list.element(0) else { continue };
|
let Some(color) = list.element(0) else { continue };
|
||||||
let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
|
let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
|
||||||
@@ -1546,7 +1556,7 @@ impl Render for List<Vector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
fn collect_metadata(&self, 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 !self.is_empty() {
|
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
||||||
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
||||||
@@ -1565,7 +1575,7 @@ impl Render for List<Vector> {
|
|||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some(source) = self.element(index) else { continue };
|
let Some(source) = self.element(index) else { continue };
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
|
|
||||||
if let Some(element_id) = caller_element_id.or(layer) {
|
if let Some(element_id) = caller_element_id.or(layer) {
|
||||||
@@ -2047,7 +2057,7 @@ impl Render for List<Color> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for List<GradientStops> {
|
impl Render for List<Gradient> {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
||||||
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
|
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
|
||||||
@@ -2562,7 +2572,7 @@ impl Render for List<String> {
|
|||||||
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
|
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
|
||||||
|
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
let Some(element_id) = caller_element_id.or(layer) else { continue };
|
let Some(element_id) = caller_element_id.or(layer) else { continue };
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ pub enum GradientType {
|
|||||||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||||
/// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient.
|
/// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient.
|
||||||
///
|
///
|
||||||
/// Not exposed via Tsify; use [`GradientStopsUI`] at the JS boundary.
|
/// Not exposed via Tsify; use [`GradientUI`] at the JS boundary.
|
||||||
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub struct GradientStops {
|
pub struct Gradient {
|
||||||
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
|
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
|
||||||
pub position: Vec<f64>,
|
pub position: Vec<f64>,
|
||||||
/// The midpoint to the right of this stop, a factor from 0-1 along the distance to the next stop. The final stop's midpoint is ignored.
|
/// The midpoint to the right of this stop, a factor from 0-1 along the distance to the next stop. The final stop's midpoint is ignored.
|
||||||
@@ -29,18 +29,18 @@ pub struct GradientStops {
|
|||||||
pub color: Vec<Color>,
|
pub color: Vec<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JS-boundary version of [`GradientStops`] where stop colors are [`SRGBA8`] byte triples instead of linear-light [`Color`].
|
/// JS-boundary version of [`Gradient`] where stop colors are [`SRGBA8`] byte triples instead of linear-light [`Color`].
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(Debug, Clone, PartialEq, Default, DynAny)]
|
#[derive(Debug, Clone, PartialEq, Default, DynAny)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct GradientStopsUI {
|
pub struct GradientUI {
|
||||||
pub position: Vec<f64>,
|
pub position: Vec<f64>,
|
||||||
pub midpoint: Vec<f64>,
|
pub midpoint: Vec<f64>,
|
||||||
pub color: Vec<SRGBA8>,
|
pub color: Vec<SRGBA8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&GradientStops> for GradientStopsUI {
|
impl From<&Gradient> for GradientUI {
|
||||||
fn from(s: &GradientStops) -> Self {
|
fn from(s: &Gradient) -> Self {
|
||||||
Self {
|
Self {
|
||||||
position: s.position.clone(),
|
position: s.position.clone(),
|
||||||
midpoint: s.midpoint.clone(),
|
midpoint: s.midpoint.clone(),
|
||||||
@@ -49,8 +49,8 @@ impl From<&GradientStops> for GradientStopsUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&GradientStopsUI> for GradientStops {
|
impl From<&GradientUI> for Gradient {
|
||||||
fn from(s: &GradientStopsUI) -> Self {
|
fn from(s: &GradientUI) -> Self {
|
||||||
Self {
|
Self {
|
||||||
position: s.position.clone(),
|
position: s.position.clone(),
|
||||||
midpoint: s.midpoint.clone(),
|
midpoint: s.midpoint.clone(),
|
||||||
@@ -59,7 +59,7 @@ impl From<&GradientStopsUI> for GradientStops {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientStopsUI {
|
impl GradientUI {
|
||||||
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
||||||
pub fn to_css_linear_gradient(&self) -> String {
|
pub fn to_css_linear_gradient(&self) -> String {
|
||||||
if self.position.len() <= 1 {
|
if self.position.len() <= 1 {
|
||||||
@@ -67,7 +67,7 @@ impl GradientStopsUI {
|
|||||||
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
||||||
}
|
}
|
||||||
// Sample via the midpoint-aware subdivision used for SVG/Vello stops so browser interpolation matches
|
// Sample via the midpoint-aware subdivision used for SVG/Vello stops so browser interpolation matches
|
||||||
let stops: GradientStops = self.into();
|
let stops: Gradient = self.into();
|
||||||
let pieces = stops
|
let pieces = stops
|
||||||
.interpolated_samples()
|
.interpolated_samples()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -83,7 +83,7 @@ impl GradientStopsUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
impl<'de> serde::Deserialize<'de> for GradientStops {
|
impl<'de> serde::Deserialize<'de> for Gradient {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct NewFormat {
|
struct NewFormat {
|
||||||
@@ -117,7 +117,7 @@ impl<'de> serde::Deserialize<'de> for GradientStops {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GradientStops {
|
impl Default for Gradient {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
position: vec![0., 1.],
|
position: vec![0., 1.],
|
||||||
@@ -127,7 +127,7 @@ impl Default for GradientStops {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderComplexity for GradientStops {
|
impl RenderComplexity for Gradient {
|
||||||
fn render_complexity(&self) -> usize {
|
fn render_complexity(&self) -> usize {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ pub struct GradientStop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct GradientStopsIter<'a> {
|
pub struct GradientStopsIter<'a> {
|
||||||
stops: &'a GradientStops,
|
stops: &'a Gradient,
|
||||||
index: usize,
|
index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ impl<'a> Iterator for GradientStopsIter<'a> {
|
|||||||
|
|
||||||
impl ExactSizeIterator for GradientStopsIter<'_> {}
|
impl ExactSizeIterator for GradientStopsIter<'_> {}
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a GradientStops {
|
impl<'a> IntoIterator for &'a Gradient {
|
||||||
type Item = GradientStop;
|
type Item = GradientStop;
|
||||||
type IntoIter = GradientStopsIter<'a>;
|
type IntoIter = GradientStopsIter<'a>;
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ impl<'a> IntoIterator for &'a GradientStops {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for GradientStops {
|
impl IntoIterator for Gradient {
|
||||||
type Item = GradientStop;
|
type Item = GradientStop;
|
||||||
type IntoIter = std::vec::IntoIter<GradientStop>;
|
type IntoIter = std::vec::IntoIter<GradientStop>;
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ impl IntoIterator for GradientStops {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientStops {
|
impl Gradient {
|
||||||
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||||
let mut position = Vec::new();
|
let mut position = Vec::new();
|
||||||
let mut midpoint = Vec::new();
|
let mut midpoint = Vec::new();
|
||||||
@@ -465,7 +465,7 @@ impl GradientStops {
|
|||||||
let color = a.color.lerp(&b.color, time as f32);
|
let color = a.color.lerp(&b.color, time as f32);
|
||||||
GradientStop { position, midpoint: 0.5, color }
|
GradientStop { position, midpoint: 0.5, color }
|
||||||
});
|
});
|
||||||
GradientStops::new(stops)
|
Gradient::new(stops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,19 +540,19 @@ pub fn initial_gradient_transform_for_bounding_box(bounds: [DVec2; 2]) -> DAffin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
pub fn migrate_to_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientStops, D::Error> {
|
pub fn migrate_to_gradient<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Gradient, D::Error> {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct LegacyTable {
|
struct LegacyTable {
|
||||||
#[serde(alias = "instances", alias = "instance")]
|
#[serde(alias = "instances", alias = "instance")]
|
||||||
element: Vec<GradientStops>,
|
element: Vec<Gradient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
enum GradientStopsFormat {
|
enum GradientStopsFormat {
|
||||||
Stops(GradientStops),
|
Stops(Gradient),
|
||||||
List(LegacyTable),
|
List(LegacyTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +562,7 @@ pub fn migrate_to_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl core_types::bounds::BoundingBox for GradientStops {
|
impl core_types::bounds::BoundingBox for Gradient {
|
||||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||||
core_types::bounds::RenderBoundingBox::Infinite
|
core_types::bounds::RenderBoundingBox::Infinite
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub mod vector;
|
|||||||
|
|
||||||
// Re-export commonly used types at the crate root
|
// Re-export commonly used types at the crate root
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType};
|
pub use gradient::{Gradient, GradientSpreadMethod, GradientStop, GradientType};
|
||||||
pub use math::{QuadExt, RectExt};
|
pub use math::{QuadExt, RectExt};
|
||||||
pub use subpath::Subpath;
|
pub use subpath::Subpath;
|
||||||
pub use vector::Vector;
|
pub use vector::Vector;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl MergeByDistanceExt for Vector {
|
|||||||
// Collect points and segments to delete at the end to avoid invalidating indices
|
// Collect points and segments to delete at the end to avoid invalidating indices
|
||||||
let mut points_to_delete = FxHashSet::default();
|
let mut points_to_delete = FxHashSet::default();
|
||||||
let mut segments_to_delete = FxHashSet::default();
|
let mut segments_to_delete = FxHashSet::default();
|
||||||
for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position.into_iter()) {
|
for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position) {
|
||||||
// Remove any segments where both endpoints are in the collapse set
|
// Remove any segments where both endpoints are in the collapse set
|
||||||
segments_to_delete.extend(self.segment_domain.iter().filter_map(|(id, start_offset, end_offset, _)| {
|
segments_to_delete.extend(self.segment_domain.iter().filter_map(|(id, start_offset, end_offset, _)| {
|
||||||
let start = self.point_domain.ids()[start_offset];
|
let start = self.point_domain.ids()[start_offset];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::PointId;
|
|||||||
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||||
use crate::subpath::{BezierHandles, ManipulatorGroup};
|
use crate::subpath::{BezierHandles, ManipulatorGroup};
|
||||||
use crate::vector::{SegmentId, Vector};
|
use crate::vector::{SegmentId, Vector};
|
||||||
|
use core_types::list::{Item, List};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
|
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
|
||||||
@@ -49,6 +50,77 @@ pub enum RowsOrColumns {
|
|||||||
Columns,
|
Columns,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A box's four corner values, such as a rectangle's corner radii, expanded on read from any number of stored
|
||||||
|
/// values by the CSS `border-radius` shorthand rules.
|
||||||
|
///
|
||||||
|
/// Wraps a `List<f64>` so the Data panel can introspect its values, mirroring how `DashPattern` wraps its lengths,
|
||||||
|
/// while remaining a single rank-0 value on the wire.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
|
pub struct BoxCorners(pub List<f64>);
|
||||||
|
|
||||||
|
impl BoxCorners {
|
||||||
|
/// Expands the stored values to the four corners, clockwise from the top-left, by the CSS `border-radius` shorthand rules.
|
||||||
|
/// - `[]` → `[0, 0, 0, 0]`
|
||||||
|
/// - `[a]` → `[a, a, a, a]`
|
||||||
|
/// - `[a, b]` → `[a, b, a, b]`
|
||||||
|
/// - `[a, b, c]` → `[a, b, c, b]`
|
||||||
|
/// - `[a, b, c, d, …]` → `[a, b, c, d]`
|
||||||
|
pub fn to_corner_values(&self) -> [f64; 4] {
|
||||||
|
let values: Vec<f64> = self.0.iter_element_values().copied().collect();
|
||||||
|
match values.as_slice() {
|
||||||
|
[] => [0., 0., 0., 0.],
|
||||||
|
&[a] => [a, a, a, a],
|
||||||
|
&[a, b] => [a, b, a, b],
|
||||||
|
&[a, b, c] => [a, b, c, b],
|
||||||
|
&[a, b, c, d, ..] => [a, b, c, d],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `List<f64>` is a runtime-only wire type, so serialize the corners as their bare values to keep documents stable
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl serde::Serialize for BoxCorners {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.collect_seq(self.0.iter_element_values())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for BoxCorners {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
Ok(Self::from(<Vec<f64> as serde::Deserialize>::deserialize(deserializer)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for BoxCorners {
|
||||||
|
fn from(value: f64) -> Self {
|
||||||
|
Self(List::new_from_element(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<f64>> for BoxCorners {
|
||||||
|
fn from(values: Vec<f64>) -> Self {
|
||||||
|
Self(values.into_iter().map(Item::new_from_element).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for BoxCorners {
|
||||||
|
fn from(text: &str) -> Self {
|
||||||
|
Self::from(
|
||||||
|
text.split([',', ' '])
|
||||||
|
.filter(|piece| !piece.is_empty())
|
||||||
|
.filter_map(|piece| piece.parse::<f64>().ok())
|
||||||
|
.collect::<Vec<f64>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for BoxCorners {
|
||||||
|
fn from(text: String) -> Self {
|
||||||
|
Self::from(text.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait AsU64 {
|
pub trait AsU64 {
|
||||||
fn as_u64(&self) -> u64;
|
fn as_u64(&self) -> u64;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
pub use crate::gradient::*;
|
pub use crate::gradient::*;
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
|
use core_types::list::{Item, List};
|
||||||
use core_types::transform::Transform;
|
use core_types::transform::Transform;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use std::f64::consts::{PI, TAU};
|
use std::f64::consts::{PI, TAU};
|
||||||
|
|
||||||
/// Describes an editable fill choice, storing color or gradient stops without gradient placement metadata.
|
/// The editor's in-memory paint picker state, storing color or gradient stops without gradient placement metadata.
|
||||||
|
/// Not stored in documents: paint inputs hold the picked value as a plain color, gradient, or no-paint type default.
|
||||||
///
|
///
|
||||||
/// Can be None, a solid [Color], or a linear/radial [GradientStops].
|
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||||
///
|
///
|
||||||
/// In the future we'll probably also add a pattern fill.
|
/// In the future we'll probably also add a pattern fill.
|
||||||
///
|
///
|
||||||
@@ -22,11 +24,11 @@ pub enum FillChoice {
|
|||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
Solid(Color),
|
Solid(Color),
|
||||||
Gradient(GradientStops),
|
Gradient(Gradient),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Deprecate [`FillChoice`] and keep this, renamed, as the main widget-controlling type
|
// TODO: Deprecate [`FillChoice`] and keep this, renamed, as the main widget-controlling type
|
||||||
/// JS-boundary version of [`FillChoice`] where the solid color is [`SRGBA8`] and the gradient is [`GradientStopsUI`].
|
/// JS-boundary version of [`FillChoice`] where the solid color is [`SRGBA8`] and the gradient is [`GradientUI`].
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(Default, Debug, Clone, PartialEq, DynAny)]
|
#[derive(Default, Debug, Clone, PartialEq, DynAny)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
@@ -34,7 +36,7 @@ pub enum FillChoiceUI {
|
|||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
Solid(SRGBA8),
|
Solid(SRGBA8),
|
||||||
Gradient(GradientStopsUI),
|
Gradient(GradientUI),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&FillChoice> for FillChoiceUI {
|
impl From<&FillChoice> for FillChoiceUI {
|
||||||
@@ -42,7 +44,7 @@ impl From<&FillChoice> for FillChoiceUI {
|
|||||||
match value {
|
match value {
|
||||||
FillChoice::None => Self::None,
|
FillChoice::None => Self::None,
|
||||||
FillChoice::Solid(color) => Self::Solid(SRGBA8::from(*color)),
|
FillChoice::Solid(color) => Self::Solid(SRGBA8::from(*color)),
|
||||||
FillChoice::Gradient(stops) => Self::Gradient(GradientStopsUI::from(stops)),
|
FillChoice::Gradient(stops) => Self::Gradient(GradientUI::from(stops)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ impl From<&FillChoiceUI> for FillChoice {
|
|||||||
match value {
|
match value {
|
||||||
FillChoiceUI::None => Self::None,
|
FillChoiceUI::None => Self::None,
|
||||||
FillChoiceUI::Solid(srgba) => Self::Solid(Color::from(*srgba)),
|
FillChoiceUI::Solid(srgba) => Self::Solid(Color::from(*srgba)),
|
||||||
FillChoiceUI::Gradient(stops) => Self::Gradient(GradientStops::from(stops)),
|
FillChoiceUI::Gradient(stops) => Self::Gradient(Gradient::from(stops)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +65,7 @@ impl FillChoiceUI {
|
|||||||
Some(*c)
|
Some(*c)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_gradient(&self) -> Option<&GradientStopsUI> {
|
pub fn as_gradient(&self) -> Option<&GradientUI> {
|
||||||
let Self::Gradient(g) = self else { return None };
|
let Self::Gradient(g) = self else { return None };
|
||||||
Some(g)
|
Some(g)
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,7 @@ impl FillChoice {
|
|||||||
Some(*color)
|
Some(*color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_gradient(&self) -> Option<&GradientStops> {
|
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||||
let Self::Gradient(gradient) = self else { return None };
|
let Self::Gradient(gradient) = self else { return None };
|
||||||
Some(gradient)
|
Some(gradient)
|
||||||
}
|
}
|
||||||
@@ -201,6 +203,65 @@ fn daffine2_identity() -> DAffine2 {
|
|||||||
DAffine2::IDENTITY
|
DAffine2::IDENTITY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A stroke's dash pattern: a sequence of lengths that alternate dash, gap, dash, gap, and so on. An odd-length
|
||||||
|
/// sequence repeats with the dash and gap roles swapped.
|
||||||
|
///
|
||||||
|
/// Wraps a `List<f64>` so the Data panel can introspect its lengths, mirroring how `Artboard` wraps a `List<Graphic>`,
|
||||||
|
/// while remaining a single rank-0 value on the wire.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
|
pub struct DashPattern(pub List<f64>);
|
||||||
|
|
||||||
|
impl DashPattern {
|
||||||
|
/// Returns the dash lengths with any negative values clamped to zero.
|
||||||
|
pub fn clamped_lengths(&self) -> Vec<f64> {
|
||||||
|
self.0.iter_element_values().map(|length| length.max(0.)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `List<f64>` is a runtime-only wire type, so serialize the pattern as its bare lengths to keep documents stable
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl serde::Serialize for DashPattern {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.collect_seq(self.0.iter_element_values())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for DashPattern {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
Ok(Self::from(<Vec<f64> as serde::Deserialize>::deserialize(deserializer)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for DashPattern {
|
||||||
|
fn from(length: f64) -> Self {
|
||||||
|
Self(List::new_from_element(length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<f64>> for DashPattern {
|
||||||
|
fn from(lengths: Vec<f64>) -> Self {
|
||||||
|
Self(lengths.into_iter().map(Item::new_from_element).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for DashPattern {
|
||||||
|
fn from(text: &str) -> Self {
|
||||||
|
Self::from(
|
||||||
|
text.split([',', ' '])
|
||||||
|
.filter(|piece| !piece.is_empty())
|
||||||
|
.filter_map(|piece| piece.parse::<f64>().ok())
|
||||||
|
.collect::<Vec<f64>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for DashPattern {
|
||||||
|
fn from(text: String) -> Self {
|
||||||
|
Self::from(text.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
|
|||||||
@@ -63,11 +63,19 @@ impl core_types::ops::FromAnchorPosition for Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity item conversion so `List<Vector>` satisfies the blanket `Convert<List<U>, ()> for List<T>`, letting its
|
// Lets a position wire feed a ranked vector connector through the input adapter's element conversion
|
||||||
// auto-inserted input wrapper be a `ConvertNode` (which also accepts a `DVec2` anchor position) rather than an `IntoNode`.
|
impl From<DVec2> for Vector {
|
||||||
impl core_types::ops::ListConvert<Vector> for Vector {
|
fn from(position: DVec2) -> Self {
|
||||||
fn convert_item(self) -> Vector {
|
<Self as core_types::ops::FromAnchorPosition>::from_anchor_position(position)
|
||||||
self
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl PipelineCache {
|
|||||||
pub async fn run<P: Pipeline>(&self, args: &P::Args<'_>) -> P::Out {
|
pub async fn run<P: Pipeline>(&self, args: &P::Args<'_>) -> P::Out {
|
||||||
let executor = self.executor.get().expect("PipelineCache not initialized");
|
let executor = self.executor.get().expect("PipelineCache not initialized");
|
||||||
let entry = self.pipeline.get().expect("PipelineCache not initialized");
|
let entry = self.pipeline.get().expect("PipelineCache not initialized");
|
||||||
let pipeline = (&**entry)
|
let pipeline = (**entry)
|
||||||
.downcast_ref::<P>()
|
.downcast_ref::<P>()
|
||||||
.unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),));
|
.unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),));
|
||||||
pipeline.run(executor, args).await
|
pipeline.run(executor, args).await
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::WgpuExecutor;
|
use crate::WgpuExecutor;
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::Ctx;
|
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{Item, List};
|
use core_types::list::{Item, List};
|
||||||
use core_types::ops::Convert;
|
use core_types::ops::Convert;
|
||||||
@@ -20,7 +19,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,
|
||||||
@@ -247,15 +246,3 @@ impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
|
|||||||
converter.convert(device).await.expect("Failed to download texture data")
|
converter.convert(device).await.expect("Failed to download texture data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads an raster texture from the CPU to the GPU. This is now deprecated and the Convert node should be used in the future.
|
|
||||||
///
|
|
||||||
/// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue.
|
|
||||||
#[node_macro::node(category(""))]
|
|
||||||
pub async fn upload_texture<'a: 'n, T: Convert<List<Raster<GPU>>, &'a WgpuExecutor>>(
|
|
||||||
_: impl Ctx,
|
|
||||||
#[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T,
|
|
||||||
executor: &'a WgpuExecutor,
|
|
||||||
) -> List<Raster<GPU>> {
|
|
||||||
input.convert(Footprint::DEFAULT, executor).await
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,8 @@ pub(crate) struct ParsedNodeFn {
|
|||||||
pub(crate) where_clause: Option<WhereClause>,
|
pub(crate) where_clause: Option<WhereClause>,
|
||||||
pub(crate) input: Input,
|
pub(crate) input: Input,
|
||||||
pub(crate) output_type: Type,
|
pub(crate) output_type: Type,
|
||||||
|
/// The peeled `Item` element of `output_type`, if the node returns `Item<T>`.
|
||||||
|
pub(crate) output_element: Option<Type>,
|
||||||
pub(crate) is_async: bool,
|
pub(crate) is_async: bool,
|
||||||
pub(crate) fields: Vec<ParsedField>,
|
pub(crate) fields: Vec<ParsedField>,
|
||||||
pub(crate) body: TokenStream2,
|
pub(crate) body: TokenStream2,
|
||||||
@@ -120,12 +122,98 @@ pub struct ParsedField {
|
|||||||
pub is_data_field: bool,
|
pub is_data_field: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ParsedField {
|
||||||
|
/// Whether the field is environment rather than an argument: `#[data]` state or a `#[scope]`-injected wire.
|
||||||
|
/// Environment fields never classify the node, never supply the element-wise frame, and broadcast by clone.
|
||||||
|
pub(crate) fn is_environment(&self) -> bool {
|
||||||
|
self.is_data_field
|
||||||
|
|| matches!(
|
||||||
|
self.ty.regular(),
|
||||||
|
Some(RegularParsedField {
|
||||||
|
value_source: ParsedValueSource::Scope(_),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ParsedFieldType {
|
pub enum ParsedFieldType {
|
||||||
Regular(RegularParsedField),
|
Regular(RegularParsedField),
|
||||||
|
/// Declared `Item<T>`: a rank-0 cell carrying element `T`; the field's `ty` keeps the full declared type.
|
||||||
|
Item {
|
||||||
|
field: RegularParsedField,
|
||||||
|
element: Type,
|
||||||
|
},
|
||||||
|
/// Declared `List<T>`: a whole-list wire carrying element `T`; the field's `ty` keeps the full declared type.
|
||||||
|
List {
|
||||||
|
field: RegularParsedField,
|
||||||
|
element: Type,
|
||||||
|
},
|
||||||
Node(NodeParsedField),
|
Node(NodeParsedField),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts `T` from a wrapper type like `Item<T>` or `List<T>`, if the type's outermost segment matches the wrapper name.
|
||||||
|
fn peel_wrapper(ty: &syn::Type, wrapper: &str) -> Option<syn::Type> {
|
||||||
|
let syn::Type::Path(type_path) = ty else { return None };
|
||||||
|
let segment = type_path.path.segments.last()?;
|
||||||
|
if segment.ident != wrapper {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else { return None };
|
||||||
|
match arguments.args.first()? {
|
||||||
|
syn::GenericArgument::Type(inner) => Some(inner.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn peel_item(ty: &syn::Type) -> Option<syn::Type> {
|
||||||
|
peel_wrapper(ty, "Item")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn peel_list(ty: &syn::Type) -> Option<syn::Type> {
|
||||||
|
peel_wrapper(ty, "List")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedFieldType {
|
||||||
|
/// Classifies a value field by its declared type: `Item<T>` and `List<T>` become the ranked variants carrying
|
||||||
|
/// element `T` (the field keeps the full declared type), and anything else stays `Regular`.
|
||||||
|
pub(crate) fn classify(field: RegularParsedField) -> ParsedFieldType {
|
||||||
|
if let Some(element) = peel_item(&field.ty) {
|
||||||
|
ParsedFieldType::Item { field, element }
|
||||||
|
} else if let Some(element) = peel_list(&field.ty) {
|
||||||
|
ParsedFieldType::List { field, element }
|
||||||
|
} else {
|
||||||
|
ParsedFieldType::Regular(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shared value-field data, present for every value field (`Regular`, `Item`, `List`) but not a lazy `Node`.
|
||||||
|
pub fn regular(&self) -> Option<&RegularParsedField> {
|
||||||
|
match self {
|
||||||
|
ParsedFieldType::Regular(field) | ParsedFieldType::Item { field, .. } | ParsedFieldType::List { field, .. } => Some(field),
|
||||||
|
ParsedFieldType::Node(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element type `T` of a rank-0 `Item<T>` field, or `None` for any other shape.
|
||||||
|
pub fn item_element(&self) -> Option<&Type> {
|
||||||
|
match self {
|
||||||
|
ParsedFieldType::Item { element, .. } => Some(element),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element type `T` of a whole-list `List<T>` field, or `None` for any other shape.
|
||||||
|
pub fn list_element(&self) -> Option<&Type> {
|
||||||
|
match self {
|
||||||
|
ParsedFieldType::List { element, .. } => Some(element),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A single numeric endpoint within a `#[soft(..)]` or `#[hard(..)]` bounds range.
|
/// A single numeric endpoint within a `#[soft(..)]` or `#[hard(..)]` bounds range.
|
||||||
/// Accepts both integer literals (e.g. `1`, `-1`) and float literals (e.g. `1.`, `-500.`).
|
/// Accepts both integer literals (e.g. `1`, `-1`) and float literals (e.g. `1.`, `-500.`).
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -248,6 +336,8 @@ pub struct RegularParsedField {
|
|||||||
pub struct NodeParsedField {
|
pub struct NodeParsedField {
|
||||||
pub input_type: Type,
|
pub input_type: Type,
|
||||||
pub output_type: Type,
|
pub output_type: Type,
|
||||||
|
/// The peeled `Item` element of `output_type`, if the lazy input's `Output` is declared `Item<T>`.
|
||||||
|
pub output_element: Option<Type>,
|
||||||
pub implementations: Punctuated<Implementation, Comma>,
|
pub implementations: Punctuated<Implementation, Comma>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +587,7 @@ impl Parse for NodeFnAttributes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNodeFn> {
|
pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNodeFn> {
|
||||||
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes:\n{e}")))?;
|
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes:\n{e}")))?;
|
||||||
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {e}. Make sure it's a valid Rust function.")))?;
|
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {e}. Make sure it's a valid Rust function.")))?;
|
||||||
|
|
||||||
@@ -510,6 +600,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
|
|||||||
|
|
||||||
let (input, fields) = parse_inputs(&input_fn.sig.inputs)?;
|
let (input, fields) = parse_inputs(&input_fn.sig.inputs)?;
|
||||||
let output_type = parse_output(&input_fn.sig.output)?;
|
let output_type = parse_output(&input_fn.sig.output)?;
|
||||||
|
let output_element = peel_item(&output_type);
|
||||||
let where_clause = input_fn.sig.generics.where_clause;
|
let where_clause = input_fn.sig.generics.where_clause;
|
||||||
let body = input_fn.block.to_token_stream();
|
let body = input_fn.block.to_token_stream();
|
||||||
let description = input_fn
|
let description = input_fn
|
||||||
@@ -538,6 +629,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
|
|||||||
fn_generics,
|
fn_generics,
|
||||||
input,
|
input,
|
||||||
output_type,
|
output_type,
|
||||||
|
output_element,
|
||||||
is_async,
|
is_async,
|
||||||
fields,
|
fields,
|
||||||
where_clause,
|
where_clause,
|
||||||
@@ -816,11 +908,14 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let output_element = peel_item(&output_type);
|
||||||
|
|
||||||
Ok(ParsedField {
|
Ok(ParsedField {
|
||||||
pat_ident,
|
pat_ident,
|
||||||
ty: ParsedFieldType::Node(NodeParsedField {
|
ty: ParsedFieldType::Node(NodeParsedField {
|
||||||
input_type,
|
input_type,
|
||||||
output_type,
|
output_type,
|
||||||
|
output_element,
|
||||||
implementations,
|
implementations,
|
||||||
}),
|
}),
|
||||||
name,
|
name,
|
||||||
@@ -861,7 +956,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||||||
|
|
||||||
Ok(ParsedField {
|
Ok(ParsedField {
|
||||||
pat_ident,
|
pat_ident,
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
ty: ParsedFieldType::classify(RegularParsedField {
|
||||||
exposed,
|
exposed,
|
||||||
number_soft_min,
|
number_soft_min,
|
||||||
number_soft_max,
|
number_soft_max,
|
||||||
@@ -936,6 +1031,12 @@ pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenS
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ParsedNodeFn {
|
impl ParsedNodeFn {
|
||||||
|
/// The node's primary: the first argument (non-environment) field, whose declared shape classifies the node
|
||||||
|
/// as an element-wise kernel, aggregation, or generator. Returns the field with its index in `fields`.
|
||||||
|
pub(crate) fn primary_input_field(&self) -> Option<(usize, &ParsedField)> {
|
||||||
|
self.fields.iter().enumerate().find(|(_, field)| !field.is_environment())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn replace_impl_trait_in_input(&mut self) {
|
pub fn replace_impl_trait_in_input(&mut self) {
|
||||||
if let Type::ImplTrait(impl_trait) = self.input.ty.clone() {
|
if let Type::ImplTrait(impl_trait) = self.input.ty.clone() {
|
||||||
let ident = Ident::new("_Input", impl_trait.span());
|
let ident = Ident::new("_Input", impl_trait.span());
|
||||||
@@ -1095,6 +1196,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(f64),
|
output_type: parse_quote!(f64),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![ParsedField {
|
fields: vec![ParsedField {
|
||||||
pat_ident: pat_ident("b"),
|
pat_ident: pat_ident("b"),
|
||||||
@@ -1165,6 +1267,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(T),
|
output_type: parse_quote!(T),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![
|
fields: vec![
|
||||||
ParsedField {
|
ParsedField {
|
||||||
@@ -1175,6 +1278,7 @@ mod tests {
|
|||||||
ty: ParsedFieldType::Node(NodeParsedField {
|
ty: ParsedFieldType::Node(NodeParsedField {
|
||||||
input_type: parse_quote!(Footprint),
|
input_type: parse_quote!(Footprint),
|
||||||
output_type: parse_quote!(T),
|
output_type: parse_quote!(T),
|
||||||
|
output_element: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
}),
|
}),
|
||||||
number_display_decimal_places: None,
|
number_display_decimal_places: None,
|
||||||
@@ -1249,6 +1353,7 @@ mod tests {
|
|||||||
context_features: vec![format_ident!("ExtractFootprint")],
|
context_features: vec![format_ident!("ExtractFootprint")],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(Vector),
|
output_type: parse_quote!(Vector),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![ParsedField {
|
fields: vec![ParsedField {
|
||||||
pat_ident: pat_ident("radius"),
|
pat_ident: pat_ident("radius"),
|
||||||
@@ -1315,6 +1420,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(List<Raster<P>>),
|
output_type: parse_quote!(List<Raster<P>>),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![ParsedField {
|
fields: vec![ParsedField {
|
||||||
pat_ident: pat_ident("shadows"),
|
pat_ident: pat_ident("shadows"),
|
||||||
@@ -1393,6 +1499,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(f64),
|
output_type: parse_quote!(f64),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![ParsedField {
|
fields: vec![ParsedField {
|
||||||
pat_ident: pat_ident("b"),
|
pat_ident: pat_ident("b"),
|
||||||
@@ -1474,6 +1581,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(List<Raster<CPU>>),
|
output_type: parse_quote!(List<Raster<CPU>>),
|
||||||
|
output_element: None,
|
||||||
is_async: true,
|
is_async: true,
|
||||||
fields: vec![ParsedField {
|
fields: vec![ParsedField {
|
||||||
pat_ident: pat_ident("path"),
|
pat_ident: pat_ident("path"),
|
||||||
@@ -1540,6 +1648,7 @@ mod tests {
|
|||||||
context_features: vec![],
|
context_features: vec![],
|
||||||
},
|
},
|
||||||
output_type: parse_quote!(i32),
|
output_type: parse_quote!(i32),
|
||||||
|
output_element: None,
|
||||||
is_async: false,
|
is_async: false,
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
@@ -1629,10 +1738,10 @@ mod tests {
|
|||||||
#[implementations(
|
#[implementations(
|
||||||
() -> List<Raster<CPU>>,
|
() -> List<Raster<CPU>>,
|
||||||
() -> List<Color>,
|
() -> List<Color>,
|
||||||
() -> List<GradientStops>,
|
() -> List<Gradient>,
|
||||||
Footprint -> List<Raster<CPU>>,
|
Footprint -> List<Raster<CPU>>,
|
||||||
Footprint -> List<Color>,
|
Footprint -> List<Color>,
|
||||||
Footprint -> List<GradientStops>,
|
Footprint -> List<Gradient>,
|
||||||
)]
|
)]
|
||||||
image: impl Node<F, Output = T>,
|
image: impl Node<F, Output = T>,
|
||||||
) -> T {
|
) -> T {
|
||||||
|
|||||||
@@ -31,21 +31,26 @@ impl ShaderCodegen for PerPixelAdjust {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
let ident = &f.pat_ident;
|
let ident = &f.pat_ident;
|
||||||
match &f.ty {
|
if let ParsedFieldType::Node { .. } = &f.ty {
|
||||||
ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")),
|
return Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics"));
|
||||||
ParsedFieldType::Regular(RegularParsedField { gpu_image: false, ty, .. }) => Ok(Param {
|
}
|
||||||
|
let regular = f.ty.regular().expect("a non-node shader field is a value field");
|
||||||
|
// Ranked connectors carry their bare element type in the uniform buffer and get rewrapped in the entry point
|
||||||
|
let element_ty = f.ty.item_element();
|
||||||
|
if regular.gpu_image {
|
||||||
|
Ok(Param {
|
||||||
|
ident: Cow::Owned(format_ident!("image_{}", &ident.ident)),
|
||||||
|
ty: quote!(Image2d),
|
||||||
|
item_wrapped: element_ty.is_some(),
|
||||||
|
param_type: ParamType::Image { binding: 0 },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Param {
|
||||||
ident: Cow::Borrowed(&ident.ident),
|
ident: Cow::Borrowed(&ident.ident),
|
||||||
ty: ty.to_token_stream(),
|
item_wrapped: element_ty.is_some(),
|
||||||
|
ty: element_ty.map(|element_ty| element_ty.to_token_stream()).unwrap_or_else(|| regular.ty.to_token_stream()),
|
||||||
param_type: ParamType::Uniform,
|
param_type: ParamType::Uniform,
|
||||||
}),
|
})
|
||||||
ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. }) => {
|
|
||||||
let param = Param {
|
|
||||||
ident: Cow::Owned(format_ident!("image_{}", &ident.ident)),
|
|
||||||
ty: quote!(Image2d),
|
|
||||||
param_type: ParamType::Image { binding: 0 },
|
|
||||||
};
|
|
||||||
Ok(param)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<syn::Result<Vec<_>>>()?;
|
.collect::<syn::Result<Vec<_>>>()?;
|
||||||
@@ -115,7 +120,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
let uniform_members = self
|
let uniform_members = self
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|Param { ident, ty, param_type }| match param_type {
|
.filter_map(|Param { ident, ty, param_type, .. }| match param_type {
|
||||||
ParamType::Image { .. } => None,
|
ParamType::Image { .. } => None,
|
||||||
ParamType::Uniform => Some(quote! {#ident: #ty}),
|
ParamType::Uniform => Some(quote! {#ident: #ty}),
|
||||||
})
|
})
|
||||||
@@ -133,19 +138,24 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
let image_params = self
|
let image_params = self
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|Param { ident, ty, param_type }| match param_type {
|
.filter_map(|Param { ident, ty, param_type, .. }| match param_type {
|
||||||
ParamType::Image { binding } => Some(quote! {#[spirv(descriptor_set = 0, binding = #binding)] #ident: &#ty}),
|
ParamType::Image { binding } => Some(quote! {#[spirv(descriptor_set = 0, binding = #binding)] #ident: &#ty}),
|
||||||
ParamType::Uniform => None,
|
ParamType::Uniform => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
// Ranked connectors compile against the no_std `Item` stand-in on the GPU, so wrapping and unwrapping here is free
|
||||||
let call_args = self
|
let call_args = self
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|Param { ident, param_type, .. }| match param_type {
|
.map(|Param { ident, param_type, item_wrapped, .. }| {
|
||||||
ParamType::Image { .. } => quote!(Color::from_vec4(#ident.fetch_with(texel_coord, lod(0)))),
|
let bare_value = match param_type {
|
||||||
ParamType::Uniform => quote!(uniform.#ident),
|
ParamType::Image { .. } => quote!(Color::from_vec4(#ident.fetch_with(texel_coord, lod(0)))),
|
||||||
|
ParamType::Uniform => quote!(uniform.#ident),
|
||||||
|
};
|
||||||
|
if *item_wrapped { quote!(Item::new_from_element(#bare_value)) } else { bare_value }
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let unwrap_result = self.parsed.output_element.as_ref().map(|_| quote!(.into_element()));
|
||||||
let context = quote!(());
|
let context = quote!(());
|
||||||
|
|
||||||
let entry_point_mod = &self.entry_point_mod;
|
let entry_point_mod = &self.entry_point_mod;
|
||||||
@@ -173,7 +183,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
) {
|
) {
|
||||||
let uniform = <Uniform as #gcore_shaders::shaders::buffer_struct::BufferStruct>::read(*uniform);
|
let uniform = <Uniform as #gcore_shaders::shaders::buffer_struct::BufferStruct>::read(*uniform);
|
||||||
let texel_coord = frag_coord.xy().as_uvec2();
|
let texel_coord = frag_coord.xy().as_uvec2();
|
||||||
let color: Color = #fn_name(#context, #(#call_args),*);
|
let color: Color = #fn_name(#context, #(#call_args),*)#unwrap_result;
|
||||||
*color_out = color.to_vec4();
|
*color_out = color.to_vec4();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,29 +201,27 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
.parsed
|
.parsed
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| match &f.ty {
|
.map(|f| {
|
||||||
ParsedFieldType::Regular(reg @ RegularParsedField { gpu_image: true, .. }) => Ok(ParsedField {
|
if let ParsedFieldType::Node { .. } = &f.ty {
|
||||||
pat_ident: PatIdent {
|
return Err(syn::Error::new_spanned(&f.pat_ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics"));
|
||||||
mutability: None,
|
}
|
||||||
by_ref: None,
|
let regular = f.ty.regular().expect("a non-node shader field is a value field");
|
||||||
..f.pat_ident.clone()
|
let pat_ident = PatIdent {
|
||||||
},
|
mutability: None,
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
by_ref: None,
|
||||||
|
..f.pat_ident.clone()
|
||||||
|
};
|
||||||
|
// A gpu_image parameter is fed the whole GPU raster list, so its wire is re-typed and re-classified
|
||||||
|
let ty = if regular.gpu_image {
|
||||||
|
ParsedFieldType::classify(RegularParsedField {
|
||||||
ty: raster_gpu.clone(),
|
ty: raster_gpu.clone(),
|
||||||
implementations: Punctuated::default(),
|
implementations: Punctuated::default(),
|
||||||
..reg.clone()
|
..regular.clone()
|
||||||
}),
|
})
|
||||||
..f.clone()
|
} else {
|
||||||
}),
|
f.ty.clone()
|
||||||
ParsedFieldType::Regular(RegularParsedField { gpu_image: false, .. }) => Ok(ParsedField {
|
};
|
||||||
pat_ident: PatIdent {
|
Ok(ParsedField { pat_ident, ty, ..f.clone() })
|
||||||
mutability: None,
|
|
||||||
by_ref: None,
|
|
||||||
..f.pat_ident.clone()
|
|
||||||
},
|
|
||||||
..f.clone()
|
|
||||||
}),
|
|
||||||
ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(&f.pat_ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")),
|
|
||||||
})
|
})
|
||||||
.collect::<syn::Result<Vec<_>>>()?;
|
.collect::<syn::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
@@ -230,8 +238,8 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
name: None,
|
name: None,
|
||||||
description: "".to_string(),
|
description: "".to_string(),
|
||||||
widget_override: Default::default(),
|
widget_override: Default::default(),
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
ty: ParsedFieldType::classify(RegularParsedField {
|
||||||
ty: parse_quote!(&'a WgpuExecutor),
|
ty: parse_quote!(#gcore::list::Item<&'a WgpuExecutor>),
|
||||||
exposed: true,
|
exposed: true,
|
||||||
value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")),
|
value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")),
|
||||||
number_soft_min: None,
|
number_soft_min: None,
|
||||||
@@ -250,7 +258,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
|
|
||||||
// find exactly one gpu_image field, runtime doesn't support more than 1 atm
|
// find exactly one gpu_image field, runtime doesn't support more than 1 atm
|
||||||
let gpu_image_field = {
|
let gpu_image_field = {
|
||||||
let mut iter = fields.iter().filter(|f| matches!(f.ty, ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. })));
|
let mut iter = fields.iter().filter(|f| f.ty.regular().is_some_and(|regular| regular.gpu_image));
|
||||||
match (iter.next(), iter.next()) {
|
match (iter.next(), iter.next()) {
|
||||||
(Some(v), None) => Ok(v),
|
(Some(v), None) => Ok(v),
|
||||||
(Some(_), Some(more)) => Err(syn::Error::new_spanned(&more.pat_ident, "No more than one parameter must be annotated with `#[gpu_image]`")),
|
(Some(_), Some(more)) => Err(syn::Error::new_spanned(&more.pat_ident, "No more than one parameter must be annotated with `#[gpu_image]`")),
|
||||||
@@ -268,7 +276,10 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|p| match p.param_type {
|
.filter_map(|p| match p.param_type {
|
||||||
ParamType::Image { .. } => None,
|
ParamType::Image { .. } => None,
|
||||||
ParamType::Uniform => Some(p.ident.as_ref()),
|
ParamType::Uniform => {
|
||||||
|
let ident = p.ident.as_ref();
|
||||||
|
Some(if p.item_wrapped { quote!(#ident: #ident.into_element()) } else { quote!(#ident) })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
quote!(Some(&super::#uniform_struct {
|
quote!(Some(&super::#uniform_struct {
|
||||||
@@ -283,7 +294,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
let entry_point_name = &self.entry_point_name;
|
let entry_point_name = &self.entry_point_name;
|
||||||
let body = quote! {
|
let body = quote! {
|
||||||
{
|
{
|
||||||
#executor.shader_runtime().run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders {
|
#executor.into_element().shader_runtime().run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders {
|
||||||
wgsl_shader: crate::WGSL_SHADER,
|
wgsl_shader: crate::WGSL_SHADER,
|
||||||
fragment_shader_name: super::#entry_point_name,
|
fragment_shader_name: super::#entry_point_name,
|
||||||
has_uniform: #has_uniform,
|
has_uniform: #has_uniform,
|
||||||
@@ -314,6 +325,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
context_features: self.parsed.input.context_features.clone(),
|
context_features: self.parsed.input.context_features.clone(),
|
||||||
},
|
},
|
||||||
output_type: raster_gpu,
|
output_type: raster_gpu,
|
||||||
|
output_element: None,
|
||||||
is_async: true,
|
is_async: true,
|
||||||
fields,
|
fields,
|
||||||
body,
|
body,
|
||||||
@@ -339,6 +351,7 @@ impl PerPixelAdjustCodegen<'_> {
|
|||||||
struct Param<'a> {
|
struct Param<'a> {
|
||||||
ident: Cow<'a, Ident>,
|
ident: Cow<'a, Ident>,
|
||||||
ty: TokenStream,
|
ty: TokenStream,
|
||||||
|
item_wrapped: bool,
|
||||||
param_type: ParamType,
|
param_type: ParamType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::parsing::{Implementation, NodeParsedField, ParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField};
|
use crate::parsing::{Implementation, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField};
|
||||||
use proc_macro_error2::emit_error;
|
use proc_macro_error2::emit_error;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
@@ -11,6 +11,9 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
|||||||
validate_primary_input_expose,
|
validate_primary_input_expose,
|
||||||
validate_min_max,
|
validate_min_max,
|
||||||
validate_range_slider_bounds,
|
validate_range_slider_bounds,
|
||||||
|
validate_no_item_parameters,
|
||||||
|
validate_element_wise,
|
||||||
|
validate_ranked_inputs,
|
||||||
];
|
];
|
||||||
|
|
||||||
for validator in validators {
|
for validator in validators {
|
||||||
@@ -20,20 +23,235 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_no_item_parameters(parsed: &ParsedNodeFn) {
|
||||||
|
if parsed.attributes.skip_impl {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An `Item` primary shares its element-wise frame with ranked parameters; a `List`/`ListDyn` aggregation primary accepts
|
||||||
|
// them as fixed ranked inputs; a `()` generator has no primary and draws its frame from the ranked parameters themselves.
|
||||||
|
let ranked = |ty: &Type| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn");
|
||||||
|
let primary = parsed.primary_input_field();
|
||||||
|
let primary_permits_item_params = match primary.map(|(_, field)| &field.ty) {
|
||||||
|
Some(ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. })) => ranked(output_type) || implementations.iter().any(|implementation| ranked(&implementation.output)),
|
||||||
|
Some(value) => {
|
||||||
|
let regular = value.regular().expect("a non-node primary is a value field");
|
||||||
|
is_unit_type(®ular.ty) || ranked(®ular.ty) || regular.implementations.iter().any(ranked)
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
let primary_index = primary.map(|(index, _)| index);
|
||||||
|
|
||||||
|
for (index, field) in parsed.fields.iter().enumerate() {
|
||||||
|
if Some(index) == primary_index || field.is_environment() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(RegularParsedField { ty, implementations, .. }) = field.ty.regular() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let pat_ident = &field.pat_ident;
|
||||||
|
|
||||||
|
// A ranked parameter requires a ranked primary: `Item<T>` (element-wise frame) or `List<T>`/`ListDyn` (aggregation)
|
||||||
|
if outer_wrapper_is(ty, "Item") && !primary_permits_item_params {
|
||||||
|
emit_error!(
|
||||||
|
pat_ident.span(),
|
||||||
|
"The `Item<T>` parameter `{}` requires the primary input to be ranked (`Item<T>`, `List<T>`, or `ListDyn`)",
|
||||||
|
pat_ident.ident
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if outer_wrapper_is(ty, "Item")
|
||||||
|
&& implementations
|
||||||
|
.iter()
|
||||||
|
.any(|ty| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn"))
|
||||||
|
{
|
||||||
|
emit_error!(pat_ident.span(), "The #[implementations(...)] of the ranked parameter `{}` must be bare element types", pat_ident.ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_element_wise(parsed: &ParsedNodeFn) {
|
||||||
|
if parsed.attributes.skip_impl {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((_, primary)) = parsed.primary_input_field() else { return };
|
||||||
|
let Some(RegularParsedField { ty, implementations, .. }) = primary.ty.regular() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !outer_wrapper_is(ty, "Item") {
|
||||||
|
// A non-`Item` primary may still emit a rank-0 `Item<T>`: a `()` generator has no input, and a `List<T>` or
|
||||||
|
// `ListDyn` aggregation (declared directly or via a generic primary's implementation rows) reduces a whole list down to a single item.
|
||||||
|
let primary_reduces_or_generates = is_unit_type(ty)
|
||||||
|
|| primary.ty.list_element().is_some()
|
||||||
|
|| outer_wrapper_is(ty, "ListDyn")
|
||||||
|
|| implementations.iter().any(|ty| outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn"));
|
||||||
|
if outer_wrapper_is(&parsed.output_type, "Item") && !primary_reduces_or_generates {
|
||||||
|
emit_error!(
|
||||||
|
parsed.output_type.span(),
|
||||||
|
"Returning `Item<T>` requires the primary input to be `Item<T>` (element-wise), `List<T>`/`ListDyn` (aggregation), or `()` (generator)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if implementations
|
||||||
|
.iter()
|
||||||
|
.any(|ty| outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "ListDyn"))
|
||||||
|
{
|
||||||
|
emit_error!(
|
||||||
|
primary.pat_ident.span(),
|
||||||
|
"The #[implementations(...)] of `{}` must be bare element types; the macro derives the Item and List wire forms",
|
||||||
|
primary.pat_ident.ident
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !outer_wrapper_is(&parsed.output_type, "Item") && !outer_wrapper_is(&parsed.output_type, "List") {
|
||||||
|
emit_error!(
|
||||||
|
parsed.output_type.span(),
|
||||||
|
"An element-wise node (declared by its `Item<T>` primary input) must return `Item<U>`, or `List<U>` for an expander"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every input must ride a ranked wire: `Item<T>`, `List<T>`, or `ListDyn`, declared directly or substituted per
|
||||||
|
/// implementations row when the field's type is a bare generic. The () type stays legal as the no-primary-input
|
||||||
|
/// sentinel for generator nodes, and `#[data]` fields are exempt as internal state rather than wires.
|
||||||
|
fn validate_ranked_inputs(parsed: &ParsedNodeFn) {
|
||||||
|
for (span, message) in ranked_input_violations(parsed) {
|
||||||
|
emit_error!(span, "{}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ranked_input_violations(parsed: &ParsedNodeFn) -> Vec<(proc_macro2::Span, String)> {
|
||||||
|
let mut violations = Vec::new();
|
||||||
|
if parsed.attributes.skip_impl {
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ranked = |ty: &Type| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn");
|
||||||
|
let primary_index = parsed.primary_input_field().map(|(index, _)| index);
|
||||||
|
|
||||||
|
for (index, field) in parsed.fields.iter().enumerate() {
|
||||||
|
if field.is_data_field {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pat_ident = &field.pat_ident;
|
||||||
|
|
||||||
|
match &field.ty {
|
||||||
|
ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => {
|
||||||
|
if ranked(output_type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_bare_generic(output_type, &parsed.fn_generics) {
|
||||||
|
for row in implementations {
|
||||||
|
let output = &row.output;
|
||||||
|
if !ranked(output) {
|
||||||
|
violations.push((
|
||||||
|
output.span(),
|
||||||
|
format!(
|
||||||
|
"Implementations row output `{ty}` of the lazy input `{name}` must be ranked: produce `Item<{ty}>` for one cell or `List<{ty}>` for a whole list",
|
||||||
|
name = pat_ident.ident,
|
||||||
|
ty = quote!(#output)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.push((
|
||||||
|
pat_ident.span(),
|
||||||
|
format!(
|
||||||
|
"Lazy input `{name}` with output type `{ty}` must be ranked: declare its `Output` as `Item<{ty}>` for one cell or `List<{ty}>` for a whole list",
|
||||||
|
name = pat_ident.ident,
|
||||||
|
ty = quote!(#output_type)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
value => {
|
||||||
|
let RegularParsedField { ty, implementations, .. } = value.regular().expect("a non-node field is a value field");
|
||||||
|
|
||||||
|
if is_unit_type(ty) {
|
||||||
|
if Some(index) != primary_index {
|
||||||
|
violations.push((
|
||||||
|
pat_ident.span(),
|
||||||
|
format!(
|
||||||
|
"Parameter `{}` cannot be typed `()`: the unit type is only the no-primary-input sentinel for generator nodes",
|
||||||
|
pat_ident.ident
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ranked(ty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bare-generic field takes each implementations row as its whole wire type, so the rows carry the rank
|
||||||
|
if is_bare_generic(ty, &parsed.fn_generics) {
|
||||||
|
for row in implementations {
|
||||||
|
if !ranked(row) && !is_bare_generic(row, &parsed.fn_generics) {
|
||||||
|
violations.push((
|
||||||
|
row.span(),
|
||||||
|
format!(
|
||||||
|
"Implementations row `{row}` of `{name}` must be ranked: wrap it as `Item<{row}>` to consume one cell or `List<{row}>` to consume a whole list",
|
||||||
|
name = pat_ident.ident,
|
||||||
|
row = quote!(#row)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.push((
|
||||||
|
pat_ident.span(),
|
||||||
|
format!(
|
||||||
|
"Parameter `{name}` of type `{ty}` must be ranked: wrap it as `Item<{ty}>` to consume one cell or `List<{ty}>` to consume a whole list",
|
||||||
|
name = pat_ident.ident,
|
||||||
|
ty = quote!(#ty)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
violations
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the type is exactly one of the function's generic parameters, like `T`.
|
||||||
|
fn is_bare_generic(ty: &Type, fn_generics: &[GenericParam]) -> bool {
|
||||||
|
let Type::Path(type_path) = ty else { return false };
|
||||||
|
let Some(ident) = type_path.path.get_ident() else { return false };
|
||||||
|
fn_generics.iter().any(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == *ident))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the type's outermost path segment is the given wrapper name, like `Item` in `Item<T>`.
|
||||||
|
fn outer_wrapper_is(ty: &Type, wrapper: &str) -> bool {
|
||||||
|
let Type::Path(type_path) = ty else { return false };
|
||||||
|
type_path.path.segments.last().is_some_and(|segment| segment.ident == wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the type is the unit type `()`, which marks a generator with no primary input.
|
||||||
|
fn is_unit_type(ty: &Type) -> bool {
|
||||||
|
matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_min_max(parsed: &ParsedNodeFn) {
|
fn validate_min_max(parsed: &ParsedNodeFn) {
|
||||||
for field in &parsed.fields {
|
for field in &parsed.fields {
|
||||||
if let ParsedField {
|
if let Some(RegularParsedField {
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
number_hard_max,
|
||||||
number_hard_max,
|
number_hard_min,
|
||||||
number_hard_min,
|
number_soft_max,
|
||||||
number_soft_max,
|
number_soft_min,
|
||||||
number_soft_min,
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
pat_ident,
|
|
||||||
..
|
..
|
||||||
} = field
|
}) = field.ty.regular()
|
||||||
{
|
{
|
||||||
|
let pat_ident = &field.pat_ident;
|
||||||
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
|
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
|
||||||
let soft_min_value: f64 = soft_min.to_f64();
|
let soft_min_value: f64 = soft_min.to_f64();
|
||||||
let hard_min_value: f64 = hard_min.to_f64();
|
let hard_min_value: f64 = hard_min.to_f64();
|
||||||
@@ -85,19 +303,16 @@ fn validate_min_max(parsed: &ParsedNodeFn) {
|
|||||||
/// otherwise it falls back to `#[hard]`, so each end must be covered by at least one of the two attributes.
|
/// otherwise it falls back to `#[hard]`, so each end must be covered by at least one of the two attributes.
|
||||||
fn validate_range_slider_bounds(parsed: &ParsedNodeFn) {
|
fn validate_range_slider_bounds(parsed: &ParsedNodeFn) {
|
||||||
for field in &parsed.fields {
|
for field in &parsed.fields {
|
||||||
if let ParsedField {
|
if let Some(RegularParsedField {
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField {
|
number_mode_range: true,
|
||||||
number_mode_range: true,
|
number_soft_min,
|
||||||
number_soft_min,
|
number_soft_max,
|
||||||
number_soft_max,
|
number_hard_min,
|
||||||
number_hard_min,
|
number_hard_max,
|
||||||
number_hard_max,
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
pat_ident,
|
|
||||||
..
|
..
|
||||||
} = field
|
}) = field.ty.regular()
|
||||||
{
|
{
|
||||||
|
let pat_ident = &field.pat_ident;
|
||||||
let min_bounded = number_soft_min.is_some() || number_hard_min.is_some();
|
let min_bounded = number_soft_min.is_some() || number_hard_min.is_some();
|
||||||
let max_bounded = number_soft_max.is_some() || number_hard_max.is_some();
|
let max_bounded = number_soft_max.is_some() || number_hard_max.is_some();
|
||||||
|
|
||||||
@@ -120,12 +335,10 @@ fn validate_range_slider_bounds(parsed: &ParsedNodeFn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_primary_input_expose(parsed: &ParsedNodeFn) {
|
fn validate_primary_input_expose(parsed: &ParsedNodeFn) {
|
||||||
if let Some(ParsedField {
|
if let Some(field) = parsed.fields.first()
|
||||||
ty: ParsedFieldType::Regular(RegularParsedField { exposed: true, .. }),
|
&& let Some(RegularParsedField { exposed: true, .. }) = field.ty.regular()
|
||||||
pat_ident,
|
|
||||||
..
|
|
||||||
}) = parsed.fields.first()
|
|
||||||
{
|
{
|
||||||
|
let pat_ident = &field.pat_ident;
|
||||||
emit_error!(
|
emit_error!(
|
||||||
pat_ident.span(),
|
pat_ident.span(),
|
||||||
"Unnecessary #[expose] attribute on primary input `{}`. Primary inputs are always exposed.",
|
"Unnecessary #[expose] attribute on primary input `{}`. Primary inputs are always exposed.",
|
||||||
@@ -148,18 +361,6 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
|||||||
|
|
||||||
let pat_ident = &field.pat_ident;
|
let pat_ident = &field.pat_ident;
|
||||||
match &field.ty {
|
match &field.ty {
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => {
|
|
||||||
if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() {
|
|
||||||
emit_error!(
|
|
||||||
ty.span(),
|
|
||||||
"Generic type `{}` in field `{}` requires an #[implementations(...)] attribute",
|
|
||||||
quote!(#ty),
|
|
||||||
pat_ident.ident;
|
|
||||||
help = "Add #[implementations(ConcreteType1, ConcreteType2)] to field '{}'", pat_ident.ident;
|
|
||||||
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ParsedFieldType::Node(NodeParsedField {
|
ParsedFieldType::Node(NodeParsedField {
|
||||||
input_type,
|
input_type,
|
||||||
output_type,
|
output_type,
|
||||||
@@ -180,6 +381,19 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
|||||||
validate_node_implementation(impl_, input_type, output_type, &parsed.fn_generics);
|
validate_node_implementation(impl_, input_type, output_type, &parsed.fn_generics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
value => {
|
||||||
|
let RegularParsedField { ty, implementations, .. } = value.regular().expect("a non-node field is a value field");
|
||||||
|
if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() {
|
||||||
|
emit_error!(
|
||||||
|
ty.span(),
|
||||||
|
"Generic type `{}` in field `{}` requires an #[implementations(...)] attribute",
|
||||||
|
quote!(#ty),
|
||||||
|
pat_ident.ident;
|
||||||
|
help = "Add #[implementations(ConcreteType1, ConcreteType2)] to field '{}'", pat_ident.ident;
|
||||||
|
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,3 +432,174 @@ fn contains_generic_param(ty: &Type, fn_generics: &[GenericParam]) -> bool {
|
|||||||
syn::visit::visit_type(&mut checker, ty);
|
syn::visit::visit_type(&mut checker, ty);
|
||||||
checker.found
|
checker.found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::parsing::parse_node_fn;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
|
||||||
|
fn violations(attr: TokenStream, input: TokenStream) -> Vec<String> {
|
||||||
|
let parsed = parse_node_fn(attr, input).expect("The test node fn should parse");
|
||||||
|
ranked_input_violations(&parsed).into_iter().map(|(_, message)| message).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_concrete_parameter_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn scale(_: impl Ctx, content: Item<Vector>, factor: f64) -> Item<Vector> {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("of type `f64` must be ranked"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ranked_parameters_and_the_unit_primary_sentinel_pass() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn circle(_: impl Ctx, _primary: (), radius: Item<f64>, points: List<DVec2>, erased: ListDyn) -> Item<Vector> {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages, Vec::<String>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_unit_typed_non_primary_parameter_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn weird(_: impl Ctx, content: Item<Vector>, marker: ()) -> Item<Vector> {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("Parameter `marker` cannot be typed `()`"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_generic_parameter_with_ranked_rows_passes() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn to_thing<T>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>, ListDyn)] content: T) -> Item<Graphic> {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages, Vec::<String>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bare_generic_parameter_with_a_bare_row_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn to_thing<T>(_: impl Ctx, #[implementations(List<Graphic>, f64)] content: T) -> Item<Graphic> {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("row `f64`"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_item_declared_parameter_with_bare_element_rows_passes() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn blend<T>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<T>, mode: Item<f64>) -> Item<T> {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages, Vec::<String>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lazy_input_with_a_bare_output_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn lazy_thing(_: impl Ctx, content: Item<Vector>, source: impl Node<Context, Output = f64>) -> Item<Vector> {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("Lazy input `source` with output type `f64` must be ranked"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lazy_generic_output_with_ranked_rows_passes() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn cache<T>(_: impl Ctx, #[implementations(Context -> Item<f64>, Context -> ListDyn)] value: impl Node<Context, Output = T>) -> T {
|
||||||
|
T::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages, Vec::<String>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lazy_unit_row_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn cache<T>(_: impl Ctx, #[implementations(Context -> (), Context -> Item<f64>)] value: impl Node<Context, Output = T>) -> T {
|
||||||
|
T::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("row output `()` of the lazy input `value` must be ranked"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lazy_generic_output_with_a_bare_row_is_rejected() {
|
||||||
|
let messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn cache<T>(_: impl Ctx, #[implementations(Context -> Item<f64>, Context -> f64)] value: impl Node<Context, Output = T>) -> T {
|
||||||
|
T::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||||
|
assert!(messages[0].contains("row output `f64` of the lazy input `value` must be ranked"), "{messages:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skip_impl_nodes_and_data_fields_are_exempt() {
|
||||||
|
let skip_impl_messages = violations(
|
||||||
|
quote::quote!(category(""), skip_impl),
|
||||||
|
quote::quote!(
|
||||||
|
fn passthrough<T>(_: impl Ctx, content: T) -> T {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(skip_impl_messages, Vec::<String>::new());
|
||||||
|
|
||||||
|
let data_field_messages = violations(
|
||||||
|
quote::quote!(category("Test")),
|
||||||
|
quote::quote!(
|
||||||
|
fn stateful(_: impl Ctx, content: Item<Vector>, #[data] cache: f64) -> Item<Vector> {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(data_field_messages, Vec::<String>::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,204 +1,25 @@
|
|||||||
use core_types::list::List;
|
use core_types::list::Item;
|
||||||
use core_types::registry::types::Percentage;
|
use core_types::registry::types::Percentage;
|
||||||
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, BlendMode, Color, Ctx};
|
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, BlendMode, Color, Ctx};
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use graphic_types::raster_types::{CPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
|
|
||||||
pub(crate) trait MultiplyAlpha {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MultiplyAlpha for Color {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn multiply_list_attribute<T>(list: &mut List<T>, key: &str, factor: f64) {
|
|
||||||
if let Some(values) = list.iter_attribute_values_mut::<f64>(key) {
|
|
||||||
for v in values {
|
|
||||||
*v *= factor;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for v in list.iter_attribute_values_mut_or_default::<f64>(key) {
|
|
||||||
*v = factor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MultiplyAlpha for List<Vector> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyAlpha for List<Graphic> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyAlpha for List<Raster<CPU>> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyAlpha for List<Color> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyAlpha for List<GradientStops> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyAlpha for List<String> {
|
|
||||||
fn multiply_alpha(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait MultiplyFill {
|
|
||||||
fn multiply_fill(&mut self, factor: f64);
|
|
||||||
}
|
|
||||||
impl MultiplyFill for Color {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<Vector> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<Graphic> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<Raster<CPU>> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<Color> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<GradientStops> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl MultiplyFill for List<String> {
|
|
||||||
fn multiply_fill(&mut self, factor: f64) {
|
|
||||||
multiply_list_attribute(self, ATTR_OPACITY_FILL, factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait SetBlendMode {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_list_blend_mode<T>(list: &mut List<T>, blend_mode: BlendMode) {
|
|
||||||
for v in list.iter_attribute_values_mut_or_default::<BlendMode>(ATTR_BLEND_MODE) {
|
|
||||||
*v = blend_mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetBlendMode for List<Vector> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetBlendMode for List<Graphic> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetBlendMode for List<Raster<CPU>> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetBlendMode for List<Color> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetBlendMode for List<GradientStops> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetBlendMode for List<String> {
|
|
||||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
|
||||||
set_list_blend_mode(self, blend_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait SetClip {
|
|
||||||
fn set_clip(&mut self, clip: bool);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_list_clip<T>(list: &mut List<T>, clip: bool) {
|
|
||||||
for v in list.iter_attribute_values_mut_or_default::<bool>(ATTR_CLIPPING_MASK) {
|
|
||||||
*v = clip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetClip for List<Vector> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetClip for List<Graphic> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetClip for List<Raster<CPU>> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetClip for List<Color> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetClip for List<GradientStops> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SetClip for List<String> {
|
|
||||||
fn set_clip(&mut self, clip: bool) {
|
|
||||||
set_list_clip(self, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies the blend mode to the input graphics. Setting this allows for customizing how overlapping content is composited together.
|
/// Applies the blend mode to the input graphics. Setting this allows for customizing how overlapping content is composited together.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn blend_mode<T: SetBlendMode>(
|
fn blend_mode<T>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The layer stack that will be composited when rendering.
|
/// The content that will be composited when rendering.
|
||||||
#[implementations(
|
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)]
|
||||||
List<Graphic>,
|
content: Item<T>,
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<String>,
|
|
||||||
)]
|
|
||||||
mut content: T,
|
|
||||||
/// The choice of equation that controls how brightness and color blends between overlapping pixels.
|
/// The choice of equation that controls how brightness and color blends between overlapping pixels.
|
||||||
blend_mode: BlendMode,
|
blend_mode: Item<BlendMode>,
|
||||||
) -> T {
|
) -> Item<T> {
|
||||||
// TODO: Find a way to make this apply once to the list's parent (i.e. its item in its parent List<T> or Item<T>) rather than applying to each item in its own list, which produces the undesired result
|
let mut content = content;
|
||||||
content.set_blend_mode(blend_mode);
|
let blend_mode = *blend_mode.element();
|
||||||
|
|
||||||
|
content.set_attribute(ATTR_BLEND_MODE, blend_mode);
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,64 +27,58 @@ fn blend_mode<T: SetBlendMode>(
|
|||||||
/// Opacity affects the transparency of the content (together with anything above which is clipped to it).
|
/// Opacity affects the transparency of the content (together with anything above which is clipped to it).
|
||||||
/// Fill affects the transparency of the content itself, independent of any content clipped to it.
|
/// Fill affects the transparency of the content itself, independent of any content clipped to it.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn opacity<T: MultiplyAlpha + MultiplyFill>(
|
fn opacity<T>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The layer stack that will be composited when rendering.
|
/// The content that will be composited when rendering.
|
||||||
#[implementations(
|
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)]
|
||||||
List<Graphic>,
|
content: Item<T>,
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<String>,
|
|
||||||
)]
|
|
||||||
mut content: T,
|
|
||||||
/// Whether the *Opacity* property is enabled, multiplying the existing opacity by the chosen percentage.
|
/// Whether the *Opacity* property is enabled, multiplying the existing opacity by the chosen percentage.
|
||||||
#[widget(ParsedWidgetOverride::Hidden)]
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
has_opacity: bool,
|
has_opacity: Item<bool>,
|
||||||
/// How visible the content should be, including any content clipped to it.
|
/// How visible the content should be, including any content clipped to it.
|
||||||
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
|
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
|
||||||
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
||||||
#[default(100.)]
|
#[default(100.)]
|
||||||
opacity: Percentage,
|
opacity: Item<Percentage>,
|
||||||
/// Whether the *Fill* property is enabled, multiplying the existing fill by the chosen percentage.
|
/// Whether the *Fill* property is enabled, multiplying the existing fill by the chosen percentage.
|
||||||
#[widget(ParsedWidgetOverride::Hidden)]
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
has_fill: bool,
|
has_fill: Item<bool>,
|
||||||
/// How visible the content should be, independent of any content clipped to it.
|
/// How visible the content should be, independent of any content clipped to it.
|
||||||
/// Ranges from 0% (fully transparent) to the default of 100% (fully opaque).
|
/// Ranges from 0% (fully transparent) to the default of 100% (fully opaque).
|
||||||
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
||||||
#[default(100.)]
|
#[default(100.)]
|
||||||
fill: Percentage,
|
fill: Item<Percentage>,
|
||||||
) -> T {
|
) -> Item<T> {
|
||||||
// TODO: Find a way to make this apply once to the list's parent (i.e. its item in its parent List<T> or Item<T>) rather than applying to each item in its own list, which produces the undesired result
|
let mut content = content;
|
||||||
|
let (has_opacity, opacity, has_fill, fill) = (*has_opacity.element(), *opacity.element(), *has_fill.element(), *fill.element());
|
||||||
|
|
||||||
if has_opacity {
|
if has_opacity {
|
||||||
content.multiply_alpha(opacity / 100.);
|
let multiplied = content.attribute_cloned_or(ATTR_OPACITY, 1.) * (opacity / 100.);
|
||||||
|
content.set_attribute(ATTR_OPACITY, multiplied);
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_fill {
|
if has_fill {
|
||||||
content.multiply_fill(fill / 100.);
|
let multiplied = content.attribute_cloned_or(ATTR_OPACITY_FILL, 1.) * (fill / 100.);
|
||||||
|
content.set_attribute(ATTR_OPACITY_FILL, multiplied);
|
||||||
}
|
}
|
||||||
|
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets whether the input graphics inherit the alpha of the content beneath them, "clipping" them to that content.
|
/// Sets whether the input graphics inherit the alpha of the content beneath them, "clipping" them to that content.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn clipping_mask<T: SetClip>(
|
fn clipping_mask<T>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The layer stack that will be composited when rendering.
|
/// The content that will be composited when rendering.
|
||||||
#[implementations(
|
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)]
|
||||||
List<Graphic>,
|
content: Item<T>,
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<String>,
|
|
||||||
)]
|
|
||||||
mut content: T,
|
|
||||||
/// Whether the content inherits the alpha of the content beneath it.
|
/// Whether the content inherits the alpha of the content beneath it.
|
||||||
clip: bool,
|
clip: Item<bool>,
|
||||||
) -> T {
|
) -> Item<T> {
|
||||||
// TODO: Find a way to make this apply once to the list's parent (i.e. its item in its parent List<T> or Item<T>) rather than applying to each item in its own list, which produces the undesired result
|
let mut content = content;
|
||||||
content.set_clip(clip);
|
let clip = *clip.element();
|
||||||
|
|
||||||
|
content.set_attribute(ATTR_CLIPPING_MASK, clip);
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::brush_cache::BrushCache;
|
use crate::brush_cache::BrushCache;
|
||||||
use crate::brush_stroke::{BrushStroke, BrushStyle};
|
use crate::brush_stroke::{BrushStyle, BrushTrace};
|
||||||
|
use core_types::ATTR_TRANSFORM;
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::color::{Alpha, Color, Pixel, Sample};
|
use core_types::color::{Alpha, Color, Pixel, Sample};
|
||||||
@@ -8,9 +9,7 @@ use core_types::list::{Item, List};
|
|||||||
use core_types::math::bbox::{AxisAlignedBbox, Bbox};
|
use core_types::math::bbox::{AxisAlignedBbox, Bbox};
|
||||||
use core_types::registry::FutureWrapperNode;
|
use core_types::registry::FutureWrapperNode;
|
||||||
use core_types::transform::Transform;
|
use core_types::transform::Transform;
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::value::ClonedNode;
|
use core_types::value::ClonedNode;
|
||||||
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
|
|
||||||
use core_types::{Ctx, Node};
|
use core_types::{Ctx, Node};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_nodes::blending_nodes::blend_colors;
|
use raster_nodes::blending_nodes::blend_colors;
|
||||||
@@ -83,10 +82,11 @@ fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f
|
|||||||
|
|
||||||
/// 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)]
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
fn blit<BlendFn>(mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>>
|
fn blit<BlendFn>(target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>>
|
||||||
where
|
where
|
||||||
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
|
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
|
||||||
{
|
{
|
||||||
|
let mut target = target;
|
||||||
if positions.is_empty() {
|
if positions.is_empty() {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ where
|
|||||||
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
|
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
|
||||||
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
|
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
|
||||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
|
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
|
||||||
let blank_texture = empty_image((), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
|
let blank_texture = empty_image((), Item::new_from_element(transform), Item::new_from_element(Color::TRANSPARENT));
|
||||||
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
|
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
|
||||||
|
|
||||||
image.into_element()
|
image.into_element()
|
||||||
@@ -191,18 +191,17 @@ pub fn blend_with_mode(background: Item<Raster<CPU>>, foreground: Item<Raster<CP
|
|||||||
async fn brush(
|
async fn brush(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// Optional raster content that may be drawn onto.
|
/// Optional raster content that may be drawn onto.
|
||||||
mut background: List<Raster<CPU>>,
|
background: Item<Raster<CPU>>,
|
||||||
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
|
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
|
||||||
trace: List<BrushStroke>,
|
trace: Item<BrushTrace>,
|
||||||
/// Internal cache data used to accelerate rendering of the brush content.
|
/// Internal cache data used to accelerate rendering of the brush content.
|
||||||
#[data]
|
#[data]
|
||||||
cache: BrushCache,
|
cache: BrushCache,
|
||||||
) -> List<Raster<CPU>> {
|
) -> Item<Raster<CPU>> {
|
||||||
if background.is_empty() {
|
let trace = trace.into_element().0;
|
||||||
background.push(Item::default());
|
|
||||||
}
|
let list_item = background;
|
||||||
// TODO: Find a way to handle more than one item
|
let mut result_item = list_item.clone();
|
||||||
let list_item = background.clone_item(0).expect("Expected the one item we just pushed");
|
|
||||||
|
|
||||||
let bounds = List::new_from_item(list_item.clone()).bounding_box(DAffine2::IDENTITY, false);
|
let bounds = List::new_from_item(list_item.clone()).bounding_box(DAffine2::IDENTITY, false);
|
||||||
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
|
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
|
||||||
@@ -223,10 +222,7 @@ async fn brush(
|
|||||||
|
|
||||||
let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes);
|
let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes);
|
||||||
|
|
||||||
// TODO: Find a way to handle more than one item
|
let mut actual_image = extend_image_to_bounds((), brush_plan.background, Item::new_from_element(background_bounds));
|
||||||
let Some(mut actual_image) = extend_image_to_bounds((), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else {
|
|
||||||
return List::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1);
|
let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1);
|
||||||
for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
|
for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
|
||||||
@@ -263,10 +259,9 @@ async fn brush(
|
|||||||
);
|
);
|
||||||
let blit_target = if idx == 0 {
|
let blit_target = if idx == 0 {
|
||||||
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
|
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
|
||||||
extend_image_to_bounds((), List::new_from_item(target), stroke_to_layer)
|
List::new_from_item(extend_image_to_bounds((), target, Item::new_from_element(stroke_to_layer)))
|
||||||
} else {
|
} else {
|
||||||
empty_image((), stroke_to_layer, List::new_from_element(Color::TRANSPARENT))
|
List::new_from_item(empty_image((), Item::new_from_element(stroke_to_layer), Item::new_from_element(Color::TRANSPARENT)))
|
||||||
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = blit_node.eval(blit_target).await;
|
let list = blit_node.eval(blit_target).await;
|
||||||
@@ -318,22 +313,14 @@ async fn brush(
|
|||||||
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
|
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The paint operation changes only the raster and its bounds, so set just the resulting transform; blending, opacity,
|
||||||
|
// clipping, and layer-path attributes carry through from the input `background` rather than being invented here.
|
||||||
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
|
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
let blend_mode: BlendMode = actual_image.attribute_cloned_or_default(ATTR_BLEND_MODE);
|
|
||||||
let opacity: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY, 1.);
|
|
||||||
let fill: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
|
||||||
let clip: bool = actual_image.attribute_cloned_or_default(ATTR_CLIPPING_MASK);
|
|
||||||
let layer: List<NodeId> = actual_image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
|
|
||||||
|
|
||||||
*background.element_mut(0).unwrap() = actual_image.into_element();
|
*result_item.element_mut() = actual_image.into_element();
|
||||||
background.set_attribute(ATTR_TRANSFORM, 0, transform);
|
result_item.set_attribute(ATTR_TRANSFORM, transform);
|
||||||
background.set_attribute(ATTR_BLEND_MODE, 0, blend_mode);
|
|
||||||
background.set_attribute(ATTR_OPACITY, 0, opacity);
|
|
||||||
background.set_attribute(ATTR_OPACITY_FILL, 0, fill);
|
|
||||||
background.set_attribute(ATTR_CLIPPING_MASK, 0, clip);
|
|
||||||
background.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer);
|
|
||||||
|
|
||||||
background
|
result_item
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blend_image_closure(foreground: Item<Raster<CPU>>, mut background: Item<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> Item<Raster<CPU>> {
|
pub fn blend_image_closure(foreground: Item<Raster<CPU>>, mut background: Item<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> Item<Raster<CPU>> {
|
||||||
@@ -404,6 +391,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;
|
||||||
|
|
||||||
@@ -421,8 +409,8 @@ mod test {
|
|||||||
let image = brush(
|
let image = brush(
|
||||||
(),
|
(),
|
||||||
&BrushCache::default(),
|
&BrushCache::default(),
|
||||||
List::new_from_element(Raster::new_cpu(Image::<Color>::default())),
|
Item::new_from_element(Raster::new_cpu(Image::<Color>::default())),
|
||||||
List::new_from_element(BrushStroke {
|
Item::new_from_element(BrushTrace::from(vec![BrushStroke {
|
||||||
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
|
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
|
||||||
style: BrushStyle {
|
style: BrushStyle {
|
||||||
color: Color::BLACK,
|
color: Color::BLACK,
|
||||||
@@ -432,9 +420,9 @@ mod test {
|
|||||||
spacing: 20.,
|
spacing: 20.,
|
||||||
blend_mode: BlendMode::Normal,
|
blend_mode: BlendMode::Normal,
|
||||||
},
|
},
|
||||||
}),
|
}])),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(image.element(0).unwrap().width, 20);
|
assert_eq!(image.element().width, 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use core_types::CacheHash;
|
use core_types::CacheHash;
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
|
use core_types::list::{Item, List};
|
||||||
use core_types::math::bbox::AxisAlignedBbox;
|
use core_types::math::bbox::AxisAlignedBbox;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
@@ -57,6 +58,22 @@ pub struct BrushStroke {
|
|||||||
pub trace: Vec<BrushInputSample>,
|
pub trace: Vec<BrushInputSample>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One Brush layer's full sequence of strokes, treated as a single rank-0 value rather than a frame of independent strokes.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, CacheHash, DynAny)]
|
||||||
|
pub struct BrushTrace(pub List<BrushStroke>);
|
||||||
|
|
||||||
|
impl From<List<BrushStroke>> for BrushTrace {
|
||||||
|
fn from(strokes: List<BrushStroke>) -> Self {
|
||||||
|
Self(strokes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<BrushStroke>> for BrushTrace {
|
||||||
|
fn from(strokes: Vec<BrushStroke>) -> Self {
|
||||||
|
Self(strokes.into_iter().map(Item::new_from_element).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BrushStroke {
|
impl BrushStroke {
|
||||||
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
||||||
let radius = self.style.diameter / 2.;
|
let radius = self.style.diameter / 2.;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use core_types::list::List;
|
use core_types::list::{Item, List};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
|
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::vector_types::GradientStops;
|
use graphic_types::vector_types::Gradient;
|
||||||
use graphic_types::{Artboard, Graphic, Vector};
|
use graphic_types::{Artboard, Graphic, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
|
|
||||||
@@ -33,19 +33,22 @@ fn real_time(
|
|||||||
ctx: impl Ctx + ExtractRealTime,
|
ctx: impl Ctx + ExtractRealTime,
|
||||||
_primary: (),
|
_primary: (),
|
||||||
/// The time and date component to be produced as a number.
|
/// The time and date component to be produced as a number.
|
||||||
component: RealTimeMode,
|
component: Item<RealTimeMode>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
|
let component = component.into_element();
|
||||||
let real_time = ctx.try_real_time().unwrap_or_default();
|
let real_time = ctx.try_real_time().unwrap_or_default();
|
||||||
|
|
||||||
// TODO: Implement proper conversion using and existing time implementation
|
// TODO: Implement proper conversion using and existing time implementation
|
||||||
match component {
|
let result = match component {
|
||||||
RealTimeMode::Utc => real_time,
|
RealTimeMode::Utc => real_time,
|
||||||
RealTimeMode::Year => (real_time / DAY / 365.25).floor() + 1970., // TODO: Factor in a chosen timezone
|
RealTimeMode::Year => (real_time / DAY / 365.25).floor() + 1970., // TODO: Factor in a chosen timezone
|
||||||
RealTimeMode::Hour => (real_time / 1000. / 3600.).floor() % 24., // TODO: Factor in a chosen timezone
|
RealTimeMode::Hour => (real_time / 1000. / 3600.).floor() % 24., // TODO: Factor in a chosen timezone
|
||||||
RealTimeMode::Minute => (real_time / 1000. / 60.).floor() % 60., // TODO: Factor in a chosen timezone
|
RealTimeMode::Minute => (real_time / 1000. / 60.).floor() % 60., // TODO: Factor in a chosen timezone
|
||||||
RealTimeMode::Second => (real_time / 1000.).floor() % 60.,
|
RealTimeMode::Second => (real_time / 1000.).floor() % 60.,
|
||||||
RealTimeMode::Millisecond => real_time % 1000.,
|
RealTimeMode::Millisecond => real_time % 1000.,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Item::new_from_element(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the time, in seconds on the timeline, since the beginning of animation playback.
|
/// Produces the time, in seconds on the timeline, since the beginning of animation playback.
|
||||||
@@ -55,42 +58,50 @@ fn animation_time(
|
|||||||
_primary: (),
|
_primary: (),
|
||||||
#[default(1)]
|
#[default(1)]
|
||||||
#[unit("/sec")]
|
#[unit("/sec")]
|
||||||
rate: f64,
|
rate: Item<f64>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
ctx.try_animation_time().unwrap_or_default() * rate
|
Item::new_from_element(ctx.try_animation_time().unwrap_or_default() * *rate.element())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Debug"))]
|
#[node_macro::node(category("Debug"))]
|
||||||
async fn quantize_real_time<T>(
|
async fn quantize_real_time<T>(
|
||||||
ctx: impl Ctx + ExtractAll + CloneVarArgs,
|
ctx: impl Ctx + ExtractAll + CloneVarArgs,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Context -> bool,
|
Context -> Item<bool>,
|
||||||
Context -> u32,
|
Context -> Item<u32>,
|
||||||
Context -> u64,
|
Context -> Item<u64>,
|
||||||
Context -> f32,
|
Context -> Item<f32>,
|
||||||
Context -> f64,
|
Context -> Item<f64>,
|
||||||
Context -> String,
|
Context -> Item<String>,
|
||||||
Context -> DAffine2,
|
Context -> Item<DAffine2>,
|
||||||
Context -> Footprint,
|
Context -> Item<Footprint>,
|
||||||
Context -> DVec2,
|
Context -> Item<DVec2>,
|
||||||
|
Context -> Item<Vector>,
|
||||||
|
Context -> Item<Graphic>,
|
||||||
|
Context -> Item<Raster<CPU>>,
|
||||||
|
Context -> Item<Raster<GPU>>,
|
||||||
|
Context -> Item<Color>,
|
||||||
|
Context -> Item<Gradient>,
|
||||||
|
Context -> Item<Artboard>,
|
||||||
|
Context -> List<String>,
|
||||||
|
Context -> List<f64>,
|
||||||
|
Context -> List<DVec2>,
|
||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Graphic>,
|
Context -> List<Graphic>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
|
Context -> List<Gradient>,
|
||||||
Context -> List<Artboard>,
|
Context -> List<Artboard>,
|
||||||
Context -> List<GradientStops>,
|
|
||||||
Context -> List<String>,
|
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
|
||||||
)]
|
)]
|
||||||
value: impl Node<'n, Context<'static>, Output = T>,
|
value: impl Node<'n, Context<'static>, Output = T>,
|
||||||
#[default(1)]
|
#[default(1)]
|
||||||
#[unit("sec")]
|
#[unit("sec")]
|
||||||
quantum: f64,
|
quantum: Item<f64>,
|
||||||
) -> T {
|
) -> T {
|
||||||
let time = ctx.try_real_time().unwrap_or_default();
|
let time = ctx.try_real_time().unwrap_or_default();
|
||||||
let time = time / 1000.;
|
let time = time / 1000.;
|
||||||
|
let quantum = quantum.into_element();
|
||||||
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
|
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
|
||||||
if !quantized_time.is_finite() {
|
if !quantized_time.is_finite() {
|
||||||
quantized_time = time;
|
quantized_time = time;
|
||||||
@@ -104,32 +115,40 @@ async fn quantize_real_time<T>(
|
|||||||
async fn quantize_animation_time<T>(
|
async fn quantize_animation_time<T>(
|
||||||
ctx: impl Ctx + ExtractAll + CloneVarArgs,
|
ctx: impl Ctx + ExtractAll + CloneVarArgs,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Context -> bool,
|
Context -> Item<bool>,
|
||||||
Context -> u32,
|
Context -> Item<u32>,
|
||||||
Context -> u64,
|
Context -> Item<u64>,
|
||||||
Context -> f32,
|
Context -> Item<f32>,
|
||||||
Context -> f64,
|
Context -> Item<f64>,
|
||||||
Context -> String,
|
Context -> Item<String>,
|
||||||
Context -> DAffine2,
|
Context -> Item<DAffine2>,
|
||||||
Context -> Footprint,
|
Context -> Item<Footprint>,
|
||||||
Context -> DVec2,
|
Context -> Item<DVec2>,
|
||||||
|
Context -> Item<Vector>,
|
||||||
|
Context -> Item<Graphic>,
|
||||||
|
Context -> Item<Raster<CPU>>,
|
||||||
|
Context -> Item<Raster<GPU>>,
|
||||||
|
Context -> Item<Color>,
|
||||||
|
Context -> Item<Gradient>,
|
||||||
|
Context -> Item<Artboard>,
|
||||||
|
Context -> List<String>,
|
||||||
|
Context -> List<f64>,
|
||||||
|
Context -> List<DVec2>,
|
||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Graphic>,
|
Context -> List<Graphic>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
|
Context -> List<Gradient>,
|
||||||
Context -> List<Artboard>,
|
Context -> List<Artboard>,
|
||||||
Context -> List<GradientStops>,
|
|
||||||
Context -> List<String>,
|
|
||||||
Context -> List<f64>,
|
|
||||||
Context -> (),
|
|
||||||
)]
|
)]
|
||||||
value: impl Node<'n, Context<'static>, Output = T>,
|
value: impl Node<'n, Context<'static>, Output = T>,
|
||||||
#[default(1)]
|
#[default(1)]
|
||||||
#[unit("sec")]
|
#[unit("sec")]
|
||||||
quantum: f64,
|
quantum: Item<f64>,
|
||||||
) -> T {
|
) -> T {
|
||||||
let time = ctx.try_animation_time().unwrap_or_default();
|
let time = ctx.try_animation_time().unwrap_or_default();
|
||||||
|
let quantum = quantum.into_element();
|
||||||
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
|
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
|
||||||
if !quantized_time.is_finite() {
|
if !quantized_time.is_finite() {
|
||||||
quantized_time = time;
|
quantized_time = time;
|
||||||
@@ -140,8 +159,8 @@ async fn quantize_animation_time<T>(
|
|||||||
|
|
||||||
/// Produces the current position of the user's pointer within the document canvas.
|
/// Produces the current position of the user's pointer within the document canvas.
|
||||||
#[node_macro::node(category("Animation"))]
|
#[node_macro::node(category("Animation"))]
|
||||||
fn pointer_position(ctx: impl Ctx + ExtractPointerPosition) -> DVec2 {
|
fn pointer_position(ctx: impl Ctx + ExtractPointerPosition) -> Item<DVec2> {
|
||||||
ctx.try_pointer_position().unwrap_or_default()
|
Item::new_from_element(ctx.try_pointer_position().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: These nodes require more sophisticated algorithms for giving the correct result
|
// TODO: These nodes require more sophisticated algorithms for giving the correct result
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use core_types::list::List;
|
use core_types::list::Item;
|
||||||
use core_types::{Color, ExtractVarArgs};
|
use core_types::{Color, ExtractVarArgs};
|
||||||
use core_types::{Ctx, ExtractIndex, ExtractPosition};
|
use core_types::{Ctx, ExtractIndex, ExtractPosition};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphic_types::vector_types::GradientStops;
|
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))]
|
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
||||||
fn read_graphic(ctx: impl Ctx + ExtractVarArgs) -> List<Graphic> {
|
fn read_graphic(ctx: impl Ctx + ExtractVarArgs) -> Item<Graphic> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ fn read_graphic(ctx: impl Ctx + ExtractVarArgs) -> List<Graphic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
||||||
fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> List<Vector> {
|
fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> Item<Vector> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> List<Vector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
||||||
fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> List<Raster<CPU>> {
|
fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> Item<Raster<CPU>> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> List<Raster<CPU>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
||||||
fn read_color(ctx: impl Ctx + ExtractVarArgs) -> List<Color> {
|
fn read_color(ctx: impl Ctx + ExtractVarArgs) -> Item<Color> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ fn read_color(ctx: impl Ctx + ExtractVarArgs) -> List<Color> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
#[node_macro::node(category("Context"), path(graphene_core::vector))]
|
||||||
fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<GradientStops> {
|
fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> Item<Gradient> {
|
||||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||||
let var_arg = var_arg as &dyn std::any::Any;
|
let var_arg = var_arg as &dyn std::any::Any;
|
||||||
|
|
||||||
@@ -53,9 +53,10 @@ async fn read_position(
|
|||||||
/// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels.
|
/// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels.
|
||||||
///
|
///
|
||||||
/// In programming terms: inside the double loop `i { j { ... } }`, *Loop Level* 0 = `j` and 1 = `i`. After inserting a third loop `k { ... }`, inside it, levels would be 0 = `k`, 1 = `j`, and 2 = `i`.
|
/// In programming terms: inside the double loop `i { j { ... } }`, *Loop Level* 0 = `j` and 1 = `i`. After inserting a third loop `k { ... }`, inside it, levels would be 0 = `k`, 1 = `j`, and 2 = `i`.
|
||||||
loop_level: u32,
|
loop_level: Item<u32>,
|
||||||
) -> DVec2 {
|
) -> Item<DVec2> {
|
||||||
ctx.try_position().and_then(|mut iter| iter.nth(loop_level as usize).or_else(|| iter.last())).unwrap_or(DVec2::ZERO)
|
let loop_level = *loop_level.element();
|
||||||
|
Item::new_from_element(ctx.try_position().and_then(|mut iter| iter.nth(loop_level as usize).or_else(|| iter.last())).unwrap_or(DVec2::ZERO))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
||||||
@@ -70,7 +71,8 @@ async fn read_index(
|
|||||||
/// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels.
|
/// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels.
|
||||||
///
|
///
|
||||||
/// In programming terms: inside the double loop `i { j { ... } }`, *Loop Level* 0 = `j` and 1 = `i`. After inserting a third loop `k { ... }`, inside it, levels would be 0 = `k`, 1 = `j`, and 2 = `i`.
|
/// In programming terms: inside the double loop `i { j { ... } }`, *Loop Level* 0 = `j` and 1 = `i`. After inserting a third loop `k { ... }`, inside it, levels would be 0 = `k`, 1 = `j`, and 2 = `i`.
|
||||||
loop_level: u32,
|
loop_level: Item<u32>,
|
||||||
) -> f64 {
|
) -> Item<f64> {
|
||||||
ctx.try_index().and_then(|mut iter| iter.nth(loop_level as usize).or_else(|| iter.last())).unwrap_or(0) as f64
|
let loop_level = *loop_level.element();
|
||||||
|
Item::new_from_element(ctx.try_index().and_then(|mut iter| iter.nth(loop_level as usize).or_else(|| iter.last())).unwrap_or(0) as f64)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use core::f64;
|
use core::f64;
|
||||||
use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll};
|
use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll};
|
||||||
use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
|
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::{Color, OwnedContextImpl};
|
use core_types::{Color, OwnedContextImpl};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::vector_types::GradientStops;
|
use graphic_types::vector_types::Gradient;
|
||||||
use graphic_types::{Artboard, Graphic, Vector};
|
use graphic_types::{Artboard, Graphic, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
|
|
||||||
@@ -16,36 +15,41 @@ async fn context_modification<T>(
|
|||||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||||
/// The data to pass through, evaluated with the stripped down context.
|
/// The data to pass through, evaluated with the stripped down context.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Context -> (),
|
Context -> Item<bool>,
|
||||||
Context -> bool,
|
Context -> Item<u32>,
|
||||||
Context -> u32,
|
Context -> Item<u64>,
|
||||||
Context -> u64,
|
Context -> Item<f32>,
|
||||||
Context -> f32,
|
Context -> Item<f64>,
|
||||||
Context -> f64,
|
Context -> Item<String>,
|
||||||
Context -> String,
|
Context -> Item<DAffine2>,
|
||||||
Context -> DAffine2,
|
Context -> Item<Footprint>,
|
||||||
Context -> Footprint,
|
Context -> Item<DVec2>,
|
||||||
Context -> DVec2,
|
Context -> Item<Vector>,
|
||||||
|
Context -> Item<Graphic>,
|
||||||
|
Context -> Item<Raster<CPU>>,
|
||||||
|
Context -> Item<Raster<GPU>>,
|
||||||
|
Context -> Item<Color>,
|
||||||
|
Context -> Item<Artboard>,
|
||||||
|
Context -> Item<Gradient>,
|
||||||
|
Context -> Item<NodeIdPath>,
|
||||||
|
Context -> Item<AttributeValueDyn>,
|
||||||
Context -> List<String>,
|
Context -> List<String>,
|
||||||
Context -> List<NodeId>,
|
|
||||||
Context -> List<f64>,
|
Context -> List<f64>,
|
||||||
Context -> List<u8>,
|
Context -> List<DVec2>,
|
||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Graphic>,
|
Context -> List<Graphic>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
Context -> List<Artboard>,
|
Context -> List<Artboard>,
|
||||||
Context -> List<GradientStops>,
|
Context -> List<Gradient>,
|
||||||
Context -> AttributeDyn,
|
|
||||||
Context -> AttributeValueDyn,
|
|
||||||
Context -> ListDyn,
|
Context -> ListDyn,
|
||||||
)]
|
)]
|
||||||
value: impl Node<Context<'static>, Output = T>,
|
value: impl Node<Context<'static>, Output = T>,
|
||||||
/// The parts of the context to keep when evaluating the input value. All other parts are nullified.
|
/// The parts of the context to keep when evaluating the input value. All other parts are nullified.
|
||||||
features_to_keep: ContextFeatures,
|
features_to_keep: Item<ContextFeatures>,
|
||||||
) -> T {
|
) -> T {
|
||||||
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep);
|
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep.into_element());
|
||||||
|
|
||||||
value.eval(Some(new_context.into())).await
|
value.eval(Some(new_context.into())).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
use core_types::Ctx;
|
use core_types::Ctx;
|
||||||
use core_types::list::List;
|
use core_types::list::Item;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_types::{CPU, Raster};
|
|
||||||
|
|
||||||
/// Meant for debugging purposes, not general use. Logs the input value to the console and passes it through unchanged.
|
/// Meant for debugging purposes, not general use. Logs the input value to the console and passes it through unchanged.
|
||||||
#[node_macro::node(category("Debug"), name("Log to Console"))]
|
#[node_macro::node(category("Debug"), name("Log to Console"))]
|
||||||
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(bool, f64, u32, u64, DVec2, DAffine2, String)] value: T) -> T {
|
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(bool, f64, u32, u64, DVec2, DAffine2, String)] value: Item<T>) -> Item<T> {
|
||||||
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
|
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
|
||||||
log::debug!("{value:#?}");
|
log::debug!("{value:#?}");
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
|
|
||||||
#[node_macro::node(category("Debug"))]
|
|
||||||
fn size_of(_: impl Ctx, ty: core_types::Type) -> Option<usize> {
|
|
||||||
ty.size()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
|
|
||||||
#[node_macro::node(category("Debug"))]
|
|
||||||
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String)] input: T) -> Option<T> {
|
|
||||||
Some(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
|
|
||||||
#[node_macro::node(category("Debug"))]
|
|
||||||
fn unwrap_option<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<u32>, Option<u64>, Option<String>)] input: Option<T>) -> T {
|
|
||||||
input.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meant for debugging purposes, not general use. Clones the input value.
|
|
||||||
#[node_macro::node(category("Debug"))]
|
|
||||||
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&List<Raster<CPU>>)] value: &'i T) -> T {
|
|
||||||
value.clone()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use core_types::list::Item;
|
||||||
use core_types::{CacheHash, Ctx};
|
use core_types::{CacheHash, Ctx};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DVec2, IVec2, UVec2};
|
use glam::{DVec2, IVec2, UVec2};
|
||||||
@@ -6,11 +7,16 @@ use glam::{DVec2, IVec2, UVec2};
|
|||||||
///
|
///
|
||||||
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
|
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
|
||||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: Item<T>, axis: Item<XY>) -> Item<f64> {
|
||||||
match axis {
|
let vector = vector.into_element();
|
||||||
|
let axis = axis.into_element();
|
||||||
|
|
||||||
|
let result = match axis {
|
||||||
XY::X => vector.into().x,
|
XY::X => vector.into().x,
|
||||||
XY::Y => vector.into().y,
|
XY::Y => vector.into().y,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Item::new_from_element(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The X or Y component of a vec2.
|
/// The X or Y component of a vec2.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use core_types::graphene_hash::CacheHash;
|
||||||
|
use core_types::list::{AttributeValueDyn, Bundle, Item, List};
|
||||||
use core_types::{Ctx, ExtractFootprint, ops::Convert, transform::Footprint};
|
use core_types::{Ctx, ExtractFootprint, ops::Convert, transform::Footprint};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
@@ -10,14 +12,92 @@ fn passthrough<'i, T: 'i + 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<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O {
|
fn into<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O {
|
||||||
value.into()
|
value.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raises an `Item` wire onto a `List` connector as its one-element list.
|
||||||
#[node_macro::node(category(""), skip_impl)]
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
async fn convert<'i, T: 'i + Send + Convert<O, C>, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData<O>) -> O {
|
fn item_to_list<'i, T: 'i + Send>(_: impl Ctx, value: Item<T>) -> List<T> {
|
||||||
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await
|
value.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boxes a ranked wire's element into a type-erased attribute value, carrying the cell's attributes through the wire.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn item_to_attribute_value<'i, T: 'i + Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static>(
|
||||||
|
_: impl Ctx,
|
||||||
|
value: Item<T>,
|
||||||
|
_element_ty: PhantomData<AttributeValueDyn>,
|
||||||
|
) -> Item<AttributeValueDyn> {
|
||||||
|
let (element, attributes) = value.into_parts();
|
||||||
|
Item::from_parts(AttributeValueDyn(Box::new(element)), attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boxes a whole `List` wire as one type-erased attribute value, for attributes whose per-item value is itself a collection.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn list_to_attribute_value<'i, T: 'i + Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static>(
|
||||||
|
_: impl Ctx,
|
||||||
|
value: T,
|
||||||
|
_element_ty: PhantomData<AttributeValueDyn>,
|
||||||
|
) -> Item<AttributeValueDyn> {
|
||||||
|
Item::new_from_element(AttributeValueDyn(Box::new(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a whole `List` onto the wire as one rank-0 `Item<Bundle<T>>` so an entire collection can feed a connector that carries it as one opaque cell.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn bundle<'i, T: 'i + Send>(_: impl Ctx, value: List<T>) -> Item<Bundle<T>> {
|
||||||
|
Item::new_from_element(Bundle(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwraps a `Bundle` wire back into the whole `List` it carries, restoring the collection after it passed through a connector as one opaque cell.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn unbundle<'i, T: 'i + Send>(_: impl Ctx, value: Item<Bundle<T>>) -> List<T> {
|
||||||
|
value.into_element().0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an `Item` wire's element to a different element type it can produce through the std `Into` trait,
|
||||||
|
/// letting a convertible wire feed an `Item` connector whose element type it does not match by identity.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn into_item<'i, T: 'i + Send + Into<E>, E: 'i + Send>(_: impl Ctx, value: Item<T>, _element_ty: PhantomData<E>) -> Item<E> {
|
||||||
|
let (value, attributes) = value.into_parts();
|
||||||
|
Item::from_parts(value.into(), attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `List` counterpart of `into_item`, converting every element to a different element type it can produce.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
fn into_list<'i, T: 'i + Send + Into<E>, E: 'i + Send>(_: impl Ctx, value: List<T>, _element_ty: PhantomData<E>) -> List<E> {
|
||||||
|
value
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| {
|
||||||
|
let (value, attributes) = item.into_parts();
|
||||||
|
Item::from_parts(value.into(), attributes)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`Convert`]-based counterpart of `into_item`, casting an `Item` wire's element to a connector's numeric element type.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
async fn convert_item<'i, T: 'i + Send + Convert<E, ()>, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: Item<T>, _element_ty: PhantomData<E>) -> Item<E> {
|
||||||
|
let footprint = *ctx.try_footprint().unwrap_or(&Footprint::DEFAULT);
|
||||||
|
let (value, attributes) = value.into_parts();
|
||||||
|
|
||||||
|
Item::from_parts(value.convert(footprint, ()).await, attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `List` counterpart of `convert_item`, casting every element to the connector's numeric element type.
|
||||||
|
#[node_macro::node(category(""), skip_impl)]
|
||||||
|
async fn convert_list<'i, T: 'i + Send + Convert<E, ()>, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: List<T>, _element_ty: PhantomData<E>) -> List<E> {
|
||||||
|
let footprint = *ctx.try_footprint().unwrap_or(&Footprint::DEFAULT);
|
||||||
|
|
||||||
|
let mut result = List::default();
|
||||||
|
for item in value.into_iter() {
|
||||||
|
let (value, attributes) = item.into_parts();
|
||||||
|
result.push(Item::from_parts(value.convert(footprint, ()).await, attributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use glam::{DAffine2, DVec2};
|
|||||||
use graphic_types::graphic::{Graphic, IntoGraphicList};
|
use graphic_types::graphic::{Graphic, IntoGraphicList};
|
||||||
use graphic_types::{Artboard, Vector};
|
use graphic_types::{Artboard, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
|
|
||||||
/// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes.
|
/// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes.
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
@@ -19,20 +19,22 @@ pub async fn create_artboard<T: IntoGraphicList>(
|
|||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Raster<GPU>>,
|
Context -> List<Raster<GPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
Context -> List<GradientStops>,
|
Context -> List<Gradient>,
|
||||||
Context -> DAffine2,
|
Context -> Item<DAffine2>,
|
||||||
)]
|
)]
|
||||||
content: impl Node<Context<'static>, Output = T>,
|
content: impl Node<Context<'static>, Output = T>,
|
||||||
/// Coordinate of the top-left corner of the artboard within the document.
|
/// Coordinate of the top-left corner of the artboard within the document.
|
||||||
location: DVec2,
|
location: Item<DVec2>,
|
||||||
/// Width and height of the artboard within the document.
|
/// Width and height of the artboard within the document.
|
||||||
dimensions: DVec2,
|
dimensions: Item<DVec2>,
|
||||||
/// Color of the artboard background.
|
/// Color of the artboard background.
|
||||||
background: List<Color>,
|
background: Item<Color>,
|
||||||
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
|
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
|
||||||
#[default(true)]
|
#[default(true)]
|
||||||
clip: bool,
|
clip: Item<bool>,
|
||||||
) -> List<Artboard> {
|
) -> Item<Artboard> {
|
||||||
|
let (location, dimensions, clip) = (location.into_element(), dimensions.into_element(), clip.into_element());
|
||||||
|
|
||||||
let footprint = ctx.try_footprint().copied();
|
let footprint = ctx.try_footprint().copied();
|
||||||
let mut new_ctx = OwnedContextImpl::from(ctx);
|
let mut new_ctx = OwnedContextImpl::from(ctx);
|
||||||
if let Some(mut footprint) = footprint {
|
if let Some(mut footprint) = footprint {
|
||||||
@@ -47,14 +49,12 @@ pub async fn create_artboard<T: IntoGraphicList>(
|
|||||||
let normalized_location = location.min(location + dimensions);
|
let normalized_location = location.min(location + dimensions);
|
||||||
let normalized_dimensions = dimensions.abs().max(DVec2::ONE);
|
let normalized_dimensions = dimensions.abs().max(DVec2::ONE);
|
||||||
|
|
||||||
let background = background.element(0).copied().unwrap_or(Color::WHITE);
|
let background = background.into_element();
|
||||||
|
|
||||||
// Name is not stored here, it's resolved live from the parent layer's display name
|
// Name is not stored here, it's resolved live from the parent layer's display name
|
||||||
List::new_from_item(
|
Item::new_from_element(Artboard::new(content))
|
||||||
Item::new_from_element(Artboard::new(content))
|
.with_attribute(ATTR_LOCATION, normalized_location)
|
||||||
.with_attribute(ATTR_LOCATION, normalized_location)
|
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
|
||||||
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
|
.with_attribute(ATTR_BACKGROUND, background)
|
||||||
.with_attribute(ATTR_BACKGROUND, background)
|
.with_attribute(ATTR_CLIP, clip)
|
||||||
.with_attribute(ATTR_CLIP, clip),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,42 @@
|
|||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
|
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
|
||||||
use core_types::registry::types::{Angle, SignedInteger};
|
use core_types::registry::types::{Angle, SignedInteger};
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{Graphic, IntoGraphicList};
|
use graphic_types::graphic::{Graphic, IntoGraphicList};
|
||||||
use graphic_types::{Artboard, Vector};
|
use graphic_types::{Artboard, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use vector_types::gradient::{GradientSpreadMethod, GradientType};
|
use vector_types::gradient::{GradientSpreadMethod, GradientType};
|
||||||
use vector_types::{GradientStop, GradientStops, ReferencePoint};
|
use vector_types::{Gradient, GradientStop, ReferencePoint};
|
||||||
|
|
||||||
/// Returns the value at the specified index in the list.
|
/// Returns the list with the item at the specified index removed.
|
||||||
/// If no value exists at that index, the type's default value is returned.
|
|
||||||
#[node_macro::node(category("General"))]
|
|
||||||
pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
|
|
||||||
_: impl Ctx,
|
|
||||||
/// The list of data.
|
|
||||||
#[implementations(
|
|
||||||
List<Artboard>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Raster<GPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<String>,
|
|
||||||
List<f64>,
|
|
||||||
List<u8>,
|
|
||||||
List<NodeId>,
|
|
||||||
)]
|
|
||||||
list: T,
|
|
||||||
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
|
||||||
index: SignedInteger,
|
|
||||||
) -> T::Output
|
|
||||||
where
|
|
||||||
T::Output: Clone + Default,
|
|
||||||
{
|
|
||||||
let index = index as i32;
|
|
||||||
|
|
||||||
if index < 0 { list.at_index_from_end(-index as usize) } else { list.at_index(index as usize) }.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the list with the element at the specified index removed.
|
|
||||||
/// If no value exists at that index, the list is returned unchanged.
|
/// If no value exists at that index, the list is returned unchanged.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"), name("Remove at Index"))]
|
||||||
pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
|
pub fn remove_at_index<T: graphic_types::graphic::OmitIndex + Clone + Default>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The list of data.
|
/// The list of data.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<String>,
|
List<String>,
|
||||||
List<Artboard>,
|
List<bool>,
|
||||||
List<Graphic>,
|
List<f32>,
|
||||||
|
List<f64>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<DVec2>,
|
||||||
|
List<DAffine2>,
|
||||||
List<Vector>,
|
List<Vector>,
|
||||||
|
List<Graphic>,
|
||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
List<Gradient>,
|
||||||
|
List<Artboard>,
|
||||||
)]
|
)]
|
||||||
list: T,
|
list: T,
|
||||||
/// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
/// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||||
index: SignedInteger,
|
index: Item<SignedInteger>,
|
||||||
) -> T {
|
) -> T {
|
||||||
let index = index as i32;
|
let index = index.into_element() as i32;
|
||||||
|
|
||||||
if index < 0 {
|
if index < 0 {
|
||||||
list.omit_index_from_end(index.unsigned_abs() as usize)
|
list.omit_index_from_end(index.unsigned_abs() as usize)
|
||||||
@@ -70,62 +45,83 @@ pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the bare element (without the item's attributes) at the specified index in a `List`.
|
/// Returns the item at the specified index in a `List`, keeping its attributes.
|
||||||
/// Use this when downstream nodes want just the inner value rather than a `List` containing a single item.
|
|
||||||
/// If no value exists at that index, the element type's default is returned.
|
/// If no value exists at that index, the element type's default is returned.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"), name("Item at Index"))]
|
||||||
pub fn extract_element<T: Clone + Default + Send + Sync + 'static>(
|
pub fn item_at_index<T: Clone + Default + Send + Sync + 'static>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The `List` of data to extract from.
|
/// The `List` of data to take the item from.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<String>,
|
List<String>,
|
||||||
|
List<bool>,
|
||||||
|
List<f32>,
|
||||||
List<f64>,
|
List<f64>,
|
||||||
List<u8>,
|
List<u32>,
|
||||||
List<NodeId>,
|
List<u64>,
|
||||||
List<Color>,
|
List<DVec2>,
|
||||||
List<GradientStops>,
|
List<DAffine2>,
|
||||||
List<Vector>,
|
List<Vector>,
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Graphic>,
|
List<Graphic>,
|
||||||
|
List<Raster<CPU>>,
|
||||||
|
List<Color>,
|
||||||
|
List<Gradient>,
|
||||||
List<Artboard>,
|
List<Artboard>,
|
||||||
)]
|
)]
|
||||||
list: List<T>,
|
list: List<T>,
|
||||||
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||||
index: SignedInteger,
|
index: Item<SignedInteger>,
|
||||||
) -> T {
|
) -> Item<T> {
|
||||||
let len = list.len();
|
let len = list.len();
|
||||||
let index = index as i32;
|
let index = index.into_element() as i32;
|
||||||
let resolved = if index < 0 {
|
let resolved = if index < 0 {
|
||||||
let from_end = index.unsigned_abs() as usize;
|
let from_end = index.unsigned_abs() as usize;
|
||||||
if from_end > len {
|
if from_end > len {
|
||||||
return T::default();
|
return Item::default();
|
||||||
}
|
}
|
||||||
len - from_end
|
len - from_end
|
||||||
} else {
|
} else {
|
||||||
index as usize
|
index as usize
|
||||||
};
|
};
|
||||||
list.element(resolved).cloned().unwrap_or_default()
|
list.clone_item(resolved).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
async fn map<Item: AnyHash + Send + Sync + CacheHash>(
|
async fn map<Item: AnyHash + Send + Sync + CacheHash>(
|
||||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<String>,
|
List<String>,
|
||||||
|
List<bool>,
|
||||||
|
List<f32>,
|
||||||
|
List<f64>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<DVec2>,
|
||||||
|
List<DAffine2>,
|
||||||
|
List<Vector>,
|
||||||
|
List<Graphic>,
|
||||||
|
List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>,
|
||||||
|
List<Color>,
|
||||||
|
List<Gradient>,
|
||||||
|
List<Artboard>,
|
||||||
)]
|
)]
|
||||||
content: List<Item>,
|
content: List<Item>,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Context -> List<Graphic>,
|
|
||||||
Context -> List<Vector>,
|
|
||||||
Context -> List<Raster<CPU>>,
|
|
||||||
Context -> List<Color>,
|
|
||||||
Context -> List<GradientStops>,
|
|
||||||
Context -> List<String>,
|
Context -> List<String>,
|
||||||
|
Context -> List<bool>,
|
||||||
|
Context -> List<f32>,
|
||||||
|
Context -> List<f64>,
|
||||||
|
Context -> List<u32>,
|
||||||
|
Context -> List<u64>,
|
||||||
|
Context -> List<DVec2>,
|
||||||
|
Context -> List<DAffine2>,
|
||||||
|
Context -> List<Vector>,
|
||||||
|
Context -> List<Graphic>,
|
||||||
|
Context -> List<Raster<CPU>>,
|
||||||
|
Context -> List<Raster<GPU>>,
|
||||||
|
Context -> List<Color>,
|
||||||
|
Context -> List<Gradient>,
|
||||||
|
Context -> List<Artboard>,
|
||||||
)]
|
)]
|
||||||
mapped: impl Node<Context<'static>, Output = List<Item>>,
|
mapped: impl Node<Context<'static>, Output = List<Item>>,
|
||||||
) -> List<Item> {
|
) -> List<Item> {
|
||||||
@@ -133,7 +129,7 @@ async fn map<Item: AnyHash + Send + Sync + CacheHash>(
|
|||||||
|
|
||||||
for (i, row) in content.into_iter().enumerate() {
|
for (i, row) in content.into_iter().enumerate() {
|
||||||
let owned_ctx = OwnedContextImpl::from(ctx.clone());
|
let owned_ctx = OwnedContextImpl::from(ctx.clone());
|
||||||
let owned_ctx = owned_ctx.with_vararg(Box::new(List::new_from_item(row))).with_index(i);
|
let owned_ctx = owned_ctx.with_vararg(Box::new(row)).with_index(i);
|
||||||
let list = mapped.eval(owned_ctx.into_context()).await;
|
let list = mapped.eval(owned_ctx.into_context()).await;
|
||||||
|
|
||||||
rows.extend(list);
|
rows.extend(list);
|
||||||
@@ -143,33 +139,34 @@ async fn map<Item: AnyHash + Send + Sync + CacheHash>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
async fn mirror<T: 'n + Send + Clone>(
|
async fn mirror<T: BoundingBox + 'n + Send + Clone>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Graphic>,
|
Graphic,
|
||||||
List<Vector>,
|
Vector,
|
||||||
List<String>,
|
Raster<CPU>,
|
||||||
List<Raster<CPU>>,
|
Raster<GPU>,
|
||||||
List<Color>,
|
Color,
|
||||||
List<GradientStops>,
|
Gradient,
|
||||||
|
String,
|
||||||
)]
|
)]
|
||||||
content: List<T>,
|
content: Item<T>,
|
||||||
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
|
#[default(ReferencePoint::Center)] relative_to_bounds: Item<ReferencePoint>,
|
||||||
#[unit(" px")] offset: f64,
|
#[unit(" px")] offset: Item<f64>,
|
||||||
#[range]
|
#[range]
|
||||||
#[soft(-90..90)]
|
#[soft(-90..90)]
|
||||||
angle: Angle,
|
angle: Item<Angle>,
|
||||||
#[default(true)] keep_original: bool,
|
#[default(true)] keep_original: Item<bool>,
|
||||||
) -> List<T>
|
) -> List<T> {
|
||||||
where
|
let (relative_to_bounds, offset, angle, keep_original) = (relative_to_bounds.into_element(), offset.into_element(), angle.into_element(), keep_original.into_element());
|
||||||
List<T>: BoundingBox,
|
|
||||||
{
|
|
||||||
// Normalize the direction vector
|
// Normalize the direction vector
|
||||||
let normal = DVec2::from_angle(angle.to_radians());
|
let normal = DVec2::from_angle(angle.to_radians());
|
||||||
|
|
||||||
// The mirror reference may be based on the bounding box if an explicit reference point is chosen
|
// The mirror reference may be based on the bounding box if an explicit reference point is chosen
|
||||||
let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else {
|
let item_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
return content;
|
let RenderBoundingBox::Rectangle(bounding_box) = content.element().bounding_box(item_transform, false) else {
|
||||||
|
return List::new_from_item(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into());
|
let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into());
|
||||||
@@ -193,19 +190,14 @@ where
|
|||||||
|
|
||||||
let mut result_list = List::new();
|
let mut result_list = List::new();
|
||||||
|
|
||||||
// Add original items depending on the keep_original flag
|
|
||||||
if keep_original {
|
if keep_original {
|
||||||
for item in content.clone().into_iter() {
|
result_list.push(content.clone());
|
||||||
result_list.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and add mirrored items
|
// Add the mirrored copy with the reflection composed onto its transform
|
||||||
for mut row in content.into_iter() {
|
let mut mirrored = content;
|
||||||
let current_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
|
mirrored.set_attribute(ATTR_TRANSFORM, reflected_transform * item_transform);
|
||||||
row.set_attribute(ATTR_TRANSFORM, reflected_transform * current_transform);
|
result_list.push(mirrored);
|
||||||
result_list.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
result_list
|
result_list
|
||||||
}
|
}
|
||||||
@@ -217,95 +209,70 @@ where
|
|||||||
/// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether
|
/// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether
|
||||||
/// the layer is at the root document network or nested inside a custom subgraph.
|
/// the layer is at the root document network or nested inside a custom subgraph.
|
||||||
#[node_macro::node(name("Path of Subgraph"), category(""))]
|
#[node_macro::node(name("Path of Subgraph"), category(""))]
|
||||||
pub fn path_of_subgraph(_: impl Ctx, node_path: List<NodeId>) -> List<NodeId> {
|
pub fn path_of_subgraph(_: impl Ctx, node_path: Item<NodeIdPath>) -> Item<NodeIdPath> {
|
||||||
|
let node_path = node_path.into_element().0;
|
||||||
let len = node_path.len();
|
let len = node_path.len();
|
||||||
node_path.into_iter().take(len.saturating_sub(1)).collect()
|
Item::new_from_element(NodeIdPath(node_path.into_iter().take(len.saturating_sub(1)).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a named attribute on the input `List`, computing one value per item via the value-producing input. That input
|
/// Sets a named attribute on the input `List`, computing one value per item via the value-producing input. That input
|
||||||
/// is evaluated once per item, with the item's index and the item itself (as a `List` containing only that item,
|
/// is evaluated once per item, with the item's index and the item itself (as a `List` containing only that item,
|
||||||
/// passed as a vararg) provided via context, so the upstream pipeline can return a different value per item that may
|
/// passed as a vararg) provided via context, so the upstream pipeline can return a different value per item that may
|
||||||
/// be derived from the item's own data. If the attribute already exists, its values are replaced; if not, it's added.
|
/// be derived from the item's own data. If the attribute already exists, its values are replaced; if not, it's added.
|
||||||
/// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only
|
/// The value is type-erased into an `Item<AttributeValueDyn>` by the auto-inserted input adapter, so this node only
|
||||||
/// monomorphizes over `T` instead of the cartesian product `(T, U)`.
|
/// monomorphizes over `T` instead of the cartesian product `(T, U)`.
|
||||||
#[node_macro::node(category("Attributes: Write"))]
|
#[node_macro::node(category("Attributes: Write"))]
|
||||||
async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
|
async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
|
||||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||||
/// The `List` to set the named attribute on (one value per item).
|
/// The `List` to set the named attribute on (one value per item).
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Artboard>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<f64>,
|
|
||||||
List<bool>,
|
|
||||||
List<String>,
|
List<String>,
|
||||||
|
List<bool>,
|
||||||
|
List<f32>,
|
||||||
|
List<f64>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<DVec2>,
|
||||||
List<DAffine2>,
|
List<DAffine2>,
|
||||||
|
List<Vector>,
|
||||||
|
List<Graphic>,
|
||||||
|
List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>,
|
||||||
|
List<Color>,
|
||||||
|
List<Gradient>,
|
||||||
|
List<Artboard>,
|
||||||
List<BlendMode>,
|
List<BlendMode>,
|
||||||
List<GradientType>,
|
List<GradientType>,
|
||||||
List<GradientSpreadMethod>,
|
List<GradientSpreadMethod>,
|
||||||
)]
|
)]
|
||||||
mut content: List<T>,
|
content: List<T>,
|
||||||
/// The attribute name (key) to write or replace.
|
/// The attribute name (key) to write or replace.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
/// The node that produces the attribute value for each item. Called once per item with the item's index in context.
|
/// The node that produces the attribute value for each item. Called once per item with the item's index in context.
|
||||||
#[implementations(Context -> AttributeValueDyn)]
|
#[implementations(Context -> Item<AttributeValueDyn>)]
|
||||||
value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>,
|
value: impl Node<'n, Context<'static>, Output = Item<AttributeValueDyn>>,
|
||||||
) -> List<T> {
|
) -> List<T> {
|
||||||
|
let name = name.into_element();
|
||||||
|
|
||||||
|
let mut content = content;
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let row = content.clone_item(index).expect("index is within bounds");
|
let row = content.clone_item(index).expect("index is within bounds");
|
||||||
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(List::new_from_item(row))).with_index(index);
|
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(row)).with_index(index);
|
||||||
let v = value.eval(owned_ctx.into_context()).await;
|
let v = value.eval(owned_ctx.into_context()).await.into_element();
|
||||||
content.set_attribute_value_dyn(&name, index, v);
|
content.set_attribute_value_dyn(&name, index, v);
|
||||||
}
|
}
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items).
|
|
||||||
/// The source is type-erased into an `AttributeDyn` by an auto-inserted convert node, so this node only monomorphizes over `T` instead of the cartesian product `(T, U)`.
|
|
||||||
#[node_macro::node(category("Attributes: Write"))]
|
|
||||||
fn attach_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
|
|
||||||
_: impl Ctx,
|
|
||||||
/// The `List` to attach the new attribute to.
|
|
||||||
#[implementations(
|
|
||||||
List<Artboard>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Raster<CPU>>,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
List<f64>,
|
|
||||||
List<bool>,
|
|
||||||
List<String>,
|
|
||||||
List<DAffine2>,
|
|
||||||
List<BlendMode>,
|
|
||||||
List<GradientType>,
|
|
||||||
List<GradientSpreadMethod>,
|
|
||||||
)]
|
|
||||||
mut content: List<T>,
|
|
||||||
/// The source values to attach.
|
|
||||||
#[expose]
|
|
||||||
source: AttributeDyn,
|
|
||||||
/// The name to assign to the new destination attribute.
|
|
||||||
name: String,
|
|
||||||
) -> List<T> {
|
|
||||||
if source.is_empty() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
content.set_attribute_dyn(name, source);
|
|
||||||
content
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads a named `Vector` attribute from the input list, outputting each value as an element of a new `Vector[]`.
|
/// Reads a named `Vector` attribute from the input list, outputting each value as an element of a new `Vector[]`.
|
||||||
#[node_macro::node(category("Attributes: Read"))]
|
#[node_macro::node(category("Attributes: Read"))]
|
||||||
fn read_attribute_vector(
|
fn read_attribute_vector(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<Vector> {
|
) -> List<Vector> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<Vector>(&name, index) else { continue };
|
let Some(value) = content.attribute::<Vector>(&name, index) else { continue };
|
||||||
@@ -320,8 +287,9 @@ fn read_attribute_number(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<f64> {
|
) -> List<f64> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let value = content
|
let value = content
|
||||||
@@ -341,8 +309,9 @@ fn read_attribute_bool(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<bool> {
|
) -> List<bool> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<bool>(&name, index) else { continue };
|
let Some(value) = content.attribute::<bool>(&name, index) else { continue };
|
||||||
@@ -357,8 +326,9 @@ fn read_attribute_string(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<String> {
|
) -> List<String> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<String>(&name, index) else { continue };
|
let Some(value) = content.attribute::<String>(&name, index) else { continue };
|
||||||
@@ -373,8 +343,9 @@ fn read_attribute_transform(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<DAffine2> {
|
) -> List<DAffine2> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<DAffine2>(&name, index) else { continue };
|
let Some(value) = content.attribute::<DAffine2>(&name, index) else { continue };
|
||||||
@@ -389,8 +360,9 @@ fn read_attribute_color(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<Color> {
|
) -> List<Color> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<Color>(&name, index) else { continue };
|
let Some(value) = content.attribute::<Color>(&name, index) else { continue };
|
||||||
@@ -405,8 +377,9 @@ fn read_attribute_blend_mode(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<BlendMode> {
|
) -> List<BlendMode> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<BlendMode>(&name, index) else { continue };
|
let Some(value) = content.attribute::<BlendMode>(&name, index) else { continue };
|
||||||
@@ -421,8 +394,9 @@ fn read_attribute_gradient_type(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<GradientType> {
|
) -> List<GradientType> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<GradientType>(&name, index) else { continue };
|
let Some(value) = content.attribute::<GradientType>(&name, index) else { continue };
|
||||||
@@ -437,8 +411,9 @@ fn read_attribute_spread_method(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<GradientSpreadMethod> {
|
) -> List<GradientSpreadMethod> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<GradientSpreadMethod>(&name, index) else { continue };
|
let Some(value) = content.attribute::<GradientSpreadMethod>(&name, index) else { continue };
|
||||||
@@ -447,17 +422,18 @@ fn read_attribute_spread_method(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a named `GradientStops` attribute from the input list, outputting each value as an element of a new `GradientStops[]`.
|
/// Reads a named `Gradient` attribute from the input list, outputting each value as an element of a new `Gradient[]`.
|
||||||
#[node_macro::node(category("Attributes: Read"))]
|
#[node_macro::node(category("Attributes: Read"))]
|
||||||
fn read_attribute_gradient_stops(
|
fn read_attribute_gradient_stops(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<GradientStops> {
|
) -> List<Gradient> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<GradientStops>(&name, index) else { continue };
|
let Some(value) = content.attribute::<Gradient>(&name, index) else { continue };
|
||||||
result.push(Item::new_from_element(value.clone()));
|
result.push(Item::new_from_element(value.clone()));
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -469,8 +445,9 @@ fn read_attribute_artboard(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<Artboard> {
|
) -> List<Artboard> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<Artboard>(&name, index) else { continue };
|
let Some(value) = content.attribute::<Artboard>(&name, index) else { continue };
|
||||||
@@ -485,8 +462,9 @@ fn read_attribute_raster(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
content: ListDyn,
|
content: ListDyn,
|
||||||
/// The attribute name (key) to read.
|
/// The attribute name (key) to read.
|
||||||
name: String,
|
name: Item<String>,
|
||||||
) -> List<Raster<CPU>> {
|
) -> List<Raster<CPU>> {
|
||||||
|
let name = name.into_element();
|
||||||
let mut result = List::with_capacity(content.len());
|
let mut result = List::with_capacity(content.len());
|
||||||
for index in 0..content.len() {
|
for index in 0..content.len() {
|
||||||
let Some(value) = content.attribute::<Raster<CPU>>(&name, index) else { continue };
|
let Some(value) = content.attribute::<Raster<CPU>>(&name, index) else { continue };
|
||||||
@@ -500,11 +478,43 @@ fn read_attribute_raster(
|
|||||||
pub async fn extend<T: 'n + Send + Clone>(
|
pub async fn extend<T: 'n + Send + Clone>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The `List` whose items will appear at the start of the extended `List`.
|
/// The `List` whose items will appear at the start of the extended `List`.
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)]
|
#[implementations(
|
||||||
|
List<String>,
|
||||||
|
List<bool>,
|
||||||
|
List<f32>,
|
||||||
|
List<f64>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<DVec2>,
|
||||||
|
List<DAffine2>,
|
||||||
|
List<Vector>,
|
||||||
|
List<Graphic>,
|
||||||
|
List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>,
|
||||||
|
List<Color>,
|
||||||
|
List<Gradient>,
|
||||||
|
List<Artboard>,
|
||||||
|
)]
|
||||||
base: List<T>,
|
base: List<T>,
|
||||||
/// The `List` whose items will appear at the end of the extended `List`.
|
/// The `List` whose items will appear at the end of the extended `List`.
|
||||||
#[expose]
|
#[expose]
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)]
|
#[implementations(
|
||||||
|
List<String>,
|
||||||
|
List<bool>,
|
||||||
|
List<f32>,
|
||||||
|
List<f64>,
|
||||||
|
List<u32>,
|
||||||
|
List<u64>,
|
||||||
|
List<DVec2>,
|
||||||
|
List<DAffine2>,
|
||||||
|
List<Vector>,
|
||||||
|
List<Graphic>,
|
||||||
|
List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>,
|
||||||
|
List<Color>,
|
||||||
|
List<Gradient>,
|
||||||
|
List<Artboard>,
|
||||||
|
)]
|
||||||
new: List<T>,
|
new: List<T>,
|
||||||
) -> List<T> {
|
) -> List<T> {
|
||||||
let mut base = base;
|
let mut base = base;
|
||||||
@@ -519,22 +529,22 @@ pub async fn extend<T: 'n + Send + Clone>(
|
|||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
|
pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>,
|
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)] base: List<T>,
|
||||||
#[expose]
|
#[expose]
|
||||||
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)]
|
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)]
|
||||||
new: List<T>,
|
new: List<T>,
|
||||||
nested_node_path: List<NodeId>,
|
nested_node_path: Item<NodeIdPath>,
|
||||||
) -> List<T> {
|
) -> List<T> {
|
||||||
// Get the penultimate element of the node path, or None if the path is too short
|
// Drop this internal node's own trailing entry so the stamped path ends at the user-facing parent layer-style node (which encapsulates it)
|
||||||
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
|
let nested_node_path = nested_node_path.into_element().0;
|
||||||
let layer = {
|
let layer_path = {
|
||||||
let index = nested_node_path.len().wrapping_sub(2);
|
let len = nested_node_path.len();
|
||||||
nested_node_path.element(index).copied()
|
NodeIdPath(nested_node_path.into_iter().take(len.saturating_sub(1)).collect())
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut base = base;
|
let mut base = base;
|
||||||
for mut row in new.into_iter() {
|
for mut row in new.into_iter() {
|
||||||
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
|
||||||
base.push(row);
|
base.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,14 +562,14 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
|
|||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
DAffine2,
|
Item<DAffine2>,
|
||||||
DVec2,
|
Item<DVec2>,
|
||||||
)]
|
)]
|
||||||
content: T,
|
content: T,
|
||||||
) -> List<Graphic> {
|
) -> Item<Graphic> {
|
||||||
List::new_from_element(content.into())
|
Item::new_from_element(content.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
|
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
|
||||||
@@ -573,7 +583,7 @@ pub async fn to_graphic<T: IntoGraphicList>(
|
|||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
)]
|
)]
|
||||||
content: T,
|
content: T,
|
||||||
@@ -583,7 +593,9 @@ pub async fn to_graphic<T: IntoGraphicList>(
|
|||||||
|
|
||||||
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
|
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: bool) -> List<Graphic> {
|
pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: Item<bool>) -> List<Graphic> {
|
||||||
|
let fully_flatten = fully_flatten.into_element();
|
||||||
|
|
||||||
// TODO: Avoid mutable reference, instead return a new List<Graphic>?
|
// TODO: Avoid mutable reference, instead return a new List<Graphic>?
|
||||||
fn flatten_list(output_graphic_list: &mut List<Graphic>, current_graphic_list: List<Graphic>, fully_flatten: bool, recursion_depth: usize) {
|
fn flatten_list(output_graphic_list: &mut List<Graphic>, current_graphic_list: List<Graphic>, fully_flatten: bool, recursion_depth: usize) {
|
||||||
for index in 0..current_graphic_list.len() {
|
for index in 0..current_graphic_list.len() {
|
||||||
@@ -663,20 +675,20 @@ pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
|
|||||||
content.into_flattened_list()
|
content.into_flattened_list()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `GradientStops[]` 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"))]
|
||||||
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
|
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Gradient>)] content: T) -> List<Gradient> {
|
||||||
content.into_flattened_list()
|
content.into_flattened_list()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
|
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
|
||||||
#[node_macro::node(category("Color"))]
|
#[node_macro::node(category("Color"), name("Colors to Gradient"))]
|
||||||
fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> List<GradientStops> {
|
fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] colors: T) -> Item<Gradient> {
|
||||||
let colors = colors.into_flattened_list::<Color>();
|
let colors = colors.into_flattened_list::<Color>();
|
||||||
let total_colors = colors.len();
|
let total_colors = colors.len();
|
||||||
|
|
||||||
if total_colors == 0 {
|
if total_colors == 0 {
|
||||||
return List::new_from_element(GradientStops::new(vec![
|
return Item::new_from_element(Gradient::new(vec![
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -691,7 +703,7 @@ fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Gr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (1, Some(&single_color)) = (total_colors, colors.element(0)) {
|
if let (1, Some(&single_color)) = (total_colors, colors.element(0)) {
|
||||||
return List::new_from_element(GradientStops::new(vec![
|
return Item::new_from_element(Gradient::new(vec![
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
@@ -710,5 +722,5 @@ fn colors_to_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Gr
|
|||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: row.into_element(),
|
color: row.into_element(),
|
||||||
});
|
});
|
||||||
List::new_from_element(GradientStops::new(colors))
|
Item::new_from_element(Gradient::new(colors))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub mod subpath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod gradient {
|
pub mod gradient {
|
||||||
pub use vector_types::{GradientStop, GradientStops};
|
pub use vector_types::{Gradient, GradientStop};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod transform {
|
pub mod transform {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ use base64::Engine;
|
|||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use canvas_utils::{Canvas, CanvasHandle};
|
use canvas_utils::{Canvas, CanvasHandle};
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{Item, List};
|
use core_types::list::Item;
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
use core_types::list::List;
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use core_types::math::bbox::Bbox;
|
use core_types::math::bbox::Bbox;
|
||||||
#[cfg(target_family = "wasm")]
|
use core_types::ops::Convert;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
|
use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
|
||||||
@@ -23,12 +25,11 @@ use graphic_types::IntoGraphicList;
|
|||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use graphic_types::raster_types::Image;
|
use graphic_types::raster_types::Image;
|
||||||
use graphic_types::raster_types::{CPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use graphic_types::vector_types::gradient::GradientStops;
|
use graphic_types::vector_types::gradient::Gradient;
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender};
|
use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender};
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
fn parse_headers(headers: &str) -> reqwest::header::HeaderMap {
|
fn parse_headers(headers: &str) -> reqwest::header::HeaderMap {
|
||||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||||
@@ -51,11 +52,14 @@ async fn get_request(
|
|||||||
_primary: (),
|
_primary: (),
|
||||||
/// The web address to send the GET request to.
|
/// The web address to send the GET request to.
|
||||||
#[name("URL")]
|
#[name("URL")]
|
||||||
url: String,
|
url: Item<String>,
|
||||||
/// Makes the request run in the background without waiting on a response. This is useful for triggering webhooks without blocking the continued execution of the graph.
|
/// Makes the request run in the background without waiting on a response. This is useful for triggering webhooks without blocking the continued execution of the graph.
|
||||||
discard_result: bool,
|
discard_result: Item<bool>,
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_area")] headers: String,
|
#[widget(ParsedWidgetOverride::Custom = "text_area")] headers: Item<String>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let (url, headers) = (url.into_element(), headers.into_element());
|
||||||
|
let discard_result = *discard_result.element();
|
||||||
|
|
||||||
let header_map = parse_headers(&headers);
|
let header_map = parse_headers(&headers);
|
||||||
let request = reqwest::Client::new().get(url).headers(header_map);
|
let request = reqwest::Client::new().get(url).headers(header_map);
|
||||||
|
|
||||||
@@ -68,13 +72,13 @@ async fn get_request(
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = request.send().await;
|
let _ = request.send().await;
|
||||||
});
|
});
|
||||||
return String::new();
|
return Item::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(response) = request.send().await else {
|
let Ok(response) = request.send().await else {
|
||||||
return String::new();
|
return Item::default();
|
||||||
};
|
};
|
||||||
response.text().await.ok().unwrap_or_default()
|
Item::new_from_element(response.text().await.ok().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an HTTP POST request to a specified URL with the provided binary data and optionally waits for the response (unless discarded) which is output as a string.
|
/// Sends an HTTP POST request to a specified URL with the provided binary data and optionally waits for the response (unless discarded) which is output as a string.
|
||||||
@@ -84,16 +88,19 @@ async fn post_request(
|
|||||||
_primary: (),
|
_primary: (),
|
||||||
/// The web address to send the POST request to.
|
/// The web address to send the POST request to.
|
||||||
#[name("URL")]
|
#[name("URL")]
|
||||||
url: String,
|
url: Item<String>,
|
||||||
/// The binary data to include in the body of the POST request.
|
/// The binary data to include in the body of the POST request.
|
||||||
body: List<u8>,
|
body: Item<Resource>,
|
||||||
/// Makes the request run in the background without waiting on a response. This is useful for triggering webhooks without blocking the continued execution of the graph.
|
/// Makes the request run in the background without waiting on a response. This is useful for triggering webhooks without blocking the continued execution of the graph.
|
||||||
discard_result: bool,
|
discard_result: Item<bool>,
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_area")] headers: String,
|
#[widget(ParsedWidgetOverride::Custom = "text_area")] headers: Item<String>,
|
||||||
) -> String {
|
) -> Item<String> {
|
||||||
|
let (url, headers) = (url.into_element(), headers.into_element());
|
||||||
|
let discard_result = *discard_result.element();
|
||||||
|
|
||||||
let mut header_map = parse_headers(&headers);
|
let mut header_map = parse_headers(&headers);
|
||||||
header_map.insert("Content-Type", "application/octet-stream".parse().unwrap());
|
header_map.insert("Content-Type", "application/octet-stream".parse().unwrap());
|
||||||
let body_bytes: Vec<u8> = body.iter_element_values().copied().collect();
|
let body_bytes: Vec<u8> = body.element().as_ref().to_vec();
|
||||||
let request = reqwest::Client::new().post(url).body(body_bytes).headers(header_map);
|
let request = reqwest::Client::new().post(url).body(body_bytes).headers(header_map);
|
||||||
|
|
||||||
if discard_result {
|
if discard_result {
|
||||||
@@ -105,40 +112,42 @@ async fn post_request(
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = request.send().await;
|
let _ = request.send().await;
|
||||||
});
|
});
|
||||||
return String::new();
|
return Item::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(response) = request.send().await else {
|
let Ok(response) = request.send().await else {
|
||||||
return String::new();
|
return Item::default();
|
||||||
};
|
};
|
||||||
response.text().await.ok().unwrap_or_default()
|
Item::new_from_element(response.text().await.ok().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a text string to raw binary data. Useful for transmission over HTTP or writing to files.
|
/// Converts a text string to raw binary data. Useful for transmission over HTTP or writing to files.
|
||||||
#[node_macro::node(category("Web Request"), name("String to Bytes"))]
|
#[node_macro::node(category("Web Request"), name("String to Bytes"))]
|
||||||
fn string_to_bytes(_: impl Ctx, string: String) -> List<u8> {
|
fn string_to_bytes(_: impl Ctx, string: Item<String>) -> Item<Resource> {
|
||||||
string.into_bytes().into_iter().map(Item::new_from_element).collect()
|
Item::new_from_element(Resource::new(string.into_element().into_bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts extracted raw RGBA pixel data from an input image. Each pixel becomes 4 sequential bytes. Useful for transmission over HTTP or writing to files.
|
/// Converts extracted raw RGBA pixel data from an input image. Each pixel becomes 4 sequential bytes. Useful for transmission over HTTP or writing to files.
|
||||||
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
||||||
fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
|
fn image_to_bytes(_: impl Ctx, image: Item<Raster<CPU>>) -> Item<Resource> {
|
||||||
let Some(image) = image.element(0) else { return List::new() };
|
let bytes: Vec<u8> = image
|
||||||
image
|
.element()
|
||||||
.data
|
.data
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|color| {
|
.flat_map(|color| {
|
||||||
let SRGBA8 { red, green, blue, alpha } = (*color).into();
|
let SRGBA8 { red, green, blue, alpha } = (*color).into();
|
||||||
[red, green, blue, alpha]
|
[red, green, blue, alpha]
|
||||||
})
|
})
|
||||||
.map(Item::new_from_element)
|
.collect();
|
||||||
.collect()
|
|
||||||
|
Item::new_from_element(Resource::new(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
|
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
|
||||||
#[node_macro::node(category("Web Request"))]
|
#[node_macro::node(category("Web Request"))]
|
||||||
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> {
|
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: Item<String>) -> Item<Resource> {
|
||||||
let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
|
let url = url.into_element();
|
||||||
|
let placeholder = || -> Item<Resource> { Item::new_from_element(Resource::empty()) };
|
||||||
|
|
||||||
let response = match reqwest::Client::new().get(&url).send().await {
|
let response = match reqwest::Client::new().get(&url).send().await {
|
||||||
Ok(response) => response,
|
Ok(response) => response,
|
||||||
@@ -149,7 +158,7 @@ async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: St
|
|||||||
};
|
};
|
||||||
|
|
||||||
match response.bytes().await {
|
match response.bytes().await {
|
||||||
Ok(bytes) => Arc::from(bytes.to_vec()),
|
Ok(bytes) => Item::new_from_element(Resource::new(bytes)),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::error!("Failed to read HTTP response for `{url}`: {error}");
|
log::error!("Failed to read HTTP response for `{url}`: {error}");
|
||||||
placeholder()
|
placeholder()
|
||||||
@@ -161,9 +170,10 @@ async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: St
|
|||||||
///
|
///
|
||||||
/// Works with standard image format (PNG, JPEG, WebP, etc.). Automatically converts the color space to linear sRGB for accurate compositing.
|
/// Works with standard image format (PNG, JPEG, WebP, etc.). Automatically converts the color space to linear sRGB for accurate compositing.
|
||||||
#[node_macro::node(category("Web Request"))]
|
#[node_macro::node(category("Web Request"))]
|
||||||
fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
|
fn decode_image(_: impl Ctx, data: Item<Resource>) -> Item<Raster<CPU>> {
|
||||||
|
let data = data.into_element();
|
||||||
let Some(image) = image::load_from_memory(data.as_ref()).ok() else {
|
let Some(image) = image::load_from_memory(data.as_ref()).ok() else {
|
||||||
return List::new();
|
return Item::default();
|
||||||
};
|
};
|
||||||
let image = image.to_rgba32f();
|
let image = image.to_rgba32f();
|
||||||
let image = Image {
|
let image = Image {
|
||||||
@@ -180,13 +190,13 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
List::new_from_element(Raster::new_cpu(image))
|
Item::new_from_element(Raster::new_cpu(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
async fn create_canvas(_: impl Ctx) -> CanvasHandle {
|
async fn create_canvas(_: impl Ctx) -> Item<CanvasHandle> {
|
||||||
CanvasHandle::new()
|
Item::new_from_element(CanvasHandle::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a view of the input graphic within an area defined by the *Footprint*.
|
/// Renders a view of the input graphic within an area defined by the *Footprint*.
|
||||||
@@ -199,17 +209,21 @@ async fn rasterize<T: WasmNotSend + Clone + 'n>(
|
|||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Graphic>,
|
List<Graphic>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<GradientStops>,
|
List<Gradient>,
|
||||||
)]
|
)]
|
||||||
mut data: List<T>,
|
data: List<T>,
|
||||||
footprint: Footprint,
|
footprint: Item<Footprint>,
|
||||||
mut canvas: CanvasHandle,
|
canvas: Item<CanvasHandle>,
|
||||||
) -> List<Raster<CPU>>
|
) -> List<Raster<CPU>>
|
||||||
where
|
where
|
||||||
List<T>: Render + Clone + graphic_types::IntoGraphicList,
|
List<T>: Render + Clone + graphic_types::IntoGraphicList,
|
||||||
{
|
{
|
||||||
|
let mut data = data;
|
||||||
|
let mut canvas = canvas.into_element();
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
|
let footprint = footprint.into_element();
|
||||||
|
|
||||||
if footprint.transform.matrix2.determinant() == 0. {
|
if footprint.transform.matrix2.determinant() == 0. {
|
||||||
log::trace!("Invalid footprint received for rasterization");
|
log::trace!("Invalid footprint received for rasterization");
|
||||||
return List::new();
|
return List::new();
|
||||||
@@ -262,29 +276,48 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""), inject_scope)]
|
#[node_macro::node(category(""), inject_scope)]
|
||||||
pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi {
|
pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> {
|
||||||
editor_api
|
editor_api
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource {
|
pub async fn resource<'a: 'n>(
|
||||||
let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources");
|
_: impl Ctx,
|
||||||
application_io.load_resource(hash).await.unwrap_or_else(|| {
|
/// The scope-provided editor API giving access to the platform's resource storage.
|
||||||
panic!("Resource {hash} not found");
|
#[scope(editor_api::IDENTIFIER)]
|
||||||
})
|
editor_api: Item<&'a PlatformEditorApi>,
|
||||||
|
/// The content hash identifying which stored resource to load.
|
||||||
|
hash: Item<ResourceHash>,
|
||||||
|
) -> Item<Resource> {
|
||||||
|
let hash = hash.into_element();
|
||||||
|
let application_io = editor_api.into_element().application_io.as_ref().expect("ApplicationIo must be available when using resources");
|
||||||
|
let resource = application_io.load_resource(hash).await.unwrap_or_else(|| panic!("Resource {hash} not found"));
|
||||||
|
Item::new_from_element(resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""), inject_scope)]
|
#[node_macro::node(category(""), inject_scope)]
|
||||||
pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor {
|
pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> {
|
||||||
editor_api
|
let executor = editor_api
|
||||||
|
.into_element()
|
||||||
.application_io
|
.application_io
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("ApplicationIo not not available")
|
.expect("ApplicationIo not available")
|
||||||
.gpu_executor()
|
.gpu_executor()
|
||||||
.expect("GPU executor not available")
|
.expect("GPU executor not available");
|
||||||
|
Item::new_from_element(executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""), inject_scope)]
|
#[node_macro::node(category(""), inject_scope)]
|
||||||
pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> {
|
pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<Option<&'a ::wgpu_executor::WgpuExecutor>> {
|
||||||
editor_api.application_io.as_ref()?.gpu_executor()
|
let executor = editor_api.into_element().application_io.as_ref().and_then(|application_io| application_io.gpu_executor());
|
||||||
|
Item::new_from_element(executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it.
|
||||||
|
#[node_macro::node(category("Debug"), memoize)]
|
||||||
|
pub async fn upload_texture<'a: 'n>(_: impl Ctx, content: Item<Raster<CPU>>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item<Raster<GPU>> {
|
||||||
|
let executor = executor.into_element();
|
||||||
|
let (raster, attributes) = content.into_parts();
|
||||||
|
|
||||||
|
Item::from_parts(raster.convert(Footprint::DEFAULT, executor).await, attributes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use core_types::ExtractVarArgs;
|
use core_types::ExtractVarArgs;
|
||||||
use core_types::color::Linear;
|
use core_types::color::Linear;
|
||||||
|
use core_types::list::Item;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::generate_uuid;
|
use core_types::uuid::generate_uuid;
|
||||||
use core_types::{Ctx, ExtractFootprint};
|
use core_types::{Ctx, ExtractFootprint};
|
||||||
@@ -14,9 +15,9 @@ use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
|
|||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
async fn render_background<'a: 'n>(
|
async fn render_background<'a: 'n>(
|
||||||
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
|
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
|
||||||
#[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
|
#[scope(composite_background_pipeline::IDENTIFIER)] pipeline: Item<WgpuPipelineCache>,
|
||||||
data: RenderOutput,
|
data: Item<RenderOutput>,
|
||||||
) -> RenderOutput {
|
) -> Item<RenderOutput> {
|
||||||
let footprint = ctx.footprint();
|
let footprint = ctx.footprint();
|
||||||
let render_params = ctx
|
let render_params = ctx
|
||||||
.vararg(0)
|
.vararg(0)
|
||||||
@@ -28,7 +29,7 @@ async fn render_background<'a: 'n>(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let RenderOutput { data: foreground_data, metadata } = data;
|
let RenderOutput { data: foreground_data, metadata } = data.into_element();
|
||||||
let mut render_params = render_params.clone();
|
let mut render_params = render_params.clone();
|
||||||
render_params.footprint = *footprint;
|
render_params.footprint = *footprint;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ async fn render_background<'a: 'n>(
|
|||||||
RenderOutputType::Texture(foreground_texture) => {
|
RenderOutputType::Texture(foreground_texture) => {
|
||||||
let doc_to_screen = render_params.footprint.transform.as_affine2();
|
let doc_to_screen = render_params.footprint.transform.as_affine2();
|
||||||
let blended = pipeline
|
let blended = pipeline
|
||||||
|
.into_element()
|
||||||
.run::<CompositeBackground>(&CompositeBackgroundArgs {
|
.run::<CompositeBackground>(&CompositeBackgroundArgs {
|
||||||
foreground: foreground_texture.as_ref(),
|
foreground: foreground_texture.as_ref(),
|
||||||
backgrounds: &metadata.backgrounds,
|
backgrounds: &metadata.backgrounds,
|
||||||
@@ -117,19 +119,19 @@ async fn render_background<'a: 'n>(
|
|||||||
_ => unreachable!("Render background node received unsupported render output type"),
|
_ => unreachable!("Render background node received unsupported render output type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
RenderOutput { data, metadata }
|
Item::new_from_element(RenderOutput { data, metadata })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""), inject_scope)]
|
#[node_macro::node(category(""), inject_scope)]
|
||||||
async fn composite_background_pipeline<'a: 'n>(
|
async fn composite_background_pipeline<'a: 'n>(
|
||||||
_ctx: impl Ctx,
|
_ctx: impl Ctx,
|
||||||
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
|
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item<Option<&'a WgpuExecutor>>,
|
||||||
#[data] pipeline: WgpuPipelineCache,
|
#[data] pipeline: WgpuPipelineCache,
|
||||||
) -> WgpuPipelineCache {
|
) -> Item<WgpuPipelineCache> {
|
||||||
if let Some(executor) = executor {
|
if let Some(executor) = executor.into_element() {
|
||||||
executor.pipeline_init::<CompositeBackground>(pipeline);
|
executor.pipeline_init::<CompositeBackground>(pipeline);
|
||||||
}
|
}
|
||||||
pipeline.clone()
|
Item::new_from_element(pipeline.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompositeBackground {
|
pub struct CompositeBackground {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Tile-based render caching for efficient viewport panning.
|
//! Tile-based render caching for efficient viewport panning.
|
||||||
|
|
||||||
|
use core_types::list::Item;
|
||||||
use core_types::math::bbox::AxisAlignedBbox;
|
use core_types::math::bbox::AxisAlignedBbox;
|
||||||
use core_types::transform::{Footprint, RenderQuality, Transform};
|
use core_types::transform::{Footprint, RenderQuality, Transform};
|
||||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
|
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
|
||||||
@@ -323,11 +324,11 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
|
|||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub async fn render_output_cache<'a: 'n>(
|
pub async fn render_output_cache<'a: 'n>(
|
||||||
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
|
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
|
||||||
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
|
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item<Option<&'a WgpuExecutor>>,
|
||||||
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi,
|
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>,
|
||||||
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
|
data: impl Node<Context<'static>, Output = Item<RenderOutput>> + Send + Sync,
|
||||||
#[data] tile_cache: TileCache,
|
#[data] tile_cache: TileCache,
|
||||||
) -> RenderOutput {
|
) -> Item<RenderOutput> {
|
||||||
let footprint = ctx.footprint();
|
let footprint = ctx.footprint();
|
||||||
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()) else {
|
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()) else {
|
||||||
log::warn!("render_output_cache: missing or invalid render params, falling back to direct render");
|
log::warn!("render_output_cache: missing or invalid render params, falling back to direct render");
|
||||||
@@ -351,7 +352,7 @@ pub async fn render_output_cache<'a: 'n>(
|
|||||||
end: footprint.resolution.as_dvec2() - device_origin_offset,
|
end: footprint.resolution.as_dvec2() - device_origin_offset,
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_region_area = editor_api.editor_preferences.max_render_region_area();
|
let max_region_area = editor_api.into_element().editor_preferences.max_render_region_area();
|
||||||
|
|
||||||
let cache_key = CacheKey::new(
|
let cache_key = CacheKey::new(
|
||||||
max_region_area,
|
max_region_area,
|
||||||
@@ -389,15 +390,15 @@ pub async fn render_output_cache<'a: 'n>(
|
|||||||
return data.eval(context.into_context()).await;
|
return data.eval(context.into_context()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let executor = executor.expect("GPU executor not available");
|
let executor = executor.into_element().expect("GPU executor not available");
|
||||||
let output_texture = executor.request_texture(physical_resolution).await;
|
let output_texture = executor.request_texture(physical_resolution).await;
|
||||||
|
|
||||||
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor);
|
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor);
|
||||||
|
|
||||||
RenderOutput {
|
Item::new_from_element(RenderOutput {
|
||||||
data: RenderOutputType::Texture(output_texture),
|
data: RenderOutputType::Texture(output_texture),
|
||||||
metadata: combined_metadata,
|
metadata: combined_metadata,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_missing_region<F, Fut>(
|
async fn render_missing_region<F, Fut>(
|
||||||
@@ -410,7 +411,7 @@ async fn render_missing_region<F, Fut>(
|
|||||||
) -> CachedRegion
|
) -> CachedRegion
|
||||||
where
|
where
|
||||||
F: Fn(Context<'static>) -> Fut,
|
F: Fn(Context<'static>) -> Fut,
|
||||||
Fut: std::future::Future<Output = RenderOutput>,
|
Fut: std::future::Future<Output = Item<RenderOutput>>,
|
||||||
{
|
{
|
||||||
let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
|
let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
|
||||||
let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
|
let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
|
||||||
@@ -428,7 +429,7 @@ where
|
|||||||
|
|
||||||
let region_params = render_params.clone();
|
let region_params = render_params.clone();
|
||||||
let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context();
|
let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context();
|
||||||
let mut result = render_fn(region_ctx).await;
|
let mut result = render_fn(region_ctx).await.into_element();
|
||||||
|
|
||||||
let RenderOutputType::Texture(texture) = result.data else {
|
let RenderOutputType::Texture(texture) = result.data else {
|
||||||
unreachable!("render_missing_region: expected texture output from Vello render");
|
unreachable!("render_missing_region: expected texture output from Vello render");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use core_types::list::List;
|
use core_types::list::{Item, List};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
|
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
|
||||||
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
|
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
|
||||||
@@ -8,7 +8,7 @@ use graphic_types::raster_types::{CPU, Raster};
|
|||||||
use graphic_types::{Artboard, Graphic, Vector};
|
use graphic_types::{Artboard, Graphic, Vector};
|
||||||
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
|
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
use wgpu_executor::{RenderContext, WgpuExecutor};
|
use wgpu_executor::{RenderContext, WgpuExecutor};
|
||||||
|
|
||||||
#[derive(Clone, dyn_any::DynAny)]
|
#[derive(Clone, dyn_any::DynAny)]
|
||||||
@@ -31,11 +31,11 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||||||
Context -> List<Vector>,
|
Context -> List<Vector>,
|
||||||
Context -> List<Raster<CPU>>,
|
Context -> List<Raster<CPU>>,
|
||||||
Context -> List<Color>,
|
Context -> List<Color>,
|
||||||
Context -> List<GradientStops>,
|
Context -> List<Gradient>,
|
||||||
Context -> List<String>,
|
Context -> List<String>,
|
||||||
)]
|
)]
|
||||||
data: impl Node<Context<'static>, Output = T>,
|
data: impl Node<Context<'static>, Output = T>,
|
||||||
) -> RenderIntermediate {
|
) -> Item<RenderIntermediate> {
|
||||||
let render_params = ctx
|
let render_params = ctx
|
||||||
.vararg(0)
|
.vararg(0)
|
||||||
.expect("Did not find var args")
|
.expect("Did not find var args")
|
||||||
@@ -48,7 +48,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||||||
let footprint = Footprint::default();
|
let footprint = Footprint::default();
|
||||||
let mut metadata = RenderMetadata::default();
|
let mut metadata = RenderMetadata::default();
|
||||||
data.collect_metadata(&mut metadata, footprint, None);
|
data.collect_metadata(&mut metadata, footprint, None);
|
||||||
match &render_params.render_output_type {
|
let intermediate = match &render_params.render_output_type {
|
||||||
RenderOutputTypeRequest::Vello => {
|
RenderOutputTypeRequest::Vello => {
|
||||||
let mut scene = vello::Scene::new();
|
let mut scene = vello::Scene::new();
|
||||||
|
|
||||||
@@ -70,15 +70,17 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||||||
metadata,
|
metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Item::new_from_element(intermediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
async fn render<'a: 'n>(
|
async fn render<'a: 'n>(
|
||||||
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
|
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
|
||||||
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
|
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item<Option<&'a WgpuExecutor>>,
|
||||||
data: RenderIntermediate,
|
data: Item<RenderIntermediate>,
|
||||||
) -> RenderOutput {
|
) -> Item<RenderOutput> {
|
||||||
let footprint = ctx.footprint();
|
let footprint = ctx.footprint();
|
||||||
let render_params = ctx
|
let render_params = ctx
|
||||||
.vararg(0)
|
.vararg(0)
|
||||||
@@ -88,7 +90,7 @@ async fn render<'a: 'n>(
|
|||||||
let mut render_params = render_params.clone();
|
let mut render_params = render_params.clone();
|
||||||
render_params.footprint = *footprint;
|
render_params.footprint = *footprint;
|
||||||
|
|
||||||
let RenderIntermediate { ty, mut metadata } = data;
|
let RenderIntermediate { ty, mut metadata } = data.into_element();
|
||||||
metadata.apply_transform(footprint.transform);
|
metadata.apply_transform(footprint.transform);
|
||||||
|
|
||||||
let data = match (render_params.render_output_type, ty) {
|
let data = match (render_params.render_output_type, ty) {
|
||||||
@@ -131,6 +133,7 @@ async fn render<'a: 'n>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let texture = executor
|
let texture = executor
|
||||||
|
.into_element()
|
||||||
.expect("GPU executor not available")
|
.expect("GPU executor not available")
|
||||||
.render_vello_scene(&transformed_scene, footprint.resolution, context, None)
|
.render_vello_scene(&transformed_scene, footprint.resolution, context, None)
|
||||||
.await
|
.await
|
||||||
@@ -140,15 +143,20 @@ async fn render<'a: 'n>(
|
|||||||
_ => unreachable!("Render node did not receive its requested data type"),
|
_ => unreachable!("Render node did not receive its requested data type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
RenderOutput { data, metadata }
|
Item::new_from_element(RenderOutput { data, metadata })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
async fn create_context<'a: 'n>(
|
async fn create_context<'a: 'n>(
|
||||||
// Context injections are defined in the wrap_network_in_scope function
|
// The executor boundary supplies the render config as the sole vararg (see `wrap_network_in_scope()`)
|
||||||
render_config: RenderConfig,
|
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
|
||||||
data: impl Node<Context<'static>, Output = RenderOutput>,
|
data: impl Node<Context<'static>, Output = Item<RenderOutput>>,
|
||||||
) -> RenderOutput {
|
) -> Item<RenderOutput> {
|
||||||
|
let render_config = ctx.vararg(0).ok().and_then(|config| config.downcast_ref::<RenderConfig>()).copied().unwrap_or_else(|| {
|
||||||
|
log::error!("The boundary context is missing its render config vararg");
|
||||||
|
RenderConfig::default()
|
||||||
|
});
|
||||||
|
|
||||||
let render_output_type = match render_config.export_format {
|
let render_output_type = match render_config.export_format {
|
||||||
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
|
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
|
||||||
ExportFormat::Raster => RenderOutputTypeRequest::Vello,
|
ExportFormat::Raster => RenderOutputTypeRequest::Vello,
|
||||||
@@ -179,6 +187,6 @@ async fn create_context<'a: 'n>(
|
|||||||
|
|
||||||
let mut result = data.eval(ctx).await;
|
let mut result = data.eval(ctx).await;
|
||||||
|
|
||||||
result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale)));
|
result.element_mut().metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale)));
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use core_types::list::Item;
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
|
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
|
||||||
use glam::{DAffine2, DVec2, UVec2, Vec2};
|
use glam::{DAffine2, DVec2, UVec2, Vec2};
|
||||||
@@ -10,9 +11,9 @@ use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
|
|||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub async fn render_pixel_preview<'a: 'n>(
|
pub async fn render_pixel_preview<'a: 'n>(
|
||||||
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
|
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
|
||||||
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
|
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: Item<WgpuPipelineCache>,
|
||||||
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
|
data: impl Node<Context<'static>, Output = Item<RenderOutput>> + Send + Sync,
|
||||||
) -> RenderOutput {
|
) -> Item<RenderOutput> {
|
||||||
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else {
|
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else {
|
||||||
log::error!("invalid render params for pixel preview");
|
log::error!("invalid render params for pixel preview");
|
||||||
let context = OwnedContextImpl::from(ctx).into_context();
|
let context = OwnedContextImpl::from(ctx).into_context();
|
||||||
@@ -52,14 +53,17 @@ pub async fn render_pixel_preview<'a: 'n>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context();
|
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context();
|
||||||
let mut result = data.eval(new_ctx).await;
|
let mut result = data.eval(new_ctx).await.into_element();
|
||||||
|
|
||||||
let RenderOutputType::Texture(ref source_texture) = result.data else { return result };
|
let RenderOutputType::Texture(ref source_texture) = result.data else {
|
||||||
|
return Item::new_from_element(result);
|
||||||
|
};
|
||||||
|
|
||||||
let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform;
|
let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform;
|
||||||
let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution);
|
let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution);
|
||||||
|
|
||||||
let resampled = pipeline
|
let resampled = pipeline
|
||||||
|
.into_element()
|
||||||
.run::<PixelPreview>(&PixelPreviewArgs {
|
.run::<PixelPreview>(&PixelPreviewArgs {
|
||||||
source: source_texture.as_ref(),
|
source: source_texture.as_ref(),
|
||||||
transform: &transform,
|
transform: &transform,
|
||||||
@@ -71,19 +75,19 @@ pub async fn render_pixel_preview<'a: 'n>(
|
|||||||
|
|
||||||
result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min));
|
result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min));
|
||||||
|
|
||||||
result
|
Item::new_from_element(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""), inject_scope)]
|
#[node_macro::node(category(""), inject_scope)]
|
||||||
async fn pixel_preview_pipeline<'a: 'n>(
|
async fn pixel_preview_pipeline<'a: 'n>(
|
||||||
_ctx: impl Ctx,
|
_ctx: impl Ctx,
|
||||||
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
|
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item<Option<&'a WgpuExecutor>>,
|
||||||
#[data] pipeline: WgpuPipelineCache,
|
#[data] pipeline: WgpuPipelineCache,
|
||||||
) -> WgpuPipelineCache {
|
) -> Item<WgpuPipelineCache> {
|
||||||
if let Some(executor) = executor {
|
if let Some(executor) = executor.into_element() {
|
||||||
executor.pipeline_init::<PixelPreview>(pipeline);
|
executor.pipeline_init::<PixelPreview>(pipeline);
|
||||||
}
|
}
|
||||||
pipeline.clone()
|
Item::new_from_element(pipeline.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PixelPreview {
|
pub struct PixelPreview {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
|
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
|
||||||
use core_types::list::List;
|
use core_types::list::{Item, List};
|
||||||
use core_types::{ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_TEXT_ALIGN, Ctx};
|
use core_types::{ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_TEXT_ALIGN, Ctx};
|
||||||
use graph_craft::application_io::resource::Resource;
|
use graph_craft::application_io::resource::Resource;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
pub use text_nodes::*;
|
pub use text_nodes::*;
|
||||||
|
|
||||||
/// Produces a styled `String[]` carrying all typographic attributes.
|
/// Produces a styled text 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"))]
|
||||||
@@ -15,15 +15,15 @@ fn text(
|
|||||||
/// The text content to be drawn.
|
/// The text content to be drawn.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
||||||
#[default("Lorem ipsum")]
|
#[default("Lorem ipsum")]
|
||||||
text: String,
|
text: Item<String>,
|
||||||
/// The loaded font file used to draw the text. The editor resolves the chosen typeface to these bytes via the resource system.
|
/// The loaded font file used to draw the text. The editor resolves the chosen typeface to these bytes via the resource system.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_font")]
|
#[widget(ParsedWidgetOverride::Custom = "text_font")]
|
||||||
font: Resource,
|
font: Item<Resource>,
|
||||||
/// The font size used to draw the text.
|
/// The font size used to draw the text.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[default(24.)]
|
#[default(24.)]
|
||||||
#[hard(1..)]
|
#[hard(1..)]
|
||||||
size: f64,
|
size: Item<f64>,
|
||||||
/// The line height ratio, relative to the font size. Each line is drawn lower than its previous line by the distance of *Size* × *Line Height*.
|
/// The line height ratio, relative to the font size. Each line is drawn lower than its previous line by the distance of *Size* × *Line Height*.
|
||||||
///
|
///
|
||||||
/// 0 means all lines overlap. 1 means all lines are spaced by just the font size. 1.2 is a common default for readable text. 2 means double-spaced text.
|
/// 0 means all lines overlap. 1 means all lines are spaced by just the font size. 1.2 is a common default for readable text. 2 means double-spaced text.
|
||||||
@@ -31,74 +31,87 @@ fn text(
|
|||||||
#[hard(0..)]
|
#[hard(0..)]
|
||||||
#[step(0.1)]
|
#[step(0.1)]
|
||||||
#[default(1.2)]
|
#[default(1.2)]
|
||||||
line_height: f64,
|
line_height: Item<f64>,
|
||||||
/// Additional spacing, in pixels, added between each character.
|
/// Additional spacing, in pixels, added between each character.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[step(0.1)]
|
#[step(0.1)]
|
||||||
letter_spacing: f64,
|
letter_spacing: Item<f64>,
|
||||||
/// The angle of faux italic slant applied to each glyph.
|
/// The angle of faux italic slant applied to each glyph.
|
||||||
#[unit("°")]
|
#[unit("°")]
|
||||||
#[hard(-85..85)]
|
#[hard(-85..85)]
|
||||||
letter_tilt: f64,
|
letter_tilt: Item<f64>,
|
||||||
/// Enables the maximum width constraint so lines can wrap.
|
/// Enables the maximum width constraint so lines can wrap.
|
||||||
#[widget(ParsedWidgetOverride::Hidden)]
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
has_max_width: bool,
|
has_max_width: Item<bool>,
|
||||||
/// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap.
|
/// The maximum width that the text block can occupy before wrapping to a new line. Otherwise, lines do not wrap.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[hard(1..)]
|
#[hard(1..)]
|
||||||
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
||||||
max_width: f64,
|
max_width: Item<f64>,
|
||||||
/// Whether the *Max Height* property is enabled so that lines beyond it are not drawn.
|
/// Whether the *Max Height* property is enabled so that lines beyond it are not drawn.
|
||||||
#[widget(ParsedWidgetOverride::Hidden)]
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
has_max_height: bool,
|
has_max_height: Item<bool>,
|
||||||
/// The maximum height that the text block can occupy. Excess lines are not drawn.
|
/// The maximum height that the text block can occupy. Excess lines are not drawn.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[hard(1..)]
|
#[hard(1..)]
|
||||||
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
#[widget(ParsedWidgetOverride::Custom = "optional_f64")]
|
||||||
max_height: f64,
|
max_height: Item<f64>,
|
||||||
/// 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: Item<TextAlign>,
|
||||||
) -> List<String> {
|
) -> Item<String> {
|
||||||
let mut list = List::new_from_element(text);
|
let text = text.into_element();
|
||||||
|
let font = font.into_element();
|
||||||
|
let (size, line_height, letter_spacing, letter_tilt) = (*size.element(), *line_height.element(), *letter_spacing.element(), *letter_tilt.element());
|
||||||
|
let (has_max_width, max_width, has_max_height, max_height) = (*has_max_width.element(), *max_width.element(), *has_max_height.element(), *max_height.element());
|
||||||
|
let align = align.into_element();
|
||||||
|
|
||||||
|
let mut item = Item::new_from_element(text);
|
||||||
|
|
||||||
if font != Resource::default() {
|
if font != Resource::default() {
|
||||||
list.set_attribute(ATTR_FONT, 0, font);
|
item.set_attribute(ATTR_FONT, font);
|
||||||
}
|
}
|
||||||
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON {
|
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON {
|
||||||
list.set_attribute(ATTR_FONT_SIZE, 0, size);
|
item.set_attribute(ATTR_FONT_SIZE, size);
|
||||||
}
|
}
|
||||||
if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON {
|
if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON {
|
||||||
list.set_attribute(ATTR_LINE_HEIGHT, 0, line_height);
|
item.set_attribute(ATTR_LINE_HEIGHT, line_height);
|
||||||
}
|
}
|
||||||
if letter_spacing != 0. {
|
if letter_spacing != 0. {
|
||||||
list.set_attribute(ATTR_LETTER_SPACING, 0, letter_spacing);
|
item.set_attribute(ATTR_LETTER_SPACING, letter_spacing);
|
||||||
}
|
}
|
||||||
if letter_tilt != 0. {
|
if letter_tilt != 0. {
|
||||||
list.set_attribute(ATTR_LETTER_TILT, 0, letter_tilt);
|
item.set_attribute(ATTR_LETTER_TILT, letter_tilt);
|
||||||
}
|
}
|
||||||
if has_max_width {
|
if has_max_width {
|
||||||
list.set_attribute(ATTR_MAX_WIDTH, 0, Some(max_width));
|
item.set_attribute(ATTR_MAX_WIDTH, Some(max_width));
|
||||||
}
|
}
|
||||||
if has_max_height {
|
if has_max_height {
|
||||||
list.set_attribute(ATTR_MAX_HEIGHT, 0, Some(max_height));
|
item.set_attribute(ATTR_MAX_HEIGHT, Some(max_height));
|
||||||
}
|
}
|
||||||
if align != TextAlign::default() {
|
if align != TextAlign::default() {
|
||||||
list.set_attribute(ATTR_TEXT_ALIGN, 0, align);
|
item.set_attribute(ATTR_TEXT_ALIGN, align);
|
||||||
}
|
}
|
||||||
|
|
||||||
list
|
item
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a styled `String[]` into vector geometry.
|
/// Converts a styled text string into a vector compound path.
|
||||||
#[node_macro::node(category("Text"), name("Text to Vector"))]
|
#[node_macro::node(category("Text"), name("Text to Vector"))]
|
||||||
fn text_to_vector(
|
fn text_to_vector(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// A styled list of text strings produced by the **Text** node (or any other `String[]` source).
|
/// A styled text string produced by the **Text** node (or any other string source).
|
||||||
#[implementations(List<String>)]
|
string: Item<String>,
|
||||||
strings: List<String>,
|
) -> Item<Vector> {
|
||||||
/// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced.
|
shape_text_item(&string, false).into_iter().next().unwrap_or_default()
|
||||||
separate_glyphs: bool,
|
}
|
||||||
|
|
||||||
|
/// Splits a styled text string into a separate vector item for each of its glyphs (letterforms).
|
||||||
|
#[node_macro::node(category("Text"), name("Text to Vector Glyphs"))]
|
||||||
|
fn text_to_vector_glyphs(
|
||||||
|
_: impl Ctx,
|
||||||
|
/// A styled text string produced by the **Text** node (or any other string source).
|
||||||
|
string: Item<String>,
|
||||||
) -> List<Vector> {
|
) -> List<Vector> {
|
||||||
shape_text_list(&strings, separate_glyphs)
|
shape_text_item(&string, true)
|
||||||
}
|
}
|
||||||
|
|||||||
+457
-304
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
|||||||
use core_types::list::{ATTR_FILL, Item, List};
|
use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List};
|
||||||
use core_types::uuid::NodeId;
|
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, BlendMode, Color,
|
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Ctx,
|
||||||
Ctx,
|
|
||||||
};
|
};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
||||||
@@ -34,8 +32,9 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
|||||||
/// Subtraction cuts overlapping areas out from the last (Subtract Front) or first (Subtract Back) path.
|
/// Subtraction cuts overlapping areas out from the last (Subtract Front) or first (Subtract Back) path.
|
||||||
/// Intersection cuts away all but the overlapping areas shared by every path.
|
/// Intersection cuts away all but the overlapping areas shared by every path.
|
||||||
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
|
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
|
||||||
operation: BooleanOperation,
|
operation: Item<BooleanOperation>,
|
||||||
) -> List<Vector> {
|
) -> Item<Vector> {
|
||||||
|
let operation = operation.into_element();
|
||||||
let content = content.into_graphic_list();
|
let content = content.into_graphic_list();
|
||||||
|
|
||||||
// The first index is the bottom of the stack
|
// The first index is the bottom of the stack
|
||||||
@@ -60,7 +59,7 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
|||||||
result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001);
|
result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
result_vector_list
|
result_vector_list.into_iter().next().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
@@ -183,6 +182,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
.flat_map(|index| {
|
.flat_map(|index| {
|
||||||
let graphic = graphic_list.element(index).unwrap();
|
let graphic = graphic_list.element(index).unwrap();
|
||||||
match graphic.clone() {
|
match graphic.clone() {
|
||||||
|
Graphic::None => Vec::new(),
|
||||||
Graphic::Vector(vector) => {
|
Graphic::Vector(vector) => {
|
||||||
// Apply the parent graphic's transform to each element of the `List<Vector>`
|
// Apply the parent graphic's transform to each element of the `List<Vector>`
|
||||||
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
@@ -197,18 +197,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
}
|
}
|
||||||
Graphic::RasterCPU(image) => {
|
Graphic::RasterCPU(image) => {
|
||||||
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| {
|
let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
|
||||||
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
subpath.apply_transform(transform);
|
subpath.apply_transform(transform);
|
||||||
|
|
||||||
let element = Vector::from_subpath(subpath);
|
let element = Vector::from_subpath(subpath);
|
||||||
|
|
||||||
let mut item = Item::new_from_element(element)
|
let mut item = Item::new_from_element(element);
|
||||||
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] {
|
||||||
.with_attribute(ATTR_OPACITY, opacity)
|
item.attributes_mut().insert_cloned_from(source_attributes, key);
|
||||||
.with_attribute(ATTR_OPACITY_FILL, fill)
|
}
|
||||||
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
|
||||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
|
||||||
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
item
|
item
|
||||||
};
|
};
|
||||||
@@ -219,29 +217,23 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
(0..image.len())
|
(0..image.len())
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
|
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
|
||||||
let layer: List<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i);
|
let source_attributes = image.clone_item_attributes(i);
|
||||||
let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
|
make_item(parent_transform * row_transform, &source_attributes)
|
||||||
let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
|
||||||
let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
|
||||||
let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i);
|
|
||||||
make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
Graphic::RasterGPU(image) => {
|
Graphic::RasterGPU(image) => {
|
||||||
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| {
|
let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| {
|
||||||
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
subpath.apply_transform(transform);
|
subpath.apply_transform(transform);
|
||||||
|
|
||||||
let element = Vector::from_subpath(subpath);
|
let element = Vector::from_subpath(subpath);
|
||||||
|
|
||||||
let mut item = Item::new_from_element(element)
|
let mut item = Item::new_from_element(element);
|
||||||
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] {
|
||||||
.with_attribute(ATTR_OPACITY, opacity)
|
item.attributes_mut().insert_cloned_from(source_attributes, key);
|
||||||
.with_attribute(ATTR_OPACITY_FILL, fill)
|
}
|
||||||
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
|
||||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
|
||||||
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
item
|
item
|
||||||
};
|
};
|
||||||
@@ -252,12 +244,8 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
(0..image.len())
|
(0..image.len())
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
|
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
|
||||||
let layer: List<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i);
|
let source_attributes = image.clone_item_attributes(i);
|
||||||
let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
|
make_item(parent_transform * row_transform, &source_attributes)
|
||||||
let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
|
||||||
let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
|
||||||
let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i);
|
|
||||||
make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,34 +12,17 @@ impl Adjust<Color> for Color {
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
mod adjust_std {
|
mod adjust_std {
|
||||||
use super::*;
|
use super::*;
|
||||||
use core_types::list::List;
|
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
use vector_types::GradientStops;
|
use vector_types::Gradient;
|
||||||
|
|
||||||
impl Adjust<Color> for List<Raster<CPU>> {
|
impl Adjust<Color> for Raster<CPU> {
|
||||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||||
for element in self.iter_element_values_mut() {
|
for color in self.data_mut().data.iter_mut() {
|
||||||
for color in element.data_mut().data.iter_mut() {
|
*color = map_fn(color);
|
||||||
*color = map_fn(color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Adjust<Color> for List<Color> {
|
impl Adjust<Color> for Gradient {
|
||||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
|
||||||
for element in self.iter_element_values_mut() {
|
|
||||||
*element = map_fn(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Adjust<Color> for List<GradientStops> {
|
|
||||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
|
||||||
for element in self.iter_element_values_mut() {
|
|
||||||
element.adjust(&map_fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Adjust<Color> for GradientStops {
|
|
||||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||||
for color in self.color.iter_mut() {
|
for color in self.color.iter_mut() {
|
||||||
*color = map_fn(color);
|
*color = map_fn(color);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user