mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 22:48:12 +08:00
Refactor the 'Fill' and 'Stroke' nodes to set "fill" and "stroke" attributes for paints (#4257)
* Allow using any graphics type for fill * Adapt gradient/fill property panels and tools to handle List<T> * Make the initial gradient transform covers the target's bounding box * Introduce AnyGraphicListDyn to avoid combinatorial explosion * Allow using any graphics type for stroke paint * Add FIll node migration * Add `for_each_vector_list_mut` instead of `set_paint_attribute` * Adapt paint flow to read and write attributes instead of legacy Fill/Stroke.color * Fix responsibilities of paint related node input setters * Fix Morph by storing List<Graphic> for attributes rather than the concrete types Store fill/stroke paints as List<Graphic> so Color/Gradient transitions do not hit set_attribute_value_dyn's type-mismatch fallback to default paint. * Preserve paint attributes in vector editing ops * Enhance the clarity between direct and chained fill gradients * Update demo arts * Consolidate Fill node gradient appearance inputs * Fix after the cubic review * Revert "Consolidate Fill node gradient appearance inputs" This reverts commit 9622feb20196e2c4da99e98ca95dcc2e34c2c98e. * Replace AnyGraphicListDyn with generic paint connectors on the Fill and Stroke nodes * Canonicalize paint attribute storage to List<Graphic> with a single write helper * Fix Solidify Stroke missing fills stored in the legacy style * Fix Solidify Stroke producing invisible strokes for legacy-only stroke colors * Fix the initial gradient transform ignoring the bounding box's vertical extent * Step paint at the morph midpoint instead of dropping it for unmixable pairings * Remove migration-stage comments * Clarify the initial gradient transform helper's doc and name * Correct the bake_paint_transforms doc and prune dead tolerance arms * Delete the unused Gradient::lerp * Fix a comment typo * Thread network paths through the legacy gradient bake so nested fills migrate * Restore the legacy fill fallback in Expand Fill and Stroke * Read gradient stops from the node's own input in the Fill properties panel * Use the transform input constant instead of a hardcoded index * Remove the unreachable wired color fallback in the Fill properties solid branch * Narrow the fill overlay redraw check to actual fill inputs * Coalesce the fill setter's graph runs into a single dispatch * Position the Gradient Value node inserted for gradient stops * Bake backup gradient placement during migration * Persist pending gradient bakes so unfinished migrations retry on reopen * Migrate the backup gradient's type and spread method * Harden the gradient-migration pass against document switches and stale bakes * Keep the Fill properties UI for layerless and nested Fill nodes * Leave a wired gradient transform input connected instead of overwriting it * Refuse to start a gradient chain ahead of existing layer content * Decode a Fill node's gradient through one shared reader * Nudge a degenerate bounding box so the Fill gradient transform stays invertible * Broadcast Fill and Stroke paint with a single attribute-column pass * Fix Morph stepping the target's stroke in near the source instead of the target * Tidy conventions: clippy get_first, comment periods, sentence-case test messages * Reattach the gradient orientation doc to its function * Re-save demo artwork --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
f82b0a8fca
commit
9f9899cfd0
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
@@ -37,6 +37,7 @@ use graph_craft::application_io::wgpu_available;
|
|||||||
use graph_craft::descriptor;
|
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 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;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
@@ -44,7 +45,7 @@ use graphene_std::subpath::Subpath;
|
|||||||
use graphene_std::vector::PointId;
|
use graphene_std::vector::PointId;
|
||||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||||
use graphene_std::vector::misc::dvec2_to_point;
|
use graphene_std::vector::misc::dvec2_to_point;
|
||||||
use graphene_std::vector::style::{Fill, RenderMode};
|
use graphene_std::vector::style::{Fill, Gradient, RenderMode};
|
||||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
use kurbo::{Affine, BezPath, Line, PathSeg};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -119,6 +120,12 @@ pub struct DocumentMessageHandler {
|
|||||||
pub graph_view_overlay_open: bool,
|
pub graph_view_overlay_open: bool,
|
||||||
/// The current opacity of the faded node graph background that covers up the artwork.
|
/// The current opacity of the faded node graph background that covers up the artwork.
|
||||||
pub graph_fade_artwork_percentage: f64,
|
pub graph_fade_artwork_percentage: f64,
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Fill nodes whose decomposed legacy gradient still awaits its bounding box measurement, each recorded as its enclosing
|
||||||
|
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
||||||
|
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, Gradient)>,
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
@@ -130,11 +137,6 @@ pub struct DocumentMessageHandler {
|
|||||||
/// The path of the to the document file.
|
/// The path of the to the document file.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub(crate) path: Option<PathBuf>,
|
pub(crate) path: Option<PathBuf>,
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// Set when a freshly-opened document still has legacy bounding-box-relative gradients; the deferred gradient
|
|
||||||
/// migration converts them to absolute after the first graph run (when geometry bounds are available) and clears this.
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) pending_gradient_migration: bool,
|
|
||||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
breadcrumb_network_path: Vec<NodeId>,
|
breadcrumb_network_path: Vec<NodeId>,
|
||||||
@@ -185,13 +187,13 @@ impl Default for DocumentMessageHandler {
|
|||||||
graph_view_overlay_open: false,
|
graph_view_overlay_open: false,
|
||||||
snapping_state: SnappingState::default(),
|
snapping_state: SnappingState::default(),
|
||||||
graph_fade_artwork_percentage: 80.,
|
graph_fade_artwork_percentage: 80.,
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
pending_gradient_bbox_bake: Vec::new(),
|
||||||
// =============================================
|
// =============================================
|
||||||
// Fields omitted from the saved document format
|
// Fields omitted from the saved document format
|
||||||
// =============================================
|
// =============================================
|
||||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||||
path: None,
|
path: None,
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
pending_gradient_migration: false,
|
|
||||||
breadcrumb_network_path: Vec::new(),
|
breadcrumb_network_path: Vec::new(),
|
||||||
selection_network_path: Vec::new(),
|
selection_network_path: Vec::new(),
|
||||||
history: DocumentHistory::default(),
|
history: DocumentHistory::default(),
|
||||||
@@ -2770,8 +2772,9 @@ impl DocumentMessageHandler {
|
|||||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
||||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
||||||
|
|
||||||
|
// `ATTR_FILL` is the source of truth when set; fall back to the legacy `style.fill` only when no attribute is present
|
||||||
let has_fill = if let Some(list) = fill_graphic_list {
|
let has_fill = if let Some(list) = fill_graphic_list {
|
||||||
list.element(0).is_some()
|
is_paint_present(list)
|
||||||
} else {
|
} else {
|
||||||
!matches!(style.fill, Fill::None)
|
!matches!(style.fill, Fill::None)
|
||||||
};
|
};
|
||||||
@@ -4011,6 +4014,21 @@ mod document_message_handler_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_utils::test_prelude::*;
|
use crate::test_utils::test_prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_gradient_bakes_round_trip_through_serialization() {
|
||||||
|
let document = DocumentMessageHandler {
|
||||||
|
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), Gradient::default())],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized = document.serialize_document();
|
||||||
|
let deserialized = DocumentMessageHandler::deserialize_document(&serialized).expect("Document with pending gradient bakes should deserialize");
|
||||||
|
assert_eq!(deserialized.pending_gradient_bbox_bake, document.pending_gradient_bbox_bake);
|
||||||
|
|
||||||
|
// The common empty case must not add the field to saved files
|
||||||
|
assert!(!DocumentMessageHandler::default().serialize_document().contains("pending_gradient_bbox_bake"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_layer_selection_with_shift_and_ctrl() {
|
async fn test_layer_selection_with_shift_and_ctrl() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
||||||
use glam::{DAffine2, DVec2};
|
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};
|
||||||
@@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode;
|
|||||||
use graphene_std::raster_types::Image;
|
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::{Fill, GradientSpreadMethod, GradientType, Stroke};
|
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke, build_transform_with_y_preservation};
|
||||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
||||||
|
|
||||||
@@ -454,29 +454,73 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_set(&mut self, fill: Fill) {
|
pub fn fill_set(&mut self, fill: Fill) {
|
||||||
let fill_index = 1;
|
|
||||||
let backup_color_index = 2;
|
|
||||||
let backup_gradient_index = 3;
|
|
||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
|
||||||
|
|
||||||
match &fill {
|
match &fill {
|
||||||
Fill::None => {
|
Fill::None => {
|
||||||
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
|
||||||
|
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), false);
|
||||||
}
|
}
|
||||||
Fill::Solid(color) => {
|
Fill::Solid(color) => {
|
||||||
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
|
||||||
|
|
||||||
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), false);
|
||||||
}
|
}
|
||||||
Fill::Gradient(gradient) => {
|
Fill::Gradient(gradient) => {
|
||||||
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::FillGradient(gradient.clone()), false), true);
|
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), true);
|
||||||
|
|
||||||
|
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX),
|
||||||
|
NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected
|
||||||
|
let old_transform: Option<DAffine2> = self
|
||||||
|
.network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&fill_node_id)
|
||||||
|
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
||||||
|
.and_then(|input| input.as_value())
|
||||||
|
.map(|value| {
|
||||||
|
if let TaggedValue::OptionalDAffine2(transform) = value {
|
||||||
|
transform.unwrap_or(DAffine2::IDENTITY)
|
||||||
|
} else {
|
||||||
|
DAffine2::IDENTITY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(old_transform) = old_transform {
|
||||||
|
let new_transform = build_transform_with_y_preservation(old_transform, gradient.start, gradient.end);
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
||||||
|
NodeInput::value(TaggedValue::OptionalDAffine2(Some(new_transform)), false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
|
||||||
|
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.set_input_with_refresh(
|
||||||
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
|
||||||
|
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let input_connector = InputConnector::node(fill_node_id, fill_index);
|
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||||
@@ -536,8 +580,42 @@ 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: GradientStops) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
|
||||||
return;
|
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
|
let starts_layer_chain = target == InputConnector::node(output_layer.to_node(), 1);
|
||||||
|
|
||||||
|
// The Gradient Value node discards its primary input, so starting a chain ahead of existing layer content would drop that content; refuse instead
|
||||||
|
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
||||||
|
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let node_id = NodeId::new();
|
||||||
|
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||||
|
|
||||||
|
if starts_layer_chain {
|
||||||
|
// No Fill node: the new node starts the layer's chain
|
||||||
|
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
||||||
|
} else {
|
||||||
|
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
||||||
|
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
||||||
|
if let Some(target_node_id) = target.node_id()
|
||||||
|
&& let Some(target_position) = self.network_interface.position(&target_node_id, &[])
|
||||||
|
{
|
||||||
|
let node_position = self.network_interface.position(&node_id, &[]).unwrap_or_default();
|
||||||
|
let desired_position = target_position + IVec2::new(-crate::consts::NODE_CHAIN_WIDTH, 2);
|
||||||
|
self.network_interface.shift_absolute_node_position(&node_id, desired_position - node_position, &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node_id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
||||||
@@ -615,6 +693,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
/// from the default (`Linear`).
|
/// from the default (`Linear`).
|
||||||
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
||||||
let create_if_nonexistent = gradient_type != GradientType::default();
|
let create_if_nonexistent = gradient_type != GradientType::default();
|
||||||
@@ -630,6 +709,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
/// from the default (`Pad`).
|
/// from the default (`Pad`).
|
||||||
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
||||||
let Some(output_layer) = self.get_output_layer() else { return };
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
|
||||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||||
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
||||||
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
||||||
@@ -655,7 +735,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::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(stroke.color), false), true);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.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);
|
||||||
@@ -804,40 +884,3 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
|
||||||
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
|
||||||
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
|
||||||
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
|
|
||||||
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
|
|
||||||
let old_x_length = old_x.length();
|
|
||||||
if old_x_length < 1e-9 {
|
|
||||||
return DVec2::new(-new_x.y, new_x.x);
|
|
||||||
}
|
|
||||||
let ex_old = old_x / old_x_length;
|
|
||||||
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
|
|
||||||
|
|
||||||
let new_x_length = new_x.length();
|
|
||||||
if new_x_length < 1e-9 {
|
|
||||||
return DVec2::ZERO;
|
|
||||||
}
|
|
||||||
let ex_new = new_x / new_x_length;
|
|
||||||
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
|
|
||||||
|
|
||||||
let parallel = old_y.dot(ex_old);
|
|
||||||
let perpendicular = old_y.dot(ey_old);
|
|
||||||
let scale = new_x_length / old_x_length;
|
|
||||||
|
|
||||||
scale * (parallel * ex_new + perpendicular * ey_new)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a new affine that maps canonical (0,0) -> (1,0) to (new_start, new_end), preserving the y-axis
|
|
||||||
/// shape of `old` proportionally to the x-axis length change.
|
|
||||||
fn build_transform_with_y_preservation(old: DAffine2, new_start: DVec2, new_end: DVec2) -> DAffine2 {
|
|
||||||
let new_x_axis = new_end - new_start;
|
|
||||||
let preserved_y_axis = scale_y_axis_to_match_new_x(old.matrix2.x_axis, old.matrix2.y_axis, new_x_axis);
|
|
||||||
DAffine2 {
|
|
||||||
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
|
|
||||||
translation: new_start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1765,11 +1765,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
||||||
use graphene_std::vector::generator_nodes::*;
|
use graphene_std::vector::generator_nodes::*;
|
||||||
|
|
||||||
let is_fill = matches!(value, TaggedValue::Fill(_));
|
|
||||||
let reference = network_interface.reference(&node_id, selection_network_path);
|
let reference = network_interface.reference(&node_id, selection_network_path);
|
||||||
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
||||||
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
||||||
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
||||||
|
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::<graphene_std::list::List<Graphic>>::INDEX;
|
||||||
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
||||||
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1782,7 +1782,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
input,
|
input,
|
||||||
});
|
});
|
||||||
responses.add(PropertiesPanelMessage::Refresh);
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
if is_fill {
|
if is_fill_input {
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
}
|
}
|
||||||
if is_stroke_node || is_fill_node || is_shape_generator_node || is_text_node {
|
if is_stroke_node || is_fill_node || is_shape_generator_node || is_text_node {
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ use graph_craft::application_io::resource::ResourceId;
|
|||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::{Type, concrete};
|
use graph_craft::{Type, concrete};
|
||||||
|
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::BrushStroke;
|
||||||
|
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;
|
||||||
use graphene_std::raster::{
|
use graphene_std::raster::{
|
||||||
@@ -31,7 +33,9 @@ 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, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{
|
||||||
|
FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
||||||
|
};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||||
|
|
||||||
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
||||||
@@ -2412,60 +2416,111 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
LayoutGroup::section(name, description, visible, pinned, expanded, node_id.0, Layout(layout))
|
LayoutGroup::section(name, description, visible, pinned, expanded, node_id.0, Layout(layout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The layer that a chain node ultimately feeds, if any. Returns `None` in a nested network since the layer metadata structure
|
||||||
|
/// is only loaded for the root document network, so a `LayerNodeIdentifier` can't be constructed there.
|
||||||
|
fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContext) -> Option<LayerNodeIdentifier> {
|
||||||
|
if !context.selection_network_path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?;
|
||||||
|
Some(LayerNodeIdentifier::new(layer_node, context.network_interface))
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer
|
/// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer
|
||||||
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
|
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
|
||||||
fn gradient_orientation_in_fill_node(node_id: NodeId, gradient: &graphene_std::vector::style::Gradient, context: &mut NodePropertiesContext) -> Option<bool> {
|
fn gradient_orientation_in_fill_node(node_id: NodeId, start: DVec2, end: DVec2, context: &mut NodePropertiesContext) -> Option<bool> {
|
||||||
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?;
|
let layer = root_layer_for_chain_node(node_id, context)?;
|
||||||
let layer = LayerNodeIdentifier::new(layer_node, context.network_interface);
|
|
||||||
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
|
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
|
||||||
Some(graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform))
|
Some(graph_modification_utils::gradient_orientation_rightward(start, end, transform))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill Node Widgets LayoutGroup
|
/// Fill Node Widgets LayoutGroup
|
||||||
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::vector::fill::*;
|
use graphene_std::vector::fill::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ResolvedFill {
|
||||||
|
Solid(Option<Color>),
|
||||||
|
Gradient {
|
||||||
|
gradient: GradientStops,
|
||||||
|
gradient_type: GradientType,
|
||||||
|
spread_method: GradientSpreadMethod,
|
||||||
|
transform: DAffine2,
|
||||||
|
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
|
||||||
|
transform_is_value: bool,
|
||||||
|
},
|
||||||
|
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::<Color>::INDEX, false, context));
|
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<List<Graphic>>::INDEX, false, context));
|
||||||
|
|
||||||
let document_node = match get_document_node(node_id, context) {
|
if get_document_node(node_id, context).is_ok_and(|node| node.inputs.get(FillInput::<List<Graphic>>::INDEX).is_some_and(|input| input.is_exposed())) {
|
||||||
Ok(document_node) => document_node,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Could not get document node in fill_properties: {err}");
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::FillGradient(backup_gradient))) = (
|
|
||||||
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
|
|
||||||
&document_node.inputs[BackupColorInput::INDEX].as_value(),
|
|
||||||
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
|
|
||||||
) {
|
|
||||||
(fill, backup_color, backup_gradient)
|
|
||||||
} else {
|
|
||||||
return vec![LayoutGroup::row(widgets_first_row)];
|
return vec![LayoutGroup::row(widgets_first_row)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Fill node not attached to a layer (or living in a nested network) still shows its full fill UI; only the gradient's
|
||||||
|
// 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 fill = match input_type.compiled_nested_type() {
|
||||||
|
Some(ty) if ty == &concrete!(List<Color>) => {
|
||||||
|
if let Ok(document_node) = get_document_node(node_id, context) {
|
||||||
|
let color = match document_node.inputs[FillInput::<List<Graphic>>::INDEX].as_value() {
|
||||||
|
Some(&TaggedValue::Color(c)) => c,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
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))
|
||||||
|
}) {
|
||||||
|
ResolvedFill::Gradient {
|
||||||
|
gradient: gradient.stops,
|
||||||
|
gradient_type: gradient.gradient_type,
|
||||||
|
spread_method: gradient.spread_method,
|
||||||
|
transform: gradient.transform,
|
||||||
|
transform_is_value: gradient.transform_is_value,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ResolvedFill::Other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ResolvedFill::Other,
|
||||||
};
|
};
|
||||||
let fill2 = fill.clone();
|
|
||||||
let backup_color_fill: Fill = (*backup_color).into();
|
let (backup_color, backup_gradient) = match get_document_node(node_id, context) {
|
||||||
let backup_gradient_fill: Fill = backup_gradient.clone().into();
|
Ok(document_node) => {
|
||||||
|
let backup_color = match document_node.inputs[BackupColorInput::INDEX].as_value() {
|
||||||
|
Some(&TaggedValue::Color(color)) => color,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let backup_stops = match document_node.inputs[BackupGradientInput::INDEX].as_value() {
|
||||||
|
Some(TaggedValue::Gradient(stops)) => stops.clone(),
|
||||||
|
_ => GradientStops::default(),
|
||||||
|
};
|
||||||
|
(backup_color, backup_stops)
|
||||||
|
}
|
||||||
|
Err(_) => (None, GradientStops::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match &fill {
|
||||||
|
ResolvedFill::Gradient { gradient: stops, .. } => {
|
||||||
|
let stops = stops.clone();
|
||||||
|
|
||||||
match fill {
|
|
||||||
Fill::Gradient(gradient) => {
|
|
||||||
let reverse_button = IconButton::new("Reverse", 24)
|
let reverse_button = IconButton::new("Reverse", 24)
|
||||||
.tooltip_label("Reverse Stops")
|
.tooltip_label("Reverse Stops")
|
||||||
.tooltip_description("Reverse the gradient color stops.")
|
.tooltip_description("Reverse the gradient color stops.")
|
||||||
.on_update(update_value(
|
.on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput::<List<Graphic>>::INDEX))
|
||||||
{
|
|
||||||
let gradient = gradient.clone();
|
|
||||||
move |_| {
|
|
||||||
let mut gradient = gradient.clone();
|
|
||||||
gradient.stops = gradient.stops.reversed();
|
|
||||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
node_id,
|
|
||||||
FillInput::<Color>::INDEX,
|
|
||||||
))
|
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets_first_row.push(reverse_button);
|
widgets_first_row.push(reverse_button);
|
||||||
@@ -2473,32 +2528,65 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
_ => add_blank_assist(&mut widgets_first_row),
|
_ => add_blank_assist(&mut widgets_first_row),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fill_choice_ui = match &fill {
|
||||||
|
ResolvedFill::Solid(color) => {
|
||||||
|
if let Some(color) = color {
|
||||||
|
FillChoiceUI::Solid(SRGBA8::from(*color))
|
||||||
|
} else {
|
||||||
|
FillChoiceUI::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStopsUI::from(stops)),
|
||||||
|
ResolvedFill::Other => FillChoiceUI::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let solid_set_messages = move |color: Option<Color>| Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: FillInput::<List<Graphic>>::INDEX,
|
||||||
|
value: TaggedValue::Color(color),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: BackupColorInput::INDEX,
|
||||||
|
value: TaggedValue::Color(color),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient_set_messages = move |gradient: GradientStops| Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: FillInput::<List<Graphic>>::INDEX,
|
||||||
|
value: TaggedValue::Gradient(gradient.clone()),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: BackupGradientInput::INDEX,
|
||||||
|
value: TaggedValue::Gradient(gradient),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets_first_row.push(
|
widgets_first_row.push(
|
||||||
ColorInput::default()
|
ColorInput::default()
|
||||||
.value(FillChoiceUI::from(&FillChoice::from(fill.clone())))
|
.value(fill_choice_ui)
|
||||||
.on_update(move |x: &ColorInput| {
|
.on_update(move |x: &ColorInput| match &x.value {
|
||||||
let new_fill = FillChoice::from(&x.value).to_fill(fill2.as_gradient());
|
FillChoiceUI::None => solid_set_messages(None),
|
||||||
let (backup_index, backup_value) = match &new_fill {
|
FillChoiceUI::Solid(srgba8) => {
|
||||||
Fill::None => (BackupColorInput::INDEX, TaggedValue::Color(None)),
|
let color = Some(Color::from(*srgba8));
|
||||||
Fill::Solid(color) => (BackupColorInput::INDEX, TaggedValue::Color(Some(*color))),
|
solid_set_messages(color)
|
||||||
Fill::Gradient(gradient) => (BackupGradientInput::INDEX, TaggedValue::FillGradient(gradient.clone())),
|
}
|
||||||
};
|
FillChoiceUI::Gradient(gradient_stops_ui) => {
|
||||||
Message::Batched {
|
let gradient = GradientStops::from(gradient_stops_ui);
|
||||||
messages: Box::new([
|
gradient_set_messages(gradient)
|
||||||
NodeGraphMessage::SetInputValue {
|
|
||||||
node_id,
|
|
||||||
input_index: backup_index,
|
|
||||||
value: backup_value,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
NodeGraphMessage::SetInputValue {
|
|
||||||
node_id,
|
|
||||||
input_index: FillInput::<Color>::INDEX,
|
|
||||||
value: TaggedValue::Fill(new_fill),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
]),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
@@ -2514,61 +2602,50 @@ 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::Fill(backup_color_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
.on_update(update_value(move |_| TaggedValue::Color(backup_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")
|
||||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
.on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput::<List<Graphic>>::INDEX))
|
||||||
.on_commit(commit_value),
|
.on_commit(commit_value),
|
||||||
];
|
];
|
||||||
|
|
||||||
row.extend_from_slice(&[
|
row.extend_from_slice(&[
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_instance(),
|
RadioInput::new(entries)
|
||||||
|
.selected_index(Some(if matches!(fill, ResolvedFill::Gradient { .. }) { 1 } else { 0 }))
|
||||||
|
.widget_instance(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
LayoutGroup::row(row)
|
LayoutGroup::row(row)
|
||||||
};
|
};
|
||||||
widgets.push(fill_type_switch);
|
widgets.push(fill_type_switch);
|
||||||
|
|
||||||
if let Fill::Gradient(gradient) = fill.clone() {
|
if let ResolvedFill::Gradient {
|
||||||
|
gradient_type,
|
||||||
|
spread_method,
|
||||||
|
transform,
|
||||||
|
transform_is_value,
|
||||||
|
..
|
||||||
|
} = fill.clone()
|
||||||
|
{
|
||||||
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
|
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
|
||||||
let mut row = vec![TextLabel::new("").widget_instance()];
|
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||||
add_blank_assist(&mut row);
|
add_blank_assist(&mut row);
|
||||||
|
|
||||||
let gradient_for_closure = gradient.clone();
|
|
||||||
|
|
||||||
let entries = [GradientType::Linear, GradientType::Radial]
|
let entries = [GradientType::Linear, GradientType::Radial]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&grad_type| {
|
.map(|&grad_type| {
|
||||||
let gradient = gradient_for_closure.clone();
|
|
||||||
let set_input_value = update_value(
|
|
||||||
move |_: &()| {
|
|
||||||
let mut new_gradient = gradient.clone();
|
|
||||||
new_gradient.gradient_type = grad_type;
|
|
||||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
|
||||||
},
|
|
||||||
node_id,
|
|
||||||
FillInput::<Color>::INDEX,
|
|
||||||
);
|
|
||||||
RadioEntryData::new(format!("{:?}", grad_type))
|
RadioEntryData::new(format!("{:?}", grad_type))
|
||||||
.label(format!("{:?}", grad_type))
|
.label(format!("{:?}", grad_type))
|
||||||
.on_update(move |_| Message::Batched {
|
.on_update(update_value(move |_| TaggedValue::GradientType(grad_type), node_id, GradientTypeInput::INDEX))
|
||||||
messages: Box::new([
|
|
||||||
set_input_value(&()),
|
|
||||||
GradientToolMessage::UpdateOptions {
|
|
||||||
options: GradientOptionsUpdate::Type(grad_type),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
row.extend_from_slice(&[
|
row.extend_from_slice(&[
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_instance(),
|
RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_instance(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
widgets.push(LayoutGroup::row(row));
|
widgets.push(LayoutGroup::row(row));
|
||||||
@@ -2577,75 +2654,41 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
// space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar.
|
// space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar.
|
||||||
let mut spread_methods_row = vec![TextLabel::new("").widget_instance()];
|
let mut spread_methods_row = vec![TextLabel::new("").widget_instance()];
|
||||||
|
|
||||||
let orientation_rightward = gradient_orientation_in_fill_node(node_id, &gradient, context).unwrap_or(true);
|
// The button writes a value into the transform input, so only offer it when the input isn't wired
|
||||||
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
if transform_is_value {
|
||||||
.tooltip_label("Reverse Direction")
|
let start = transform.transform_point2(DVec2::ZERO);
|
||||||
.tooltip_description(if gradient.gradient_type == GradientType::Radial {
|
let end = transform.transform_point2(DVec2::X);
|
||||||
"Reverse which end the gradient radiates from."
|
let new_transform = build_transform_with_y_preservation(transform, end, start);
|
||||||
} else {
|
let orientation_rightward = gradient_orientation_in_fill_node(node_id, start, end, context).unwrap_or(true);
|
||||||
"Swap the start and end points of the gradient line."
|
|
||||||
})
|
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||||
.on_update(update_value(
|
.tooltip_label("Reverse Direction")
|
||||||
{
|
.tooltip_description(if gradient_type == GradientType::Radial {
|
||||||
let gradient = gradient.clone();
|
"Reverse which end the gradient radiates from."
|
||||||
move |_| {
|
} else {
|
||||||
let mut gradient = gradient.clone();
|
"Swap the start and end points of the gradient line."
|
||||||
std::mem::swap(&mut gradient.start, &mut gradient.end);
|
})
|
||||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
.on_update(update_value(move |_| TaggedValue::OptionalDAffine2(Some(new_transform)), node_id, TransformInput::INDEX))
|
||||||
}
|
.widget_instance();
|
||||||
},
|
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
node_id,
|
spread_methods_row.push(reverse_direction_button);
|
||||||
FillInput::<Color>::INDEX,
|
} else {
|
||||||
))
|
add_blank_assist(&mut spread_methods_row);
|
||||||
.widget_instance();
|
}
|
||||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
|
||||||
spread_methods_row.push(reverse_direction_button);
|
|
||||||
|
|
||||||
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
|
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&spread_method| {
|
.map(|&spread_method| {
|
||||||
let gradient_for_input = gradient_for_closure.clone();
|
|
||||||
let gradient_for_backup = gradient_for_closure.clone();
|
|
||||||
|
|
||||||
let set_input_value = update_value(
|
|
||||||
move |_: &()| {
|
|
||||||
let mut new_gradient = gradient_for_input.clone();
|
|
||||||
new_gradient.spread_method = spread_method;
|
|
||||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
|
||||||
},
|
|
||||||
node_id,
|
|
||||||
FillInput::<Color>::INDEX,
|
|
||||||
);
|
|
||||||
|
|
||||||
let set_backup_value = update_value(
|
|
||||||
move |_: &()| {
|
|
||||||
let mut new_gradient = gradient_for_backup.clone();
|
|
||||||
new_gradient.spread_method = spread_method;
|
|
||||||
TaggedValue::FillGradient(new_gradient)
|
|
||||||
},
|
|
||||||
node_id,
|
|
||||||
BackupGradientInput::INDEX,
|
|
||||||
);
|
|
||||||
|
|
||||||
RadioEntryData::new(format!("{:?}", spread_method))
|
RadioEntryData::new(format!("{:?}", spread_method))
|
||||||
.label(format!("{:?}", spread_method))
|
.label(format!("{:?}", spread_method))
|
||||||
.on_update(move |_| Message::Batched {
|
.on_update(update_value(move |_| TaggedValue::GradientSpreadMethod(spread_method), node_id, SpreadMethodInput::INDEX))
|
||||||
messages: Box::new([
|
|
||||||
set_input_value(&()),
|
|
||||||
set_backup_value(&()),
|
|
||||||
GradientToolMessage::UpdateOptions {
|
|
||||||
options: GradientOptionsUpdate::SetSpreadMethod(spread_method),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
spread_methods_row.extend_from_slice(&[
|
spread_methods_row.extend_from_slice(&[
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance(),
|
RadioInput::new(spread_method_entries).selected_index(Some(spread_method as u32)).widget_instance(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
widgets.push(LayoutGroup::row(spread_methods_row));
|
widgets.push(LayoutGroup::row(spread_methods_row));
|
||||||
@@ -2660,7 +2703,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||||||
let document_node = match get_document_node(node_id, context) {
|
let document_node = match get_document_node(node_id, context) {
|
||||||
Ok(document_node) => document_node,
|
Ok(document_node) => document_node,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Could not get document node in fill_properties: {err}");
|
log::error!("Could not get document node in stroke_properties: {err}");
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2676,7 +2719,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||||||
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
|
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
|
||||||
|
|
||||||
let color = color_widget(
|
let color = color_widget(
|
||||||
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
|
ParameterWidgetsInfo::new(node_id, PaintInput::<List<Graphic>>::INDEX, true, context),
|
||||||
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
|
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
|
||||||
);
|
);
|
||||||
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
|
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
|||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
|
||||||
use crate::messages::prelude::DocumentMessageHandler;
|
use crate::messages::prelude::DocumentMessageHandler;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
|
||||||
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::descriptor;
|
||||||
@@ -15,7 +14,7 @@ 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::style::{PaintOrder, StrokeAlign};
|
use graphene_std::vector::style::{Fill, 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;
|
||||||
@@ -1122,9 +1121,6 @@ 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();
|
||||||
|
|
||||||
// Legacy `Fill::Gradient`s are converted to absolute by the deferred migration pre-pass that measures each fill's geometry
|
|
||||||
document.pending_gradient_migration = !graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).is_empty();
|
|
||||||
|
|
||||||
let network = document.network_interface.document_network().clone();
|
let network = document.network_interface.document_network().clone();
|
||||||
|
|
||||||
// Apply string and node replacements to each node
|
// Apply string and node replacements to each node
|
||||||
@@ -1575,6 +1571,96 @@ 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
|
||||||
|
// value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform).
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 {
|
||||||
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
|
||||||
|
// Content: no change
|
||||||
|
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
|
||||||
|
match old_inputs[1].as_value() {
|
||||||
|
Some(TaggedValue::Fill(old_fill)) => {
|
||||||
|
let exposed = old_inputs[1].is_exposed();
|
||||||
|
let fill_value = match old_fill {
|
||||||
|
Fill::None => TaggedValue::Color(None),
|
||||||
|
Fill::Solid(color) => TaggedValue::Color(Some(*color)),
|
||||||
|
Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
|
||||||
|
};
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.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
|
||||||
|
if let Fill::Gradient(gradient) = old_fill {
|
||||||
|
document.network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, 4),
|
||||||
|
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||||
|
network_path,
|
||||||
|
);
|
||||||
|
document.network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, 5),
|
||||||
|
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
|
||||||
|
network_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
let transform = if gradient.absolute {
|
||||||
|
Some(gradient.transform * gradient.to_transform())
|
||||||
|
} else {
|
||||||
|
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint target's bounds
|
||||||
|
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.
|
||||||
|
// The generic paint connector accepts `List<Color>`/`List<GradientStops>` sources directly, and there were no other nodes which can generate output type that implements `From` for `Fill`.
|
||||||
|
_ => {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color backup: no change
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||||
|
|
||||||
|
// Gradient backup: extract stops
|
||||||
|
if let Some(TaggedValue::FillGradient(g)) = old_inputs[3].as_value() {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path);
|
||||||
|
|
||||||
|
// A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore
|
||||||
|
if matches!(old_inputs[1].as_value(), Some(TaggedValue::Fill(Fill::None | Fill::Solid(_)))) {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path);
|
||||||
|
document.network_interface.set_input(
|
||||||
|
&InputConnector::node(*node_id, 5),
|
||||||
|
NodeInput::value(TaggedValue::GradientSpreadMethod(g.spread_method), false),
|
||||||
|
network_path,
|
||||||
|
);
|
||||||
|
|
||||||
|
let transform = if g.absolute {
|
||||||
|
Some(g.transform * g.to_transform())
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
||||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
|||||||
@@ -1491,9 +1491,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
// A freshly-opened document with legacy gradients runs a one-time measurement pre-pass instead of rendering, until every gradient is converted to absolute space
|
// A freshly-opened document with legacy gradients (whether newly decomposed or persisted from a save made before every bake landed) runs a
|
||||||
if document.pending_gradient_migration {
|
// one-time measurement pre-pass instead of rendering, until every gradient's transform is baked into absolute space
|
||||||
self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses);
|
if !document.pending_gradient_bbox_bake.is_empty() && self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||||
use graphene_std::Color;
|
|
||||||
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;
|
||||||
@@ -15,8 +14,9 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster};
|
|||||||
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::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
|
||||||
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType};
|
||||||
|
use graphene_std::{Color, Graphic};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
||||||
@@ -271,11 +271,18 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN
|
|||||||
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center)
|
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the closest Fill node's ID to the provided layer, if any.
|
||||||
|
pub fn get_fill_node_id_with_direct_fill_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
|
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||||
|
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||||
|
matches!(fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?, NodeInput::Value { .. }).then_some(fill_node_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine the input connector where the gradient chain enters the layer.
|
/// Determine the input connector where the gradient chain enters the layer.
|
||||||
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
|
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
|
||||||
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
|
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
|
||||||
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
|
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
|
||||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX)
|
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)
|
||||||
} else {
|
} else {
|
||||||
InputConnector::node(layer.to_node(), 1)
|
InputConnector::node(layer.to_node(), 1)
|
||||||
}
|
}
|
||||||
@@ -292,89 +299,29 @@ pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_i
|
|||||||
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
|
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// Get the layer's "Fill" node itself (whose `fill` input holds the paint value), not the node feeding that input.
|
|
||||||
pub fn get_fill_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
|
||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the node connected to Fill's fill input, if any.
|
/// Get the node connected to Fill's fill input, if any.
|
||||||
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||||
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||||
let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)? else {
|
let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(*node_id)
|
Some(*node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current gradient of a layer from the closest "Fill" node.
|
|
||||||
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
|
||||||
let fill_index = 1;
|
|
||||||
|
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
|
||||||
let TaggedValue::Fill(Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
Some(gradient.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// The legacy bounding-box-relative gradient (`absolute == false`) in a "Fill" node's active `fill` input, if any.
|
|
||||||
fn legacy_active_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
|
||||||
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
|
||||||
let TaggedValue::Fill(Fill::Gradient(gradient)) = node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
(!gradient.absolute).then(|| gradient.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// The legacy bounding-box-relative gradient (`absolute == false`) stashed in a "Fill" node's `_backup_gradient` input, if any.
|
|
||||||
/// The backup is inert until the fill is toggled back to a gradient, at which point it becomes the active fill, so it needs converting too.
|
|
||||||
fn legacy_backup_gradient_in_fill_node(fill_node_id: NodeId, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
|
||||||
let node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
|
||||||
let TaggedValue::FillGradient(gradient) = node.inputs.get(graphene_std::vector::fill::BackupGradientInput::INDEX)?.as_value()? else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
(!gradient.absolute).then(|| gradient.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// Convert a "Fill" node's legacy gradients (the active `fill` and/or the stashed `_backup_gradient`) to absolute space using
|
|
||||||
/// the geometry's measured bounding box, writing each back in place. The active fill is written as a `Fill`, the backup as a bare `FillGradient`.
|
|
||||||
pub fn migrate_fill_node_gradients_to_absolute(fill_node_id: NodeId, network_interface: &mut NodeNetworkInterface, bounding_box: DAffine2, layer_transform: DAffine2) {
|
|
||||||
if let Some(gradient) = legacy_active_gradient_in_fill_node(fill_node_id, network_interface) {
|
|
||||||
let absolute = gradient.to_absolute(bounding_box, layer_transform);
|
|
||||||
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX);
|
|
||||||
network_interface.set_input(&input, NodeInput::value(TaggedValue::Fill(Fill::Gradient(absolute)), false), &[]);
|
|
||||||
}
|
|
||||||
if let Some(gradient) = legacy_backup_gradient_in_fill_node(fill_node_id, network_interface) {
|
|
||||||
let absolute = gradient.to_absolute(bounding_box, layer_transform);
|
|
||||||
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
|
||||||
network_interface.set_input(&input, NodeInput::value(TaggedValue::FillGradient(absolute), false), &[]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
/// Find every root-network "Fill" node holding a legacy bounding-box-relative gradient, either as its active `fill` or as its `_backup_gradient`.
|
|
||||||
///
|
|
||||||
/// Scans the document network structurally instead of walking each layer's primary flow, so it also catches fills on
|
|
||||||
/// secondary inputs and in hidden, disabled, or orphaned branches. Fills nested inside subgraph node networks are skipped.
|
|
||||||
pub fn legacy_gradient_fill_nodes(network_interface: &NodeNetworkInterface) -> Vec<NodeId> {
|
|
||||||
let fill_identifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
|
||||||
network_interface
|
|
||||||
.document_network()
|
|
||||||
.nodes
|
|
||||||
.keys()
|
|
||||||
.copied()
|
|
||||||
.filter(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&fill_identifier))
|
|
||||||
.filter(|&node_id| legacy_active_gradient_in_fill_node(node_id, network_interface).is_some() || legacy_backup_gradient_in_fill_node(node_id, network_interface).is_some())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<GradientStops> {
|
||||||
|
// Try to find the gradient stops value that is created by a Fill node first
|
||||||
|
if let Some(fill_node_id) = get_fill_node_id_with_direct_fill_input(layer, network_interface) {
|
||||||
|
return network_interface
|
||||||
|
.document_network()
|
||||||
|
.nodes
|
||||||
|
.get(&fill_node_id)
|
||||||
|
.and_then(|node| node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX))
|
||||||
|
.and_then(|input| input.as_value())
|
||||||
|
.and_then(|value| if let TaggedValue::Gradient(gradient) = value { Some(gradient.clone()) } else { None });
|
||||||
|
}
|
||||||
|
|
||||||
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?;
|
||||||
let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
|
let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
@@ -398,14 +345,6 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
|
|||||||
.unwrap_or(metadata.document_to_viewport);
|
.unwrap_or(metadata.document_to_viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
// Only an existing legacy `Fill::Gradient` is in (0, 0)..(1, 1) bounding-box space; migrated and newly-created gradients are absolute (layer space).
|
|
||||||
if get_gradient(layer, network_interface).is_some_and(|gradient| !gradient.absolute) {
|
|
||||||
let bounds = metadata.nonzero_bounding_box(layer);
|
|
||||||
let bound_transform = glam::DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
|
||||||
return metadata.transform_to_viewport(layer) * bound_transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata.transform_to_viewport(layer)
|
metadata.transform_to_viewport(layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,13 +362,11 @@ pub fn gradient_orientation_rightward(start: glam::DVec2, end: glam::DVec2, tran
|
|||||||
|
|
||||||
/// 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 fill_index = 1;
|
|
||||||
|
|
||||||
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::Fill(Fill::Solid(color)) = inputs.get(fill_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;
|
||||||
};
|
};
|
||||||
Some(color)
|
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.
|
||||||
@@ -686,16 +623,74 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A Fill node's decoded gradient inputs, with the transform kept in its raw form (not yet baked into `start`/`end`).
|
||||||
|
pub struct FillNodeGradient {
|
||||||
|
pub stops: GradientStops,
|
||||||
|
pub gradient_type: GradientType,
|
||||||
|
pub spread_method: GradientSpreadMethod,
|
||||||
|
pub transform: DAffine2,
|
||||||
|
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
|
||||||
|
pub transform_is_value: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a Fill node's gradient metadata inputs, resolving an unset transform to the default over `bounding_box`. Returns `None` when the fill input isn't a gradient value.
|
||||||
|
pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option<FillNodeGradient> {
|
||||||
|
use graphene_std::vector::fill;
|
||||||
|
|
||||||
|
let TaggedValue::Gradient(stops) = fill_node.inputs.get(fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let gradient_type = match fill_node.inputs.get(fill::GradientTypeInput::INDEX).and_then(|input| input.as_value()) {
|
||||||
|
Some(&TaggedValue::GradientType(value)) => value,
|
||||||
|
_ => GradientType::default(),
|
||||||
|
};
|
||||||
|
let spread_method = match fill_node.inputs.get(fill::SpreadMethodInput::INDEX).and_then(|input| input.as_value()) {
|
||||||
|
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
|
||||||
|
_ => GradientSpreadMethod::default(),
|
||||||
|
};
|
||||||
|
let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value());
|
||||||
|
let transform = match transform_input {
|
||||||
|
Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())),
|
||||||
|
_ => DAffine2::IDENTITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(FillNodeGradient {
|
||||||
|
stops: stops.clone(),
|
||||||
|
gradient_type,
|
||||||
|
spread_method,
|
||||||
|
transform,
|
||||||
|
transform_is_value: transform_input.is_some(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update this to return Graphic once the legacy `Fill` enum has been eliminated
|
||||||
/// Returns the `Fill` value from a layer's upstream Fill node.
|
/// Returns the `Fill` value from a layer's upstream Fill node.
|
||||||
pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> {
|
pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> {
|
||||||
let fill_index = graphene_std::vector::fill::FillInput::<Fill>::INDEX;
|
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||||
let tagged = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER), fill_index)?;
|
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||||
if let TaggedValue::Fill(fill) = tagged { Some(fill.clone()) } else { None }
|
|
||||||
|
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
|
||||||
|
&TaggedValue::Color(color) => Some(color.map_or(Fill::None, Fill::Solid)),
|
||||||
|
TaggedValue::Gradient(_) => {
|
||||||
|
let gradient = read_fill_node_gradient(fill_node, || network_interface.document_metadata().nonzero_bounding_box(layer))?;
|
||||||
|
Some(Fill::Gradient(Gradient {
|
||||||
|
stops: gradient.stops,
|
||||||
|
gradient_type: gradient.gradient_type,
|
||||||
|
spread_method: gradient.spread_method,
|
||||||
|
start: gradient.transform.transform_point2(DVec2::ZERO),
|
||||||
|
end: gradient.transform.transform_point2(DVec2::X),
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
absolute: true,
|
||||||
|
transform: DAffine2::IDENTITY,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the stroke color from a layer's upstream Stroke node.
|
/// Returns the stroke color from a layer's upstream Stroke node.
|
||||||
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::ColorInput::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 }
|
if let TaggedValue::Color(color) = tagged { Some(*color) } else { None }
|
||||||
}
|
}
|
||||||
@@ -816,7 +811,7 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
|
|||||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
|
||||||
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::ColorInput::INDEX;
|
let input_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
|
||||||
let value = TaggedValue::Color(color);
|
let value = TaggedValue::Color(color);
|
||||||
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -203,16 +203,16 @@ impl Fsm for FillToolFsmState {
|
|||||||
mod test_fill {
|
mod test_fill {
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
|
use graphene_std::list::List;
|
||||||
use graphene_std::vector::fill;
|
use graphene_std::vector::fill;
|
||||||
use graphene_std::vector::style::Fill;
|
|
||||||
|
|
||||||
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> {
|
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<List<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<Fill>>(&editor.runtime).collect()
|
instrumented.grab_all_input::<fill::FillInput<List<Color>>>(&editor.runtime).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -242,7 +242,8 @@ 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);
|
||||||
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::GREEN));
|
let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
|
||||||
|
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -254,6 +255,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);
|
||||||
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::YELLOW));
|
let color = fills.first().unwrap().element(0).expect("Color is stored in the list");
|
||||||
|
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasi
|
|||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops, gradient_chain_target_input};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||||
|
self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input,
|
||||||
|
};
|
||||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
@@ -355,7 +357,12 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
|
|||||||
// TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot
|
// TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot
|
||||||
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||||
if let Some(stops) = get_gradient_stops(layer, network_interface) {
|
if let Some(stops) = get_gradient_stops(layer, network_interface) {
|
||||||
// Try to construct a gradient out of a chain, which is directly connected to a layer
|
// A Fill node holding a direct gradient value decodes through the shared reader
|
||||||
|
if get_fill_node_id_with_direct_fill_input(layer, network_interface).is_some() {
|
||||||
|
return graph_modification_utils::get_fill_value(layer, network_interface)?.as_gradient().cloned();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, try to construct a gradient out of a chain, which is directly connected to a Fill node or a layer
|
||||||
let chain_state = read_gradient_chain_state(layer, network_interface);
|
let chain_state = read_gradient_chain_state(layer, network_interface);
|
||||||
Some(Gradient {
|
Some(Gradient {
|
||||||
stops,
|
stops,
|
||||||
@@ -368,8 +375,7 @@ fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
|||||||
transform: DAffine2::IDENTITY,
|
transform: DAffine2::IDENTITY,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Try to find a legacy Fill::Gradient that is selected in a Fill node
|
None
|
||||||
graph_modification_utils::get_gradient(layer, network_interface)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +487,7 @@ struct SelectedGradient {
|
|||||||
dragging: GradientDragTarget,
|
dragging: GradientDragTarget,
|
||||||
initial_gradient: Gradient,
|
initial_gradient: Gradient,
|
||||||
// TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `List<GradientStops>` replaces legacy `Fill::Gradient`
|
// TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `List<GradientStops>` replaces legacy `Fill::Gradient`
|
||||||
is_gradient_list: 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: &GradientStops, mouse: DVec2) -> Option<f64> {
|
||||||
@@ -531,7 +537,7 @@ impl SelectedGradient {
|
|||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: GradientDragTarget::End,
|
dragging: GradientDragTarget::End,
|
||||||
initial_gradient: gradient,
|
initial_gradient: gradient,
|
||||||
is_gradient_list: get_gradient_stops(layer, &document.network_interface).is_some(),
|
is_gradient_chain: get_upstream_gradient_value_node_id(layer, &document.network_interface).is_some(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,7 +754,7 @@ impl SelectedGradient {
|
|||||||
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
||||||
if let Some(layer) = self.layer {
|
if let Some(layer) = self.layer {
|
||||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
|
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
|
||||||
if self.is_gradient_list {
|
if self.is_gradient_chain {
|
||||||
dispatch_gradient_writes(layer, &self.gradient, responses);
|
dispatch_gradient_writes(layer, &self.gradient, responses);
|
||||||
} else {
|
} else {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
@@ -1173,7 +1179,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
// The gradient has only one point and so should become a fill
|
// The gradient has only one point and so should become a fill
|
||||||
// TODO: Drop the legacy `Fill::Solid` branch when all gradients become `List<GradientStops>`
|
// TODO: Drop the legacy `Fill::Solid` branch when all gradients become `List<GradientStops>`
|
||||||
if selected_gradient.gradient.stops.len() == 1 {
|
if selected_gradient.gradient.stops.len() == 1 {
|
||||||
if selected_gradient.is_gradient_list {
|
if selected_gradient.is_gradient_chain {
|
||||||
selected_gradient.render_gradient(responses);
|
selected_gradient.render_gradient(responses);
|
||||||
} else if let Some(layer) = selected_gradient.layer {
|
} else if let Some(layer) = selected_gradient.layer {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
@@ -1270,7 +1276,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
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) {
|
||||||
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
let is_gradient_list = get_gradient_stops(layer, &document.network_interface).is_some();
|
let is_gradient_chain = get_upstream_gradient_value_node_id(layer, &document.network_interface).is_some();
|
||||||
|
|
||||||
// Check for dragging a midpoint diamond
|
// Check for dragging a midpoint diamond
|
||||||
if drag_hint.is_none() {
|
if drag_hint.is_none() {
|
||||||
@@ -1298,7 +1304,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: GradientDragTarget::Midpoint(i),
|
dragging: GradientDragTarget::Midpoint(i),
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
is_gradient_list,
|
is_gradient_chain,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -1339,7 +1345,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: drag_target,
|
dragging: drag_target,
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
is_gradient_list,
|
is_gradient_chain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1356,7 +1362,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: dragging_target,
|
dragging: dragging_target,
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
is_gradient_list,
|
is_gradient_chain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1785,7 @@ fn apply_gradient_update(
|
|||||||
|
|
||||||
// Only check for the gradient list once we know we'll write back, since this is a graph traversal per layer
|
// Only check for the gradient list once we know we'll write back, since this is a graph traversal per layer
|
||||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
|
// TODO: Drop the `Fill::Gradient` branch when all gradients become `List<GradientStops>`
|
||||||
if get_gradient_stops(layer, &context.document.network_interface).is_some() {
|
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
|
||||||
dispatch_gradient_writes(layer, &gradient, responses);
|
dispatch_gradient_writes(layer, &gradient, responses);
|
||||||
} else {
|
} else {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
@@ -1821,7 +1827,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if get_gradient_stops(layer, &context.document.network_interface).is_some() {
|
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
|
||||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
|
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
|
||||||
updated_any_layer = true;
|
updated_any_layer = true;
|
||||||
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
|
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
|
||||||
@@ -1924,40 +1930,62 @@ mod test_gradient {
|
|||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_fill_node_id_with_direct_fill_input;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id;
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
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::vector::style::{Fill, Gradient};
|
use graphene_std::list::List;
|
||||||
|
use graphene_std::vector::style::{Gradient, GradientSpreadMethod};
|
||||||
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
||||||
|
use graphene_std::{Graphic, NodeInputDecleration};
|
||||||
|
|
||||||
use super::gradient_space_transform;
|
use super::gradient_space_transform;
|
||||||
|
|
||||||
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<(Fill, DAffine2)> {
|
async fn get_gradients_from_fill(editor: &mut EditorTestUtils) -> Vec<(Gradient, DAffine2)> {
|
||||||
let instrumented = match editor.eval_graph().await {
|
|
||||||
Ok(instrumented) => instrumented,
|
|
||||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let document = editor.active_document();
|
let document = editor.active_document();
|
||||||
let layers = document.metadata().all_layers();
|
document
|
||||||
layers
|
.metadata()
|
||||||
|
.all_layers()
|
||||||
.filter_map(|layer| {
|
.filter_map(|layer| {
|
||||||
let fill = instrumented.grab_input_from_layer::<fill::FillInput<Fill>>(layer, &document.network_interface, &editor.runtime)?;
|
// Only read Fill-owned gradient values, not chains
|
||||||
|
get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?;
|
||||||
|
|
||||||
|
let gradient = super::get_gradient(layer, &document.network_interface)?;
|
||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
Some((fill, transform))
|
Some((gradient, transform))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_gradient(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) {
|
async fn get_gradients_from_chain(editor: &mut EditorTestUtils) -> Vec<(Gradient, DAffine2)> {
|
||||||
let fills = get_fills(editor).await;
|
let document = editor.active_document();
|
||||||
assert_eq!(fills.len(), 1, "Expected 1 gradient fill, found {}", fills.len());
|
document
|
||||||
|
.metadata()
|
||||||
|
.all_layers()
|
||||||
|
.filter_map(|layer| {
|
||||||
|
// Only read actual gradient chains, not Fill-owned gradient values
|
||||||
|
get_upstream_gradient_value_node_id(layer, &document.network_interface)?;
|
||||||
|
|
||||||
let (fill, transform) = fills.first().unwrap();
|
let gradient = super::get_gradient(layer, &document.network_interface)?;
|
||||||
let gradient = fill.as_gradient().expect("Expected gradient fill type");
|
let transform = gradient_space_transform(layer, document);
|
||||||
|
Some((gradient, transform))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
(gradient.clone(), *transform)
|
async fn get_gradient_from_fill(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) {
|
||||||
|
let gradients = get_gradients_from_fill(editor).await;
|
||||||
|
assert_eq!(gradients.len(), 1, "Expected 1 gradient fill, found {}", gradients.len());
|
||||||
|
|
||||||
|
gradients.into_iter().next().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_gradient_from_chain(editor: &mut EditorTestUtils) -> (Gradient, DAffine2) {
|
||||||
|
let gradients = get_gradients_from_chain(editor).await;
|
||||||
|
assert_eq!(gradients.len(), 1, "Expected 1 gradient chain, found {}", gradients.len());
|
||||||
|
gradients.into_iter().next().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_stops_at_positions(actual_positions: &[f64], expected_positions: &[f64], tolerance: f64) {
|
fn assert_stops_at_positions(actual_positions: &[f64], expected_positions: &[f64], tolerance: f64) {
|
||||||
@@ -2010,13 +2038,51 @@ mod test_gradient {
|
|||||||
layer
|
layer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_fill_gradient_chain_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier {
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
let document = editor.active_document();
|
||||||
|
let layer = document.metadata().all_layers().next().unwrap();
|
||||||
|
let fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface).expect("Fill node should exist");
|
||||||
|
|
||||||
|
let gradient_node_id = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)).await;
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::CreateWire {
|
||||||
|
output_connector: OutputConnector::node(gradient_node_id, 0),
|
||||||
|
input_connector: InputConnector::node(fill_node_id, fill::FillInput::<List<Graphic>>::INDEX),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
|
node_id: gradient_node_id,
|
||||||
|
input_index: 1,
|
||||||
|
value: TaggedValue::Gradient(GradientStops::new([
|
||||||
|
GradientStop {
|
||||||
|
position: 0.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::RED,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 1.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::BLUE,
|
||||||
|
},
|
||||||
|
])),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ignore_artboard() {
|
async fn ignore_artboard() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
editor.new_document().await;
|
editor.new_document().await;
|
||||||
editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
|
||||||
assert!(get_fills(&mut editor).await.is_empty());
|
assert!(get_gradients_from_fill(&mut editor).await.is_empty());
|
||||||
|
assert!(get_gradients_from_chain(&mut editor).await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -2025,7 +2091,8 @@ mod test_gradient {
|
|||||||
editor.new_document().await;
|
editor.new_document().await;
|
||||||
editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await;
|
editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await;
|
||||||
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 2., 2., 4., 4., ModifierKeys::empty()).await;
|
||||||
assert!(get_fills(&mut editor).await.is_empty());
|
assert!(get_gradients_from_fill(&mut editor).await.is_empty());
|
||||||
|
assert!(get_gradients_from_chain(&mut editor).await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -2037,7 +2104,7 @@ mod test_gradient {
|
|||||||
editor.select_secondary_color(Color::BLUE).await;
|
editor.select_secondary_color(Color::BLUE).await;
|
||||||
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
let (gradient, transform) = get_gradient(&mut editor).await;
|
let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
|
||||||
|
|
||||||
// Gradient goes from primary color to secondary color
|
// Gradient goes from primary color to secondary color
|
||||||
let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>();
|
let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>();
|
||||||
@@ -2046,6 +2113,21 @@ mod test_gradient {
|
|||||||
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn draw_updates_fill_gradient_chain_line() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
let layer = create_fill_gradient_chain_layer(&mut editor).await;
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let (gradient, transform) = get_gradient_from_chain(&mut editor).await;
|
||||||
|
|
||||||
|
// Gradient line is updated while existing stops are preserved
|
||||||
|
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
||||||
|
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn snap_simple_draw() {
|
async fn snap_simple_draw() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
@@ -2060,7 +2142,7 @@ mod test_gradient {
|
|||||||
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
|
||||||
editor.drag_tool(ToolType::Gradient, start.x, start.y, end.x, end.y, ModifierKeys::SHIFT).await;
|
editor.drag_tool(ToolType::Gradient, start.x, start.y, end.x, end.y, ModifierKeys::SHIFT).await;
|
||||||
|
|
||||||
let (gradient, transform) = get_gradient(&mut editor).await;
|
let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
|
||||||
|
|
||||||
assert!(transform.transform_point2(gradient.start).abs_diff_eq(start, 1e-10));
|
assert!(transform.transform_point2(gradient.start).abs_diff_eq(start, 1e-10));
|
||||||
|
|
||||||
@@ -2103,7 +2185,7 @@ mod test_gradient {
|
|||||||
|
|
||||||
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 2., 3., 24., 4., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
let (gradient, transform) = get_gradient(&mut editor).await;
|
let (gradient, transform) = get_gradient_from_fill(&mut editor).await;
|
||||||
|
|
||||||
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
||||||
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
||||||
@@ -2120,7 +2202,7 @@ mod test_gradient {
|
|||||||
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Get initial gradient state (should have 2 stops)
|
// Get initial gradient state (should have 2 stops)
|
||||||
let (initial_gradient, _) = get_gradient(&mut editor).await;
|
let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
||||||
|
|
||||||
editor.select_tool(ToolType::Gradient).await;
|
editor.select_tool(ToolType::Gradient).await;
|
||||||
@@ -2129,7 +2211,7 @@ mod test_gradient {
|
|||||||
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Check that a new stop has been added
|
// Check that a new stop has been added
|
||||||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len());
|
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len());
|
||||||
|
|
||||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||||
@@ -2165,7 +2247,7 @@ mod test_gradient {
|
|||||||
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Get the initial gradient state
|
// Get the initial gradient state
|
||||||
let (initial_gradient, transform) = get_gradient(&mut editor).await;
|
let (initial_gradient, transform) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
||||||
|
|
||||||
// Verify initial gradient endpoints in viewport space
|
// Verify initial gradient endpoints in viewport space
|
||||||
@@ -2195,7 +2277,7 @@ mod test_gradient {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Check the updated gradient
|
// Check the updated gradient
|
||||||
let (updated_gradient, transform) = get_gradient(&mut editor).await;
|
let (updated_gradient, transform) = get_gradient_from_fill(&mut editor).await;
|
||||||
|
|
||||||
// Verify the start point hasn't changed
|
// Verify the start point hasn't changed
|
||||||
let updated_start = transform.transform_point2(updated_gradient.start);
|
let updated_start = transform.transform_point2(updated_gradient.start);
|
||||||
@@ -2223,7 +2305,7 @@ mod test_gradient {
|
|||||||
editor.left_mousedown(25., 0., ModifierKeys::empty()).await;
|
editor.left_mousedown(25., 0., ModifierKeys::empty()).await;
|
||||||
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
editor.left_mouseup(25., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
let (initial_gradient, _) = get_gradient(&mut editor).await;
|
let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len());
|
assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len());
|
||||||
|
|
||||||
// Verify initial stop positions and colors
|
// Verify initial stop positions and colors
|
||||||
@@ -2262,7 +2344,7 @@ mod test_gradient {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops after dragging, found {}", updated_gradient.stops.len());
|
assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops after dragging, found {}", updated_gradient.stops.len());
|
||||||
|
|
||||||
// Verify updated stop positions and colors
|
// Verify updated stop positions and colors
|
||||||
@@ -2290,7 +2372,7 @@ mod test_gradient {
|
|||||||
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Get initial gradient state (should have 2 stops)
|
// Get initial gradient state (should have 2 stops)
|
||||||
let (initial_gradient, _) = get_gradient(&mut editor).await;
|
let (initial_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len());
|
||||||
|
|
||||||
editor.select_tool(ToolType::Gradient).await;
|
editor.select_tool(ToolType::Gradient).await;
|
||||||
@@ -2304,7 +2386,7 @@ mod test_gradient {
|
|||||||
editor.left_mousedown(75., 0., ModifierKeys::empty()).await;
|
editor.left_mousedown(75., 0., ModifierKeys::empty()).await;
|
||||||
editor.left_mouseup(75., 0., ModifierKeys::empty()).await;
|
editor.left_mouseup(75., 0., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
let (updated_gradient, _) = get_gradient(&mut editor).await;
|
let (updated_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len());
|
assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len());
|
||||||
|
|
||||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
let positions: Vec<f64> = updated_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||||
@@ -2330,7 +2412,7 @@ mod test_gradient {
|
|||||||
editor.press(Key::Delete, ModifierKeys::empty()).await;
|
editor.press(Key::Delete, ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Verify we now have 3 stops
|
// Verify we now have 3 stops
|
||||||
let (final_gradient, _) = get_gradient(&mut editor).await;
|
let (final_gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len());
|
assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len());
|
||||||
|
|
||||||
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|stop| stop.position).collect();
|
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|stop| stop.position).collect();
|
||||||
@@ -2374,15 +2456,13 @@ mod test_gradient {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn change_spread_method() {
|
async fn change_spread_method() {
|
||||||
use graphene_std::vector::style::GradientSpreadMethod;
|
|
||||||
|
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
editor.new_document().await;
|
editor.new_document().await;
|
||||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await;
|
editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
// Verify default spread method is Pad
|
// Verify default spread method is Pad
|
||||||
let (gradient, _) = get_gradient(&mut editor).await;
|
let (gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
|
||||||
|
|
||||||
// Update spread method to Repeat
|
// Update spread method to Repeat
|
||||||
@@ -2392,7 +2472,7 @@ mod test_gradient {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (gradient, _) = get_gradient(&mut editor).await;
|
let (gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
|
||||||
|
|
||||||
// Update spread method to Reflect
|
// Update spread method to Reflect
|
||||||
@@ -2402,12 +2482,45 @@ mod test_gradient {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (gradient, _) = get_gradient(&mut editor).await;
|
let (gradient, _) = get_gradient_from_fill(&mut editor).await;
|
||||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gradient_list_drag_endpoint() {
|
async fn change_spread_method_chain() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
let layer = create_fill_gradient_chain_layer(&mut editor).await;
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
editor.select_tool(ToolType::Gradient).await;
|
||||||
|
|
||||||
|
// Verify default spread method is Pad
|
||||||
|
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
|
||||||
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
|
||||||
|
|
||||||
|
// Update spread method to Repeat
|
||||||
|
editor
|
||||||
|
.handle_message(GradientToolMessage::UpdateOptions {
|
||||||
|
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
|
||||||
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
|
||||||
|
|
||||||
|
// Update spread method to Reflect
|
||||||
|
editor
|
||||||
|
.handle_message(GradientToolMessage::UpdateOptions {
|
||||||
|
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (gradient, _) = get_gradient_from_chain(&mut editor).await;
|
||||||
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gradient_list_layer_drag_endpoint() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
editor.new_document().await;
|
editor.new_document().await;
|
||||||
let layer = create_gradient_list_layer(&mut editor).await;
|
let layer = create_gradient_list_layer(&mut editor).await;
|
||||||
@@ -2479,7 +2592,7 @@ mod test_gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gradient_list_preserves_stops() {
|
async fn gradient_list_layer_preserves_stops() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
editor.new_document().await;
|
editor.new_document().await;
|
||||||
let layer = create_gradient_list_layer(&mut editor).await;
|
let layer = create_gradient_list_layer(&mut editor).await;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
|
||||||
use glam::{DAffine2, DVec2, UVec2};
|
use glam::{DAffine2, DVec2, UVec2};
|
||||||
use graph_craft::application_io::EditorPreferences;
|
use graph_craft::application_io::EditorPreferences;
|
||||||
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
||||||
use graphene_std::bounds::RenderBoundingBox;
|
use graphene_std::bounds::RenderBoundingBox;
|
||||||
@@ -15,7 +15,8 @@ use graphene_std::raster::{CPU, Raster};
|
|||||||
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
|
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
|
||||||
use graphene_std::transform::Footprint;
|
use graphene_std::transform::Footprint;
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::Vector;
|
||||||
use graphene_std::{ATTR_TRANSFORM, Context, Graphic};
|
use graphene_std::vector::style::Gradient;
|
||||||
|
use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration};
|
||||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -68,6 +69,10 @@ pub struct NodeGraphExecutor {
|
|||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// In-progress one-time pre-pass that converts legacy bounding-box-relative gradients to absolute space, if any.
|
/// In-progress one-time pre-pass that converts legacy bounding-box-relative gradients to absolute space, if any.
|
||||||
gradient_migration: Option<GradientMigration>,
|
gradient_migration: Option<GradientMigration>,
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Documents whose gradient migration pass already ran this session, so entries that failed to measure
|
||||||
|
/// (kept pending for a retry on the next open) don't re-run the pass on every render request.
|
||||||
|
gradient_migration_attempted: HashSet<DocumentId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -75,18 +80,20 @@ struct ExecutionContext {
|
|||||||
export_config: Option<ExportConfig>,
|
export_config: Option<ExportConfig>,
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Set when this execution is a gradient-migration measurement run for the given "Fill" node, whose evaluated geometry
|
/// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
|
||||||
/// is read back from the inspect result to size the gradient; such runs never touch the visible artwork.
|
/// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
|
||||||
measure_fill: Option<NodeId>,
|
/// 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, Gradient)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// State for the deferred legacy-gradient migration: a queue of root-network "Fill" nodes still holding bounding-box-relative
|
/// State for the deferred legacy-gradient migration: a queue of "Fill" nodes (each addressed by its enclosing network path)
|
||||||
/// gradients, measured one at a time by redirecting the document export to each so even hidden/orphaned branches evaluate.
|
/// whose decomposed gradient still needs its transform baked, each paired with its original relative gradient and measured
|
||||||
|
/// one at a time by redirecting the document export so even hidden/orphaned/nested branches evaluate.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct GradientMigration {
|
struct GradientMigration {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
remaining: VecDeque<NodeId>,
|
remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)>,
|
||||||
resolution: UVec2,
|
resolution: UVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
}
|
}
|
||||||
@@ -106,6 +113,7 @@ impl NodeGraphExecutor {
|
|||||||
current_execution_id: 0,
|
current_execution_id: 0,
|
||||||
previous_node_to_inspect: Vec::new(),
|
previous_node_to_inspect: Vec::new(),
|
||||||
gradient_migration: None,
|
gradient_migration: None,
|
||||||
|
gradient_migration_attempted: HashSet::new(),
|
||||||
};
|
};
|
||||||
(node_runtime, node_executor)
|
(node_runtime, node_executor)
|
||||||
}
|
}
|
||||||
@@ -342,6 +350,12 @@ impl NodeGraphExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
// A gradient migration belongs to one document; if the active document changed away from it, cancel so its stale in-flight response is ignored and revisiting restarts a fresh pass
|
||||||
|
if self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id != document_id) {
|
||||||
|
self.cancel_gradient_migration();
|
||||||
|
}
|
||||||
|
|
||||||
let results = self.runtime_io.receive().collect::<Vec<_>>();
|
let results = self.runtime_io.receive().collect::<Vec<_>>();
|
||||||
for response in results {
|
for response in results {
|
||||||
match response {
|
match response {
|
||||||
@@ -370,9 +384,9 @@ impl NodeGraphExecutor {
|
|||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
// Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork.
|
// Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork.
|
||||||
// Apply only to the active document's in-progress migration, dropping responses left over from a document switch.
|
// Apply only to the active document's in-progress migration, dropping responses left over from a document switch.
|
||||||
if let Some(fill_node_id) = execution_context.measure_fill {
|
if let Some(bake_target) = execution_context.measure_fill {
|
||||||
if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) {
|
if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) {
|
||||||
self.handle_gradient_measurement(document, document_id, fill_node_id, result.ok().and(inspect_result), responses);
|
self.handle_gradient_measurement(document, document_id, bake_target, result.ok().and(inspect_result), responses);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -463,20 +477,25 @@ impl NodeGraphExecutor {
|
|||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Kick off the one-time pre-pass converting legacy bounding-box gradients to absolute space, or no-op if already running.
|
/// Kick off the one-time pre-pass converting legacy bounding-box gradients to absolute space, or no-op if already running.
|
||||||
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) {
|
/// Returns false when the render should proceed normally instead: the pass already ran this session, so any entries still
|
||||||
|
/// pending are unmeasurable ones kept to retry on the next open.
|
||||||
|
pub(crate) fn drive_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, resolution: UVec2, scale: f64, responses: &mut VecDeque<Message>) -> bool {
|
||||||
|
if self.gradient_migration_attempted.contains(&document_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
match &self.gradient_migration {
|
match &self.gradient_migration {
|
||||||
// Already running for this document
|
// Already running for this document
|
||||||
Some(migration) if migration.document_id == document_id => return,
|
Some(migration) if migration.document_id == document_id => return true,
|
||||||
// A different document's migration is in progress; cancel it so this one can run (the other restarts when revisited)
|
// A different document's migration is in progress; cancel it so this one can run (the other restarts when revisited)
|
||||||
Some(_) => self.gradient_migration = None,
|
Some(_) => self.gradient_migration = None,
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining: VecDeque<NodeId> = graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).into_iter().collect();
|
// 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 Some(&first_fill) = remaining.front() else {
|
let remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
|
||||||
document.pending_gradient_migration = false;
|
let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
return false;
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.gradient_migration = Some(GradientMigration {
|
self.gradient_migration = Some(GradientMigration {
|
||||||
@@ -485,36 +504,39 @@ impl NodeGraphExecutor {
|
|||||||
resolution,
|
resolution,
|
||||||
scale,
|
scale,
|
||||||
});
|
});
|
||||||
self.dispatch_gradient_measurement(document, document_id, first_fill, resolution, scale, responses);
|
self.dispatch_gradient_measurement(document, document_id, first_network_path, first_fill, first_gradient, resolution, scale, responses);
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Run the graph with its export redirected to `fill_node_id` (so its branch evaluates even when hidden or orphaned) and
|
/// Run the graph with its export chain redirected down to the (possibly nested) Fill at `network_path`/`fill_node_id`
|
||||||
/// that node inspected, so its evaluated geometry returns in the execution response without touching the visible artwork.
|
/// (so its branch evaluates even when hidden or orphaned) and that node inspected, so its evaluated geometry returns
|
||||||
|
/// in the execution response without touching the visible artwork.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn dispatch_gradient_measurement(
|
fn dispatch_gradient_measurement(
|
||||||
&mut self,
|
&mut self,
|
||||||
document: &mut DocumentMessageHandler,
|
document: &mut DocumentMessageHandler,
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
|
network_path: Vec<NodeId>,
|
||||||
fill_node_id: NodeId,
|
fill_node_id: NodeId,
|
||||||
|
gradient: Gradient,
|
||||||
resolution: UVec2,
|
resolution: UVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) {
|
) {
|
||||||
let mut network = document.network_interface.document_network().clone();
|
let mut network = document.network_interface.document_network().clone();
|
||||||
|
|
||||||
// On this throwaway clone, un-hide just the Fill so it's measured as a real Fill rather than a passthrough.
|
// On this throwaway clone, redirect each level's export down to the Fill, un-hiding the Fill and its enclosing subnetworks so it's measured as a real Fill rather than a passthrough.
|
||||||
// But upstream generators keep their visibility, so a hidden one intentionally contributes no geometry.
|
// But upstream generators keep their visibility, so a hidden one intentionally contributes no geometry.
|
||||||
if let Some(node) = network.nodes.get_mut(&fill_node_id) {
|
let full_path: Vec<NodeId> = network_path.iter().copied().chain(std::iter::once(fill_node_id)).collect();
|
||||||
node.visible = true;
|
if !redirect_export_chain(&mut network, &full_path) {
|
||||||
}
|
// The fill was deleted or is otherwise unreachable, so drop its stale entry instead of retrying it on every open
|
||||||
|
log::warn!("Gradient migration: could not redirect the document network's export chain to fill node {fill_node_id:?}; dropping its pending gradient bake");
|
||||||
let Some(export) = network.exports.first_mut() else {
|
remove_pending_gradient_bake(document, &network_path, fill_node_id);
|
||||||
// No export to redirect through, so skip this Fill rather than leaving the migration stuck
|
|
||||||
log::warn!("Gradient migration: document network has no export to redirect");
|
|
||||||
self.advance_gradient_migration(document, document_id, responses);
|
self.advance_gradient_migration(document, document_id, responses);
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
*export = NodeInput::node(fill_node_id, 0);
|
|
||||||
|
|
||||||
let resources = document.resources.registry.clone();
|
let resources = document.resources.registry.clone();
|
||||||
if self
|
if self
|
||||||
@@ -522,7 +544,7 @@ impl NodeGraphExecutor {
|
|||||||
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
.send(GraphRuntimeRequest::GraphUpdate(GraphUpdate {
|
||||||
network,
|
network,
|
||||||
resources,
|
resources,
|
||||||
node_to_inspect: vec![fill_node_id],
|
node_to_inspect: full_path.clone(),
|
||||||
}))
|
}))
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@@ -533,7 +555,7 @@ impl NodeGraphExecutor {
|
|||||||
|
|
||||||
// Force the next normal render to recompile and re-send the real, non-redirected network
|
// Force the next normal render to recompile and re-send the real, non-redirected network
|
||||||
self.node_graph_hash = 0;
|
self.node_graph_hash = 0;
|
||||||
self.previous_node_to_inspect = vec![fill_node_id];
|
self.previous_node_to_inspect = full_path;
|
||||||
|
|
||||||
let viewport = Footprint {
|
let viewport = Footprint {
|
||||||
transform: document.metadata().document_to_viewport,
|
transform: document.metadata().document_to_viewport,
|
||||||
@@ -556,26 +578,50 @@ impl NodeGraphExecutor {
|
|||||||
ExecutionContext {
|
ExecutionContext {
|
||||||
export_config: None,
|
export_config: None,
|
||||||
document_id,
|
document_id,
|
||||||
measure_fill: Some(fill_node_id),
|
measure_fill: Some((network_path, fill_node_id, gradient)),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Convert the just-measured fill's legacy gradients to absolute space using its evaluated geometry, then advance the queue.
|
/// Bake the just-measured fill's gradient transform into absolute space using its evaluated geometry, then advance the queue.
|
||||||
fn handle_gradient_measurement(
|
fn handle_gradient_measurement(
|
||||||
&mut self,
|
&mut self,
|
||||||
document: &mut DocumentMessageHandler,
|
document: &mut DocumentMessageHandler,
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
fill_node_id: NodeId,
|
bake_target: (Vec<NodeId>, NodeId, Gradient),
|
||||||
inspect_result: Option<InspectResult>,
|
inspect_result: Option<InspectResult>,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) {
|
) {
|
||||||
let measured = inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data));
|
let (network_path, fill_node_id, gradient) = bake_target;
|
||||||
|
|
||||||
match measured {
|
// Ignore a stale response whose fill is no longer the one being measured (a re-dispatch after a document switch can leave two measurements in flight for the same entry)
|
||||||
Some((bounding_box, item_transform)) => graph_modification_utils::migrate_fill_node_gradients_to_absolute(fill_node_id, &mut document.network_interface, bounding_box, item_transform),
|
let is_current = self
|
||||||
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving it in legacy space"),
|
.gradient_migration
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|migration| migration.remaining.front())
|
||||||
|
.is_some_and(|(front_path, front_fill, _)| *front_fill == fill_node_id && *front_path == network_path);
|
||||||
|
if !is_current {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match inspect_result.and_then(|mut result| result.take_data()).and_then(|data| measure_fill_geometry(&data)) {
|
||||||
|
Some((bounding_box, item_transform)) => {
|
||||||
|
// Skip the bake if the user already placed this gradient themselves, but drop the now-superseded entry either way
|
||||||
|
if fill_transform_unbaked(document, &network_path, fill_node_id) {
|
||||||
|
let absolute_gradient = gradient.to_absolute(bounding_box, item_transform);
|
||||||
|
let gradient_transform = absolute_gradient.transform * absolute_gradient.to_transform();
|
||||||
|
let input = InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&input, NodeInput::value(TaggedValue::OptionalDAffine2(Some(gradient_transform)), false), &network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The transform is settled, so its entry no longer needs to persist for a retry on the next open
|
||||||
|
remove_pending_gradient_bake(document, &network_path, fill_node_id);
|
||||||
|
}
|
||||||
|
// Leave the entry pending so the next open retries it
|
||||||
|
None => log::warn!("Gradient migration could not measure geometry for fill node {fill_node_id:?}; leaving its transform unbaked and pending a retry on the next open"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.advance_gradient_migration(document, document_id, responses);
|
self.advance_gradient_migration(document, document_id, responses);
|
||||||
@@ -584,23 +630,30 @@ impl NodeGraphExecutor {
|
|||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Move to the next queued fill, or finish the migration and trigger a normal render once the queue is empty.
|
/// Move to the next queued fill, or finish the migration and trigger a normal render once the queue is empty.
|
||||||
fn advance_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
fn advance_gradient_migration(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
||||||
let next_fill = self.gradient_migration.as_mut().and_then(|migration| {
|
let next = self.gradient_migration.as_mut().and_then(|migration| {
|
||||||
migration.remaining.pop_front();
|
migration.remaining.pop_front();
|
||||||
migration.remaining.front().copied()
|
migration.remaining.front().cloned()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(next_fill) = next_fill {
|
if let Some((next_network_path, next_fill, next_gradient)) = next {
|
||||||
let (resolution, scale) = self.gradient_migration.as_ref().map(|migration| (migration.resolution, migration.scale)).unwrap_or((UVec2::ONE, 1.));
|
let (resolution, scale) = self.gradient_migration.as_ref().map(|migration| (migration.resolution, migration.scale)).unwrap_or((UVec2::ONE, 1.));
|
||||||
self.dispatch_gradient_measurement(document, document_id, next_fill, resolution, scale, responses);
|
self.dispatch_gradient_measurement(document, document_id, next_network_path, next_fill, next_gradient, resolution, scale, responses);
|
||||||
} else {
|
} else {
|
||||||
self.gradient_migration = None;
|
// Entries still pending failed to measure; they stay persisted for the next open while this marker stops re-runs this session
|
||||||
self.previous_node_to_inspect = Vec::new();
|
self.cancel_gradient_migration();
|
||||||
self.node_graph_hash = 0;
|
self.gradient_migration_attempted.insert(document_id);
|
||||||
document.pending_gradient_migration = false;
|
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Tear down the in-progress migration and clear the stale inspect target and hash so the next normal render recompiles the real network.
|
||||||
|
fn cancel_gradient_migration(&mut self) {
|
||||||
|
self.gradient_migration = None;
|
||||||
|
self.previous_node_to_inspect = Vec::new();
|
||||||
|
self.node_graph_hash = 0;
|
||||||
|
}
|
||||||
|
|
||||||
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
let TaggedValue::RenderOutput(render_output) = node_graph_output else {
|
let TaggedValue::RenderOutput(render_output) = node_graph_output else {
|
||||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||||
@@ -768,6 +821,53 @@ impl NodeGraphExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Whether the fill node's transform input is still the unset `OptionalDAffine2(None)` placeholder that the migration leaves
|
||||||
|
/// behind, meaning its gradient placement has not yet been baked (or set by the user), so a measured bake may safely be written.
|
||||||
|
fn fill_transform_unbaked(document: &DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) -> bool {
|
||||||
|
let Some(network) = document.network_interface.document_network().nested_network(network_path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(node) = network.nodes.get(&fill_node_id) else { return false };
|
||||||
|
matches!(
|
||||||
|
node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX).and_then(|input| input.as_value()),
|
||||||
|
Some(TaggedValue::OptionalDAffine2(None))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Remove a fill's entry from the document's persisted pending-bake list, either because its bake landed or because it is stale.
|
||||||
|
fn remove_pending_gradient_bake(document: &mut DocumentMessageHandler, network_path: &[NodeId], fill_node_id: NodeId) {
|
||||||
|
if let Some(index) = document
|
||||||
|
.pending_gradient_bbox_bake
|
||||||
|
.iter()
|
||||||
|
.position(|(path, node_id, _)| *node_id == fill_node_id && path == network_path)
|
||||||
|
{
|
||||||
|
document.pending_gradient_bbox_bake.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this document upgrade code
|
||||||
|
/// Redirect the primary export at each level of `network` down through the subnetwork nodes named by `full_path`,
|
||||||
|
/// un-hiding each so the innermost node's branch evaluates. Returns false if any step is missing or not a subnetwork.
|
||||||
|
fn redirect_export_chain(network: &mut NodeNetwork, full_path: &[NodeId]) -> bool {
|
||||||
|
let Some((node_id, deeper_path)) = full_path.split_first() else { return true };
|
||||||
|
|
||||||
|
let Some(export) = network.exports.first_mut() else { return false };
|
||||||
|
*export = NodeInput::node(*node_id, 0);
|
||||||
|
|
||||||
|
let Some(node) = network.nodes.get_mut(node_id) else { return false };
|
||||||
|
node.visible = true;
|
||||||
|
|
||||||
|
if deeper_path.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match &mut node.implementation {
|
||||||
|
DocumentNodeImplementation::Network(nested) => redirect_export_chain(nested, deeper_path),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
/// Turn a measured fill node's evaluated geometry into a `[0,1]² -> bounding box` affine plus the geometry's item transform.
|
/// Turn a measured fill node's evaluated geometry into a `[0,1]² -> bounding box` affine plus the geometry's item transform.
|
||||||
fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> {
|
fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2, DAffine2)> {
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ tagged_value! {
|
|||||||
DVec2(DVec2),
|
DVec2(DVec2),
|
||||||
#[serde(alias = "Affine2")]
|
#[serde(alias = "Affine2")]
|
||||||
DAffine2(DAffine2),
|
DAffine2(DAffine2),
|
||||||
|
OptionalDAffine2(Option<DAffine2>),
|
||||||
FillGradient(Gradient),
|
FillGradient(Gradient),
|
||||||
Font(Font),
|
Font(Font),
|
||||||
Footprint(Footprint),
|
Footprint(Footprint),
|
||||||
@@ -583,6 +584,8 @@ impl TaggedValue {
|
|||||||
// `Color` (not in a `List`) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants
|
// `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(|color| TaggedValue::Color(Some(color)))?,
|
() if ty == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
||||||
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
||||||
|
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
||||||
|
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
||||||
() if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
() if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||||
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
||||||
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
|
||||||
|
|||||||
@@ -673,6 +673,21 @@ impl ItemAttributeValues {
|
|||||||
}
|
}
|
||||||
self.0.push((to_key, value));
|
self.0.push((to_key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clones the attribute with `key` from `source`, replacing any existing attribute with the same key.
|
||||||
|
pub fn insert_cloned_from(&mut self, source: &Self, key: &str) {
|
||||||
|
let Some((_, value)) = source.0.iter().find(|(existing_key, _)| existing_key == key) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = value.clone();
|
||||||
|
|
||||||
|
if let Some((_, existing_value)) = self.0.iter_mut().find(|(existing_key, _)| existing_key == key) {
|
||||||
|
*existing_value = value;
|
||||||
|
} else {
|
||||||
|
self.0.push((key.to_string(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========
|
// ==========
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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, Item, List};
|
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List};
|
||||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
use core_types::ops::{FromAnchorPosition, ListConvert};
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -215,29 +215,34 @@ pub fn color_to_graphic_list(color: Option<Color>) -> Option<List<Graphic>> {
|
|||||||
color.as_ref().map(|color| List::new_from_element((*color).into()))
|
color.as_ref().map(|color| List::new_from_element((*color).into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the paint graphics stored under attribute for a vector item, normalizing any graphic list type to `List<Graphic>`.
|
/// Whether a normalized paint graphic list actually carries renderable paint.
|
||||||
|
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
|
||||||
|
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
|
||||||
|
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List<Graphic>` form.
|
||||||
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
|
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
|
||||||
list.attribute::<List<Graphic>>(attribute, index)
|
list.attribute::<List<Graphic>>(attribute, index)
|
||||||
.map(Cow::Borrowed)
|
.map(Cow::Borrowed)
|
||||||
.or_else(|| list.attribute::<List<Color>>(attribute, index).map(|c| Cow::Owned(c.clone().into_graphic_list())))
|
|
||||||
.or_else(|| list.attribute::<List<GradientStops>>(attribute, index).map(|g| Cow::Owned(g.clone().into_graphic_list())))
|
|
||||||
.or_else(|| list.attribute::<List<Vector>>(attribute, index).map(|v| Cow::Owned(v.clone().into_graphic_list())))
|
|
||||||
.or_else(|| list.attribute::<List<Raster<CPU>>>(attribute, index).map(|r| Cow::Owned(r.clone().into_graphic_list())))
|
|
||||||
.or_else(|| list.attribute::<List<Raster<GPU>>>(attribute, index).map(|r| Cow::Owned(r.clone().into_graphic_list())))
|
|
||||||
// Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it.
|
// Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it.
|
||||||
.filter(|graphic_list| graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()))
|
.filter(|graphic_list| is_paint_present(graphic_list))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the item carries a non-blank paint attribute in any representation (`Graphic`, `Color`,
|
/// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute,
|
||||||
/// `GradientStops`, `Vector`, or raster), checked by borrowing without cloning the renderable list.
|
/// checked by borrowing without cloning the renderable list.
|
||||||
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
|
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
|
||||||
list.attribute::<List<Graphic>>(attribute, index)
|
list.attribute::<List<Graphic>>(attribute, index).is_some_and(is_paint_present)
|
||||||
.is_some_and(|graphics| graphics.element(0).is_some_and(|graphic| !graphic.is_empty()))
|
}
|
||||||
|| list.attribute::<List<Color>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|
|
||||||
|| list.attribute::<List<GradientStops>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|
/// Stores a paint attribute in its canonical `List<Graphic>` form, the only representation paint readers accept.
|
||||||
|| list.attribute::<List<Vector>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
|
||||||
|| list.attribute::<List<Raster<CPU>>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|
attributes.insert(key, paint.into_graphic_list());
|
||||||
|| list.attribute::<List<Raster<GPU>>>(attribute, index).is_some_and(|paint_list| !paint_list.is_empty())
|
}
|
||||||
|
|
||||||
|
/// Stores a paint attribute at a list index in its canonical `List<Graphic>` form, the only representation paint readers accept.
|
||||||
|
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
|
||||||
|
list.set_attribute(key, index, paint.into_graphic_list());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the fill paint graphics for a vector item, falling back to the legacy
|
/// Look up the fill paint graphics for a vector item, falling back to the legacy
|
||||||
@@ -320,6 +325,36 @@ pub fn is_stroke_fully_transparent_at(list: &List<Vector>, index: usize) -> bool
|
|||||||
color.a() == 0.
|
color.a() == 0.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
|
||||||
|
/// canonical `List<Graphic>` fill and stroke attributes.
|
||||||
|
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
|
||||||
|
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
|
||||||
|
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
|
*item_transform = transform * *item_transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
|
||||||
|
for graphic in graphics.iter_element_values_mut() {
|
||||||
|
match graphic {
|
||||||
|
Graphic::Graphic(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::Vector(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::Gradient(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::Text(list) => bake_list_transform(list, transform),
|
||||||
|
Graphic::Color(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for paint_key in [ATTR_FILL, ATTR_STROKE] {
|
||||||
|
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
|
||||||
|
bake_graphic_paint_transform(graphics, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
|
/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
|
||||||
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
|
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
|
||||||
pub trait TryFromGraphic: Clone + Sized {
|
pub trait TryFromGraphic: Clone + Sized {
|
||||||
@@ -357,7 +392,7 @@ impl TryFromGraphic for String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
||||||
pub trait IntoGraphicList {
|
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
|
||||||
fn into_graphic_list(self) -> List<Graphic>;
|
fn into_graphic_list(self) -> List<Graphic>;
|
||||||
|
|
||||||
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
|
/// Deeply flattens any content of type `T` within a `List<Graphic>`, discarding all other content, and returning a flat `List<T>`.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use dyn_any::DynAny;
|
|||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphene_hash::CacheHashWrapper;
|
use graphene_hash::CacheHashWrapper;
|
||||||
use graphene_resource::Resource;
|
use graphene_resource::Resource;
|
||||||
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, stroke_graphic_list_at};
|
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, stroke_graphic_list_at};
|
||||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
|
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
|
||||||
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
||||||
use graphic_types::vector_types::subpath::Subpath;
|
use graphic_types::vector_types::subpath::Subpath;
|
||||||
@@ -1128,11 +1128,12 @@ impl Render for List<Vector> {
|
|||||||
|
|
||||||
let mut cloned_vector = vector.clone();
|
let mut cloned_vector = vector.clone();
|
||||||
cloned_vector.style.clear_stroke();
|
cloned_vector.style.clear_stroke();
|
||||||
cloned_vector.style.set_fill(Fill::solid(Color::BLACK));
|
|
||||||
|
|
||||||
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
|
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
|
||||||
// The wrapping SVG group (above) handles the user-set opacity.
|
// The wrapping SVG group (above) handles the user-set opacity.
|
||||||
let vector_item = List::new_from_item(Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform));
|
let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform);
|
||||||
|
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
|
let vector_item = List::new_from_item(mask_item);
|
||||||
|
|
||||||
(id, mask_type, vector_item)
|
(id, mask_type, vector_item)
|
||||||
});
|
});
|
||||||
@@ -1197,7 +1198,7 @@ impl Render for List<Vector> {
|
|||||||
.style
|
.style
|
||||||
.stroke()
|
.stroke()
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
if stroke_graphic_list.as_ref().and_then(|l| l.element(0)).is_some() {
|
if stroke_graphic_list.as_deref().is_some_and(is_paint_present) {
|
||||||
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
|
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -1474,11 +1475,12 @@ impl Render for List<Vector> {
|
|||||||
if use_layer {
|
if use_layer {
|
||||||
let mut cloned_element = element.clone();
|
let mut cloned_element = element.clone();
|
||||||
cloned_element.style.clear_stroke();
|
cloned_element.style.clear_stroke();
|
||||||
cloned_element.style.set_fill(Fill::solid(Color::BLACK));
|
|
||||||
|
|
||||||
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
|
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
|
||||||
// The outer opacity/blend layer (above) handles the user-set opacity.
|
// The outer opacity/blend layer (above) handles the user-set opacity.
|
||||||
let vector_list = List::new_from_item(Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform));
|
let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform);
|
||||||
|
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
|
let vector_list = List::new_from_item(mask_item);
|
||||||
|
|
||||||
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
|
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
|
||||||
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
|
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
|
||||||
|
|||||||
@@ -458,6 +458,15 @@ impl GradientStops {
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||||
|
let stops = self.iter().zip(other.iter()).map(|(a, b)| {
|
||||||
|
let position = a.position + (b.position - a.position) * time;
|
||||||
|
let color = a.color.lerp(&b.color, time as f32);
|
||||||
|
GradientStop { position, midpoint: 0.5, color }
|
||||||
|
});
|
||||||
|
GradientStops::new(stops)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -601,30 +610,6 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
|
||||||
let start = self.start + (other.start - self.start) * time;
|
|
||||||
let end = self.end + (other.end - self.end) * time;
|
|
||||||
let stops = self.stops.iter().zip(other.stops.iter()).map(|(a, b)| {
|
|
||||||
let position = a.position + (b.position - a.position) * time;
|
|
||||||
let color = a.color.lerp(&b.color, time as f32);
|
|
||||||
GradientStop { position, midpoint: 0.5, color }
|
|
||||||
});
|
|
||||||
let stops = GradientStops::new(stops);
|
|
||||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
|
||||||
let spread_method = if time < 0.5 { self.spread_method } else { other.spread_method };
|
|
||||||
|
|
||||||
Self {
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
stops,
|
|
||||||
gradient_type,
|
|
||||||
spread_method,
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
absolute: self.absolute,
|
|
||||||
transform: if time < 0.5 { self.transform } else { other.transform },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a stop into the gradient, the index if successful
|
/// Insert a stop into the gradient, the index if successful
|
||||||
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
|
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
|
||||||
// Transform the start and end positions to the same coordinate space as the mouse.
|
// Transform the start and end positions to the same coordinate space as the mouse.
|
||||||
@@ -663,6 +648,54 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
||||||
|
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
||||||
|
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
||||||
|
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
|
||||||
|
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
|
||||||
|
let old_x_length = old_x.length();
|
||||||
|
if old_x_length < 1e-9 {
|
||||||
|
return DVec2::new(-new_x.y, new_x.x);
|
||||||
|
}
|
||||||
|
let ex_old = old_x / old_x_length;
|
||||||
|
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
|
||||||
|
|
||||||
|
let new_x_length = new_x.length();
|
||||||
|
if new_x_length < 1e-9 {
|
||||||
|
return DVec2::ZERO;
|
||||||
|
}
|
||||||
|
let ex_new = new_x / new_x_length;
|
||||||
|
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
|
||||||
|
|
||||||
|
let parallel = old_y.dot(ex_old);
|
||||||
|
let perpendicular = old_y.dot(ey_old);
|
||||||
|
let scale = new_x_length / old_x_length;
|
||||||
|
|
||||||
|
scale * (parallel * ex_new + perpendicular * ey_new)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a new affine that maps canonical (0,0) -> (1,0) to (new_start, new_end), preserving the y-axis
|
||||||
|
/// shape of `old` proportionally to the x-axis length change.
|
||||||
|
pub fn build_transform_with_y_preservation(old: DAffine2, new_start: DVec2, new_end: DVec2) -> DAffine2 {
|
||||||
|
let new_x_axis = new_end - new_start;
|
||||||
|
let preserved_y_axis = scale_y_axis_to_match_new_x(old.matrix2.x_axis, old.matrix2.y_axis, new_x_axis);
|
||||||
|
DAffine2 {
|
||||||
|
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
|
||||||
|
translation: new_start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the default transform for a gradient not yet given one: a horizontal gradient spanning the
|
||||||
|
/// bounding box's width, running through its vertical middle.
|
||||||
|
pub fn initial_gradient_transform_for_bounding_box(bounds: [DVec2; 2]) -> DAffine2 {
|
||||||
|
let [min, max] = bounds;
|
||||||
|
let x_axis = DVec2::new(max.x - min.x, 0.);
|
||||||
|
DAffine2 {
|
||||||
|
matrix2: glam::DMat2::from_cols(x_axis, x_axis.perp()),
|
||||||
|
translation: DVec2::new(min.x, (min.y + max.y) / 2.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientStops, D::Error> {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|||||||
@@ -66,30 +66,6 @@ impl Fill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
|
||||||
let transparent = Self::solid(Color::TRANSPARENT);
|
|
||||||
let a = if *self == Self::None && *other != Self::None { &transparent } else { self };
|
|
||||||
let b = if *other == Self::None && *self != Self::None { &transparent } else { other };
|
|
||||||
|
|
||||||
match (a, b) {
|
|
||||||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
|
||||||
(Self::Solid(a), Self::Gradient(b)) => {
|
|
||||||
let mut solid_to_gradient = b.clone();
|
|
||||||
solid_to_gradient.stops.color.iter_mut().for_each(|color| *color = *a);
|
|
||||||
let a = &solid_to_gradient;
|
|
||||||
Self::Gradient(a.lerp(b, time))
|
|
||||||
}
|
|
||||||
(Self::Gradient(a), Self::Solid(b)) => {
|
|
||||||
let mut gradient_to_solid = a.clone();
|
|
||||||
gradient_to_solid.stops.color.iter_mut().for_each(|color| *color = *b);
|
|
||||||
let b = &gradient_to_solid;
|
|
||||||
Self::Gradient(a.lerp(b, time))
|
|
||||||
}
|
|
||||||
(Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)),
|
|
||||||
(Self::None, _) | (_, Self::None) => Self::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract a gradient from the fill
|
/// Extract a gradient from the fill
|
||||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||||
match self {
|
match self {
|
||||||
@@ -389,7 +365,8 @@ fn daffine2_identity() -> DAffine2 {
|
|||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
/// Stroke color
|
/// Deprecated, use `ATTR_STROKE` instead.
|
||||||
|
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||||
pub color: Option<Color>,
|
pub color: Option<Color>,
|
||||||
/// Line thickness
|
/// Line thickness
|
||||||
pub weight: f64,
|
pub weight: f64,
|
||||||
@@ -632,30 +609,6 @@ impl PathStyle {
|
|||||||
Self { stroke, fill }
|
Self { stroke, fill }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
fill: self.fill.lerp(&other.fill, time),
|
|
||||||
stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) {
|
|
||||||
(Some(a), Some(b)) => Some(a.lerp(b, time)),
|
|
||||||
(Some(a), None) => {
|
|
||||||
if time < 0.5 {
|
|
||||||
Some(a.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(None, Some(b)) => {
|
|
||||||
if time < 0.5 {
|
|
||||||
Some(b.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(None, None) => None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current path's [Fill].
|
/// Get the current path's [Fill].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
@@ -690,25 +643,6 @@ impl PathStyle {
|
|||||||
self.stroke.clone()
|
self.stroke.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the path's [Fill] with a provided one.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
|
||||||
/// # use core_types::Color;
|
|
||||||
/// let mut style = PathStyle::default();
|
|
||||||
///
|
|
||||||
/// assert_eq!(*style.fill(), Fill::None);
|
|
||||||
///
|
|
||||||
/// let fill = Fill::solid(Color::RED);
|
|
||||||
/// style.set_fill(fill.clone());
|
|
||||||
///
|
|
||||||
/// assert_eq!(*style.fill(), fill);
|
|
||||||
/// ```
|
|
||||||
pub fn set_fill(&mut self, fill: Fill) {
|
|
||||||
self.fill = fill;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||||
if let Some(stroke) = &mut self.stroke {
|
if let Some(stroke) = &mut self.stroke {
|
||||||
stroke.transform = transform;
|
stroke.transform = transform;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use vector_types::GradientStops;
|
|||||||
|
|
||||||
/// 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(""))]
|
||||||
pub async fn create_artboard<T: IntoGraphicList + 'n>(
|
pub async fn create_artboard<T: IntoGraphicList>(
|
||||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||||
/// Graphics to include within the artboard.
|
/// Graphics to include within the artboard.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
|
|||||||
/// 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[]`.
|
||||||
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
|
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
pub async fn to_graphic<T: IntoGraphicList + 'n>(
|
pub async fn to_graphic<T: IntoGraphicList>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Graphic>,
|
List<Graphic>,
|
||||||
@@ -620,7 +620,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
|
|||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
|
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
|
||||||
#[node_macro::node(category("Vector"))]
|
#[node_macro::node(category("Vector"))]
|
||||||
pub async fn flatten_vector<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
||||||
let graphic_list = content.into_graphic_list();
|
let graphic_list = content.into_graphic_list();
|
||||||
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
|
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
|
||||||
|
|
||||||
@@ -653,25 +653,25 @@ pub async fn flatten_vector<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx,
|
|||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
|
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
|
||||||
#[node_macro::node(category("Raster"))]
|
#[node_macro::node(category("Raster"))]
|
||||||
pub async fn flatten_raster<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
|
pub async fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
|
||||||
content.into_flattened_list()
|
content.into_flattened_list()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
|
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
|
||||||
#[node_macro::node(category("General"))]
|
#[node_macro::node(category("General"))]
|
||||||
pub async fn flatten_color<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
|
pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
|
||||||
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 `GradientStops[]` 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 + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
|
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
|
||||||
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"))]
|
||||||
fn colors_to_gradient<T: IntoGraphicList + 'n + Send + Clone>(_: 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) -> List<GradientStops> {
|
||||||
let colors = colors.into_flattened_list::<Color>();
|
let colors = colors.into_flattened_list::<Color>();
|
||||||
let total_colors = colors.len();
|
let total_colors = colors.len();
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
use core_types::list::{Item, List};
|
use core_types::list::{ATTR_FILL, Item, List};
|
||||||
use core_types::uuid::NodeId;
|
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, BlendMode, Color,
|
||||||
Ctx,
|
Ctx,
|
||||||
};
|
};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::vector_types::gradient::{Gradient, GradientSpreadMethod, GradientType};
|
use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute};
|
||||||
|
use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType};
|
||||||
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
|
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
|
||||||
use graphic_types::vector_types::vector::PointId;
|
use graphic_types::vector_types::vector::PointId;
|
||||||
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
use graphic_types::vector_types::vector::style::Fill;
|
|
||||||
use graphic_types::{Graphic, Vector};
|
use graphic_types::{Graphic, Vector};
|
||||||
use linesweeper::topology::Topology;
|
use linesweeper::topology::Topology;
|
||||||
use linesweeper::{BinaryOp, FillRule, binary_op};
|
use linesweeper::{BinaryOp, FillRule, binary_op};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
|
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
|
||||||
pub use vector_types::vector::misc::BooleanOperation;
|
pub use vector_types::vector::misc::BooleanOperation;
|
||||||
|
use vector_types::vector::style::Fill;
|
||||||
|
|
||||||
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
|
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
|
||||||
// TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
|
// TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
|
||||||
@@ -23,7 +24,7 @@ pub use vector_types::vector::misc::BooleanOperation;
|
|||||||
|
|
||||||
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
|
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
|
||||||
#[node_macro::node(category("Vector: Modifier"), memoize)]
|
#[node_macro::node(category("Vector: Modifier"), memoize)]
|
||||||
async fn boolean_operation<I: graphic_types::IntoGraphicList + 'n + Send + Clone>(
|
async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
|
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
|
||||||
#[implementations(List<Graphic>, List<Vector>)]
|
#[implementations(List<Graphic>, List<Vector>)]
|
||||||
@@ -143,12 +144,15 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
|
|||||||
let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
// The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own
|
// The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own
|
||||||
attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY);
|
attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY);
|
||||||
|
|
||||||
|
bake_paint_transforms(&mut attributes, copy_from_transform);
|
||||||
|
|
||||||
let copy_from = vector.element(index).unwrap();
|
let copy_from = vector.element(index).unwrap();
|
||||||
let mut element = Vector {
|
let mut element = Vector {
|
||||||
style: copy_from.style.clone(),
|
style: copy_from.style.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
// An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points
|
// Legacy Fill fallback: An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points
|
||||||
if let Fill::Gradient(gradient) = element.style.fill_mut()
|
if let Fill::Gradient(gradient) = element.style.fill_mut()
|
||||||
&& gradient.absolute
|
&& gradient.absolute
|
||||||
{
|
{
|
||||||
@@ -204,15 +208,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
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 mut element = Vector::from_subpath(subpath);
|
let element = Vector::from_subpath(subpath);
|
||||||
element.style.set_fill(Fill::Solid(Color::BLACK));
|
|
||||||
|
|
||||||
Item::new_from_element(element)
|
let mut item = Item::new_from_element(element)
|
||||||
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
||||||
.with_attribute(ATTR_OPACITY, opacity)
|
.with_attribute(ATTR_OPACITY, opacity)
|
||||||
.with_attribute(ATTR_OPACITY_FILL, fill)
|
.with_attribute(ATTR_OPACITY_FILL, fill)
|
||||||
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
||||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer)
|
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
||||||
|
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
|
item
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the parent graphic's transform to each raster element, preserving each item's layer
|
// Apply the parent graphic's transform to each raster element, preserving each item's layer
|
||||||
@@ -236,15 +241,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
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 mut element = Vector::from_subpath(subpath);
|
let element = Vector::from_subpath(subpath);
|
||||||
element.style.set_fill(Fill::Solid(Color::BLACK));
|
|
||||||
|
|
||||||
Item::new_from_element(element)
|
let mut item = Item::new_from_element(element)
|
||||||
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
.with_attribute(ATTR_BLEND_MODE, blend_mode)
|
||||||
.with_attribute(ATTR_OPACITY, opacity)
|
.with_attribute(ATTR_OPACITY, opacity)
|
||||||
.with_attribute(ATTR_OPACITY_FILL, fill)
|
.with_attribute(ATTR_OPACITY_FILL, fill)
|
||||||
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
.with_attribute(ATTR_CLIPPING_MASK, clip)
|
||||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer)
|
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer);
|
||||||
|
set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
|
||||||
|
item
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the parent graphic's transform to each raster element, preserving each item's layer
|
// Apply the parent graphic's transform to each raster element, preserving each item's layer
|
||||||
@@ -278,9 +284,10 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
Graphic::Color(color) => color
|
Graphic::Color(color) => color
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
let (color, attributes) = row.into_parts();
|
let (color, mut attributes) = row.into_parts();
|
||||||
|
set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color));
|
||||||
|
|
||||||
let mut element = Vector::default();
|
let mut element = Vector::default();
|
||||||
element.style.set_fill(Fill::Solid(color));
|
|
||||||
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||||
|
|
||||||
Item::from_parts(element, attributes)
|
Item::from_parts(element, attributes)
|
||||||
@@ -289,19 +296,21 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
Graphic::Gradient(gradient) => gradient
|
Graphic::Gradient(gradient) => gradient
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
let (stops, attributes) = row.into_parts();
|
let (stops, mut attributes) = row.into_parts();
|
||||||
|
|
||||||
|
let mut gradient_paint = List::new_from_element(stops);
|
||||||
|
if let Some(transform) = attributes.remove::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
|
gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform);
|
||||||
|
}
|
||||||
|
if let Some(gradient_type) = attributes.remove::<GradientType>(ATTR_GRADIENT_TYPE) {
|
||||||
|
gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type);
|
||||||
|
}
|
||||||
|
if let Some(spread_method) = attributes.remove::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
|
||||||
|
gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method);
|
||||||
|
}
|
||||||
|
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
|
||||||
|
|
||||||
let mut element = Vector::default();
|
let mut element = Vector::default();
|
||||||
// Convert the gradient's transform to absolute endpoints, matching `From<List<GradientStops>> for Fill`
|
|
||||||
let transform = attributes.get::<DAffine2>(ATTR_TRANSFORM).cloned().unwrap_or_default();
|
|
||||||
element.style.set_fill(Fill::Gradient(Gradient {
|
|
||||||
stops,
|
|
||||||
gradient_type: attributes.get::<GradientType>(ATTR_GRADIENT_TYPE).cloned().unwrap_or_default(),
|
|
||||||
spread_method: attributes.get::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).cloned().unwrap_or_default(),
|
|
||||||
start: transform.transform_point2(DVec2::ZERO),
|
|
||||||
end: transform.transform_point2(DVec2::X),
|
|
||||||
absolute: true,
|
|
||||||
transform: DAffine2::IDENTITY,
|
|
||||||
}));
|
|
||||||
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||||
|
|
||||||
Item::from_parts(element, attributes)
|
Item::from_parts(element, attributes)
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ use core::f64::consts::{PI, TAU};
|
|||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
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::list::{ATTR_FILL, ATTR_STROKE, Item, List, ListDyn};
|
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
|
||||||
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::uuid::NodeId;
|
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_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll,
|
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, CloneVarArgs,
|
||||||
OwnedContextImpl,
|
Color, Context, Ctx, ExtractAll, OwnedContextImpl,
|
||||||
};
|
};
|
||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
|
use graphic_types::graphic::{bake_paint_transforms, fill_graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, set_paint_attribute_at, stroke_graphic_list_at};
|
||||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
use graphic_types::{Graphic, IntoGraphicList};
|
use graphic_types::{Graphic, IntoGraphicList};
|
||||||
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
|
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
|
||||||
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
|
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
|
||||||
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
||||||
use vector_types::vector::PointDomain;
|
use vector_types::vector::PointDomain;
|
||||||
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
|
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
|
||||||
@@ -29,14 +31,17 @@ use vector_types::vector::misc::{
|
|||||||
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
||||||
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
||||||
};
|
};
|
||||||
use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||||
|
use vector_types::{GradientSpreadMethod, GradientType};
|
||||||
|
|
||||||
/// Implemented for types that contain vector items reachable via mutable access.
|
/// Implemented for types that contain vector items reachable via mutable access.
|
||||||
/// Used for the fill and stroke nodes so they can apply to either `List<Graphic>` or `List<Vector>`.
|
/// Used for the fill and stroke nodes so they can apply to either `List<Graphic>` or `List<Vector>`.
|
||||||
trait VectorListIterMut {
|
trait VectorListIterMut {
|
||||||
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
|
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
|
||||||
|
|
||||||
|
fn for_each_vector_list_mut(&mut self, f: impl FnMut(&mut List<Vector>));
|
||||||
|
|
||||||
fn vector_count(&self) -> usize;
|
fn vector_count(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +56,14 @@ impl VectorListIterMut for List<Graphic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
|
||||||
|
for graphic in self.iter_element_values_mut() {
|
||||||
|
if let Some(vector_list) = graphic.as_vector_mut() {
|
||||||
|
f(vector_list);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn vector_count(&self) -> usize {
|
fn vector_count(&self) -> usize {
|
||||||
self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum()
|
self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum()
|
||||||
}
|
}
|
||||||
@@ -64,6 +77,10 @@ impl VectorListIterMut for List<Vector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
|
||||||
|
f(self);
|
||||||
|
}
|
||||||
|
|
||||||
fn vector_count(&self) -> usize {
|
fn vector_count(&self) -> usize {
|
||||||
self.len()
|
self.len()
|
||||||
}
|
}
|
||||||
@@ -109,26 +126,29 @@ where
|
|||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||||
|
|
||||||
let mut i: usize = 0;
|
let mut i: usize = 0;
|
||||||
content.for_each_vector_mut(|vector, _transform| {
|
content.for_each_vector_list_mut(|vector_list| {
|
||||||
let factor = match randomize {
|
for index in 0..vector_list.len() {
|
||||||
true => rng.random::<f64>(),
|
let factor = match randomize {
|
||||||
false => match repeat_every {
|
true => rng.random::<f64>(),
|
||||||
0 => i as f64 / (length - 1).max(1) as f64,
|
false => match repeat_every {
|
||||||
1 => 0.,
|
0 => i as f64 / (length - 1).max(1) as f64,
|
||||||
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
|
1 => 0.,
|
||||||
},
|
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let color = gradient.evaluate(factor);
|
let color = gradient.evaluate(factor);
|
||||||
|
let paint = List::new_from_element(color).into_graphic_list();
|
||||||
|
|
||||||
if fill {
|
if fill {
|
||||||
vector.style.set_fill(Fill::Solid(color));
|
set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
|
||||||
|
}
|
||||||
|
if stroke && vector_list.element(index).and_then(|vector| vector.style.stroke()).is_some() {
|
||||||
|
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
if stroke && let Some(stroke) = vector.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
|
|
||||||
vector.style.set_stroke(stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
content
|
content
|
||||||
@@ -136,41 +156,77 @@ where
|
|||||||
|
|
||||||
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
|
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
|
||||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
|
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
|
||||||
async fn fill<F: Into<Fill> + 'n + Send, V: VectorListIterMut + 'n + Send>(
|
async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send + 'static>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The content with vector paths to apply the fill style to.
|
/// The content with vector paths to apply the fill style to.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Vector>,
|
List<Vector>, List<Vector>, List<Vector>, List<Vector>, List<Vector>, List<Vector>,
|
||||||
List<Vector>,
|
List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
List<Vector>,
|
|
||||||
List<Vector>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Graphic>,
|
|
||||||
List<Graphic>,
|
|
||||||
)]
|
)]
|
||||||
mut content: V,
|
mut content: V,
|
||||||
/// The fill to paint the path with.
|
/// The fill to paint the path with.
|
||||||
#[default(Color::BLACK)]
|
#[default(Color::BLACK)]
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Fill,
|
List<Graphic>, List<Vector>, List<Color>, List<GradientStops>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Graphic>, List<Vector>, List<Color>, List<GradientStops>, List<Raster<CPU>>, List<Raster<GPU>>,
|
||||||
List<GradientStops>,
|
|
||||||
Gradient,
|
|
||||||
Fill,
|
|
||||||
List<Color>,
|
|
||||||
List<GradientStops>,
|
|
||||||
Gradient,
|
|
||||||
)]
|
)]
|
||||||
fill: F,
|
mut fill: F,
|
||||||
_backup_color: List<Color>,
|
_backup_color: List<Color>,
|
||||||
_backup_gradient: Gradient,
|
_backup_gradient: List<GradientStops>,
|
||||||
|
_gradient_type: GradientType,
|
||||||
|
_spread_method: GradientSpreadMethod,
|
||||||
|
_transform: Option<DAffine2>,
|
||||||
) -> V {
|
) -> V {
|
||||||
let fill: Fill = fill.into();
|
if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::<List<GradientStops>>() {
|
||||||
content.for_each_vector_mut(|vector, _transform| {
|
if gradient.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_none() {
|
||||||
vector.style.set_fill(fill.clone());
|
for value in gradient.iter_attribute_values_mut_or_default::<GradientType>(ATTR_GRADIENT_TYPE) {
|
||||||
});
|
*value = _gradient_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gradient.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_none() {
|
||||||
|
for value in gradient.iter_attribute_values_mut_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD) {
|
||||||
|
*value = _spread_method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gradient.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_none() {
|
||||||
|
let transform = _transform.unwrap_or_else(|| {
|
||||||
|
// Construct a transform that covers the bounding box of the paint target
|
||||||
|
let mut bounds: Option<[DVec2; 2]> = None;
|
||||||
|
content.for_each_vector_mut(|vector, _| {
|
||||||
|
if let Some([min, max]) = vector.bounding_box() {
|
||||||
|
bounds = Some(match bounds {
|
||||||
|
Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)],
|
||||||
|
None => [min, max],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box`
|
||||||
|
let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||||
|
if max.x - min.x < 1e-10 {
|
||||||
|
max.x = min.x + 1.;
|
||||||
|
}
|
||||||
|
if max.y - min.y < 1e-10 {
|
||||||
|
max.y = min.y + 1.;
|
||||||
|
}
|
||||||
|
initial_gradient_transform_for_bounding_box([min, max])
|
||||||
|
});
|
||||||
|
|
||||||
|
for value in gradient.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
|
*value = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fill = fill.into_graphic_list();
|
||||||
|
content.for_each_vector_list_mut(|vector_list| {
|
||||||
|
// Broadcast the same paint to every item, scanning the attribute column once instead of per index
|
||||||
|
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(ATTR_FILL) {
|
||||||
|
*slot = fill.clone();
|
||||||
|
}
|
||||||
|
});
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +251,41 @@ impl IntoF64Vec for String {
|
|||||||
|
|
||||||
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
|
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
|
||||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
|
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
|
||||||
async fn stroke<V, L: IntoF64Vec>(
|
async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The content with vector paths to apply the stroke style to.
|
/// The content with vector paths to apply the stroke style to.
|
||||||
#[implementations(List<Vector>, List<Vector>, List<Vector>, List<Graphic>, List<Graphic>, List<Graphic>)]
|
#[implementations(
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
)]
|
||||||
mut content: List<V>,
|
mut content: List<V>,
|
||||||
/// The stroke color.
|
/// The stroke paint.
|
||||||
#[default(Color::BLACK)]
|
#[default(Color::BLACK)]
|
||||||
color: List<Color>,
|
#[implementations(
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Color>, List<Color>, List<Color>,
|
||||||
|
List<GradientStops>, List<GradientStops>, List<GradientStops>,
|
||||||
|
List<Raster<CPU>>, List<Raster<CPU>>, List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>, List<Raster<GPU>>, List<Raster<GPU>>,
|
||||||
|
List<Graphic>, List<Graphic>, List<Graphic>,
|
||||||
|
List<Vector>, List<Vector>, List<Vector>,
|
||||||
|
List<Color>, List<Color>, List<Color>,
|
||||||
|
List<GradientStops>, List<GradientStops>, List<GradientStops>,
|
||||||
|
List<Raster<CPU>>, List<Raster<CPU>>, List<Raster<CPU>>,
|
||||||
|
List<Raster<GPU>>, List<Raster<GPU>>, List<Raster<GPU>>,
|
||||||
|
)]
|
||||||
|
paint: P,
|
||||||
/// The stroke thickness.
|
/// The stroke thickness.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[default(2.)]
|
#[default(2.)]
|
||||||
@@ -220,7 +303,20 @@ async fn stroke<V, L: IntoF64Vec>(
|
|||||||
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
||||||
paint_order: PaintOrder,
|
paint_order: PaintOrder,
|
||||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||||
#[implementations(List<f64>, f64, String, List<f64>, f64, String)]
|
#[implementations(
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
List<f64>, f64, String,
|
||||||
|
)]
|
||||||
dash_lengths: L,
|
dash_lengths: L,
|
||||||
/// The phase offset distance from the starting point of the dash pattern.
|
/// The phase offset distance from the starting point of the dash pattern.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
@@ -232,7 +328,8 @@ where
|
|||||||
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
|
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
|
||||||
|
|
||||||
let stroke = Stroke {
|
let stroke = Stroke {
|
||||||
color: color.element(0).copied(),
|
// TODO: Remove once the deprecated `Stroke.color` field is deleted in favor of the `ATTR_STROKE` attribute
|
||||||
|
color: None,
|
||||||
weight,
|
weight,
|
||||||
dash_lengths,
|
dash_lengths,
|
||||||
dash_offset,
|
dash_offset,
|
||||||
@@ -250,6 +347,13 @@ where
|
|||||||
vector.style.set_stroke(stroke);
|
vector.style.set_stroke(stroke);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let paint = paint.into_graphic_list();
|
||||||
|
content.for_each_vector_list_mut(|vector_list| {
|
||||||
|
// Broadcast the same paint to every item, scanning the attribute column once instead of per index
|
||||||
|
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(ATTR_STROKE) {
|
||||||
|
*slot = paint.clone();
|
||||||
|
}
|
||||||
|
});
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,15 +1260,21 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
||||||
// TODO: Make this node support stroke align, which it currently ignores
|
// TODO: Make this node support stroke align, which it currently ignores
|
||||||
|
|
||||||
let graphic_list = content.into_graphic_list();
|
let graphic_list = content.into_graphic_list();
|
||||||
let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
|
let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
|
||||||
|
|
||||||
|
// A fill exists when the canonical attribute carries paint or, matching the renderer's fallback, when the legacy `style.fill` does
|
||||||
|
let has_fills: Vec<bool> = (0..flattened.len())
|
||||||
|
.map(|index| has_paint_at(&flattened, index, ATTR_FILL) || flattened.element(index).is_some_and(|vector| !vector.style.fill().is_none()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut output: List<Vector> = flattened
|
let mut output: List<Vector> = flattened
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|row| {
|
.zip(has_fills)
|
||||||
|
.flat_map(|(row, has_fill)| {
|
||||||
let (mut vector, attributes) = row.into_parts();
|
let (mut vector, attributes) = row.into_parts();
|
||||||
|
|
||||||
let stroke = vector.style.stroke().clone().unwrap_or_default();
|
let stroke = vector.style.stroke().clone().unwrap_or_default();
|
||||||
@@ -1197,7 +1307,7 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
|
|||||||
let stroke_options_default = kurbo::StrokeOpts::default();
|
let stroke_options_default = kurbo::StrokeOpts::default();
|
||||||
let stroke_options_stable = kurbo::StrokeOpts::default().stable_dash_order(true);
|
let stroke_options_stable = kurbo::StrokeOpts::default().stable_dash_order(true);
|
||||||
|
|
||||||
// 0.25 is balanced between performace and accuracy of the curve.
|
// 0.25 is balanced between performance and accuracy of the curve.
|
||||||
const STROKE_TOLERANCE: f64 = 0.25;
|
const STROKE_TOLERANCE: f64 = 0.25;
|
||||||
|
|
||||||
for mut path in bezpaths {
|
for mut path in bezpaths {
|
||||||
@@ -1214,14 +1324,7 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
|
|||||||
solidified_stroke.append_bezpath(solidified);
|
solidified_stroke.append_bezpath(solidified);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We set the solidified stroke's fill to the stroke's color and without a stroke.
|
|
||||||
if let Some(stroke) = vector.style.stroke() {
|
|
||||||
solidified_stroke.style.set_fill(Fill::solid_or_none(stroke.color));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the original vector has a fill, preserve it as a separate item with the stroke cleared.
|
// If the original vector has a fill, preserve it as a separate item with the stroke cleared.
|
||||||
let has_attr_fill = attributes.keys().any(|k| k == ATTR_FILL);
|
|
||||||
let has_fill = has_attr_fill || !vector.style.fill().is_none();
|
|
||||||
let fill_row = has_fill.then(|| {
|
let fill_row = has_fill.then(|| {
|
||||||
vector.style.clear_stroke();
|
vector.style.clear_stroke();
|
||||||
let mut fill_attributes = attributes.clone();
|
let mut fill_attributes = attributes.clone();
|
||||||
@@ -1235,6 +1338,13 @@ async fn solidify_stroke<T: IntoGraphicList + 'n + Send + Clone>(_: impl Ctx, #[
|
|||||||
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
|
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
|
||||||
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
|
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
|
||||||
|
|
||||||
|
// Fall back to the legacy stroke color when no canonical stroke paint attribute was carried over
|
||||||
|
if !stroke_attributes.get::<List<Graphic>>(ATTR_FILL).is_some_and(is_paint_present)
|
||||||
|
&& let Some(color) = stroke.color
|
||||||
|
{
|
||||||
|
set_paint_attribute(&mut stroke_attributes, ATTR_FILL, List::new_from_element(color));
|
||||||
|
}
|
||||||
|
|
||||||
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
|
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
|
||||||
|
|
||||||
// Ordering based on the paint order. The first item in the `List` is rendered below the second.
|
// Ordering based on the paint order. The first item in the `List` is rendered below the second.
|
||||||
@@ -1331,12 +1441,14 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List<Vec
|
|||||||
|
|
||||||
// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes.
|
// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes.
|
||||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||||
pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
|
||||||
let graphic_list = content.into_graphic_list();
|
let graphic_list = content.into_graphic_list();
|
||||||
let flattened = graphic_list.clone().into_flattened_list::<Vector>();
|
let flattened = graphic_list.clone().into_flattened_list::<Vector>();
|
||||||
|
|
||||||
// Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to
|
// Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to
|
||||||
let mut output_list = List::new_from_element(Vector::default());
|
let mut output_list = List::new_from_element(Vector::default());
|
||||||
|
let mut primary_source = None;
|
||||||
|
|
||||||
let output = output_list.element_mut(0).unwrap();
|
let output = output_list.element_mut(0).unwrap();
|
||||||
|
|
||||||
// Concatenate every vector element's subpaths into the single output compound path
|
// Concatenate every vector element's subpaths into the single output compound path
|
||||||
@@ -1349,11 +1461,30 @@ pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[impleme
|
|||||||
(index, node_id).hash(&mut hasher);
|
(index, node_id).hash(&mut hasher);
|
||||||
let collision_hash_seed = hasher.finish();
|
let collision_hash_seed = hasher.finish();
|
||||||
|
|
||||||
output.concat(element, flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index), collision_hash_seed);
|
let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
output.concat(element, source_transform, collision_hash_seed);
|
||||||
|
|
||||||
// TODO: Make this instead use the first encountered style
|
// TODO: Make this instead use the first encountered style
|
||||||
// Use the last encountered style as the output style
|
// Use the last encountered style as the output style
|
||||||
output.style = element.style.clone();
|
output.style = element.style.clone();
|
||||||
|
|
||||||
|
primary_source = Some((index, source_transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((primary, source_transform)) = primary_source {
|
||||||
|
let source_attributes = flattened.clone_item_attributes(primary);
|
||||||
|
let mut attributes = ItemAttributeValues::new();
|
||||||
|
|
||||||
|
attributes.insert_cloned_from(&source_attributes, ATTR_FILL);
|
||||||
|
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE);
|
||||||
|
bake_paint_transforms(&mut attributes, source_transform);
|
||||||
|
|
||||||
|
let output = std::mem::take(output_list.element_mut(0).unwrap());
|
||||||
|
output_list = List::new_from_item(Item::from_parts(output, attributes));
|
||||||
|
|
||||||
|
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
|
||||||
|
let layer_path: List<NodeId> = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary);
|
||||||
|
output_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve a reference to the original upstream `List<Graphic>` so the renderer can recurse into it
|
// Preserve a reference to the original upstream `List<Graphic>` so the renderer can recurse into it
|
||||||
@@ -1361,13 +1492,6 @@ pub async fn flatten_path<T: IntoGraphicList + 'n + Send>(_: impl Ctx, #[impleme
|
|||||||
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
|
// This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge.
|
||||||
output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
|
output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list);
|
||||||
|
|
||||||
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
|
|
||||||
if !flattened.is_empty() {
|
|
||||||
let primary = flattened.len() - 1;
|
|
||||||
let layer_path: List<NodeId> = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary);
|
|
||||||
output_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
output_list
|
output_list
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2064,7 +2188,7 @@ async fn offset_points(
|
|||||||
///
|
///
|
||||||
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
|
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
|
||||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
|
async fn morph<I: IntoGraphicList>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
|
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
|
||||||
#[implementations(List<Graphic>, List<Vector>)]
|
#[implementations(List<Graphic>, List<Vector>)]
|
||||||
@@ -2177,6 +2301,79 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lerp_gradient_transform(gradient_list_a: &List<GradientStops>, gradient_list_b: &List<GradientStops>, time: f64) -> DAffine2 {
|
||||||
|
let transform_a = gradient_list_a.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
|
||||||
|
let transform_b = gradient_list_b.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
|
||||||
|
|
||||||
|
let start_a = transform_a.translation;
|
||||||
|
let end_a = transform_a.translation + transform_a.matrix2.x_axis;
|
||||||
|
let start_b = transform_b.translation;
|
||||||
|
let end_b = transform_b.translation + transform_b.matrix2.x_axis;
|
||||||
|
|
||||||
|
let start = start_a.lerp(start_b, time);
|
||||||
|
let end = end_a.lerp(end_b, time);
|
||||||
|
|
||||||
|
let metadata_source_transform = if time < 0.5 { transform_a } else { transform_b };
|
||||||
|
build_transform_with_y_preservation(metadata_source_transform, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lerp between two graphics. Solid color and gradient pairings interpolate; all other pairings step at the midpoint.
|
||||||
|
fn lerp_graphic(a: Option<&List<Graphic>>, b: Option<&List<Graphic>>, time: f64) -> Option<List<Graphic>> {
|
||||||
|
let transparent = List::new_from_element(Color::TRANSPARENT).into_graphic_list();
|
||||||
|
|
||||||
|
let a = a.filter(|graphic_list| is_paint_present(graphic_list));
|
||||||
|
let b = b.filter(|graphic_list| is_paint_present(graphic_list));
|
||||||
|
|
||||||
|
let (a, b) = match (a, b) {
|
||||||
|
(None, None) => return None,
|
||||||
|
(Some(a), None) => (a, &transparent),
|
||||||
|
(None, Some(b)) => (&transparent, b),
|
||||||
|
(Some(a), Some(b)) => (a, b),
|
||||||
|
};
|
||||||
|
|
||||||
|
// This keeps the gradient metadata attributes
|
||||||
|
let gradient_with_stops = |mut gradient_list: List<GradientStops>, stops: GradientStops| -> Graphic {
|
||||||
|
if let Some(target) = gradient_list.element_mut(0) {
|
||||||
|
*target = stops;
|
||||||
|
} else {
|
||||||
|
gradient_list.push(Item::new_from_element(stops));
|
||||||
|
}
|
||||||
|
Graphic::Gradient(gradient_list)
|
||||||
|
};
|
||||||
|
|
||||||
|
let graphic = match (a.element(0), b.element(0)) {
|
||||||
|
(Some(Graphic::Color(color_list_a)), Some(Graphic::Color(color_list_b))) => color_list_a
|
||||||
|
.element(0)
|
||||||
|
.zip(color_list_b.element(0))
|
||||||
|
.map(|(color_a, color_b)| Graphic::from(color_a.lerp(color_b, time as f32))),
|
||||||
|
(Some(Graphic::Color(color_list_a)), Some(Graphic::Gradient(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| {
|
||||||
|
let mut solid_to_gradient = stops_b.clone();
|
||||||
|
solid_to_gradient.color.iter_mut().for_each(|color| *color = *color_a);
|
||||||
|
let stops = solid_to_gradient.lerp(stops_b, time);
|
||||||
|
gradient_with_stops(gradient_list_b.clone(), stops)
|
||||||
|
}),
|
||||||
|
(Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Color(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| {
|
||||||
|
let mut gradient_to_solid = stops_a.clone();
|
||||||
|
gradient_to_solid.color.iter_mut().for_each(|color| *color = *color_b);
|
||||||
|
let stops = stops_a.lerp(&gradient_to_solid, time);
|
||||||
|
gradient_with_stops(gradient_list_a.clone(), stops)
|
||||||
|
}),
|
||||||
|
(Some(Graphic::Gradient(gradient_list_a)), Some(Graphic::Gradient(gradient_list_b))) => gradient_list_a.element(0).zip(gradient_list_b.element(0)).map(|(stops_a, stops_b)| {
|
||||||
|
let stops = stops_a.lerp(stops_b, time);
|
||||||
|
let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
|
||||||
|
|
||||||
|
let mut gradient_list = metadata_source.clone();
|
||||||
|
gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
|
||||||
|
|
||||||
|
gradient_with_stops(gradient_list, stops)
|
||||||
|
}),
|
||||||
|
// Pairings beyond solid colors and gradients (raster, vector, or mixed) can't be interpolated, so step at the midpoint
|
||||||
|
_ => return Some(if time < 0.5 { a.clone() } else { b.clone() }),
|
||||||
|
};
|
||||||
|
|
||||||
|
graphic.map(List::new_from_element)
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
|
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
|
||||||
let mut graphic_list_content = content.clone().into_graphic_list();
|
let mut graphic_list_content = content.clone().into_graphic_list();
|
||||||
|
|
||||||
@@ -2435,9 +2632,35 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
|
|||||||
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
|
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut vector = Vector {
|
let mut vector = Vector::default();
|
||||||
style: source_element.style.lerp(&target_element.style, time),
|
vector.style.stroke = match (source_element.style.stroke.as_ref(), target_element.style.stroke.as_ref()) {
|
||||||
..Default::default()
|
(Some(a), Some(b)) => Some(a.lerp(b, time)),
|
||||||
|
(Some(a), None) => {
|
||||||
|
if time < 0.5 {
|
||||||
|
Some(a.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(b)) => {
|
||||||
|
if time < 0.5 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(b.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fill_paint = {
|
||||||
|
let source = fill_graphic_list_at(&content, source_index);
|
||||||
|
let target = fill_graphic_list_at(&content, target_index);
|
||||||
|
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
||||||
|
};
|
||||||
|
let stroke_paint = {
|
||||||
|
let source = stroke_graphic_list_at(&content, source_index);
|
||||||
|
let target = stroke_graphic_list_at(&content, target_index);
|
||||||
|
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
||||||
@@ -2588,16 +2811,23 @@ async fn morph<I: IntoGraphicList + 'n + Send + Clone>(
|
|||||||
let primary_index = if time < 0.5 { source_index } else { target_index };
|
let primary_index = if time < 0.5 { source_index } else { target_index };
|
||||||
let layer_path: List<NodeId> = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index);
|
let layer_path: List<NodeId> = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index);
|
||||||
|
|
||||||
List::new_from_item(
|
let mut item = Item::new_from_element(vector)
|
||||||
Item::new_from_element(vector)
|
.with_attribute(ATTR_TRANSFORM, lerped_transform)
|
||||||
.with_attribute(ATTR_TRANSFORM, lerped_transform)
|
.with_attribute(ATTR_BLEND_MODE, lerped_blend_mode)
|
||||||
.with_attribute(ATTR_BLEND_MODE, lerped_blend_mode)
|
.with_attribute(ATTR_OPACITY, lerped_opacity)
|
||||||
.with_attribute(ATTR_OPACITY, lerped_opacity)
|
.with_attribute(ATTR_OPACITY_FILL, lerped_fill)
|
||||||
.with_attribute(ATTR_OPACITY_FILL, lerped_fill)
|
.with_attribute(ATTR_CLIPPING_MASK, lerped_clip)
|
||||||
.with_attribute(ATTR_CLIPPING_MASK, lerped_clip)
|
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
|
||||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
|
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
|
||||||
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content),
|
|
||||||
)
|
if let Some(fill) = fill_paint {
|
||||||
|
item.set_attribute(ATTR_FILL, fill);
|
||||||
|
}
|
||||||
|
if let Some(stroke) = stroke_paint {
|
||||||
|
item.set_attribute(ATTR_STROKE, stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
List::new_from_item(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector {
|
fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector {
|
||||||
@@ -3207,6 +3437,36 @@ mod test {
|
|||||||
assert!((morphed.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3);
|
assert!((morphed.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn morph_interpolates_fill() {
|
||||||
|
let rect = || {
|
||||||
|
let mut v = Vector::default();
|
||||||
|
v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
|
||||||
|
v
|
||||||
|
};
|
||||||
|
|
||||||
|
let item_a = Item::new_from_element(rect())
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
|
||||||
|
.with_attribute(ATTR_FILL, List::new_from_element(Color::RED).into_graphic_list());
|
||||||
|
let item_b = Item::new_from_element(rect())
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into()))
|
||||||
|
.with_attribute(ATTR_FILL, List::new_from_element(Color::BLUE).into_graphic_list());
|
||||||
|
|
||||||
|
let mut content = List::new_from_item(item_a);
|
||||||
|
content.push(item_b);
|
||||||
|
|
||||||
|
let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await;
|
||||||
|
|
||||||
|
let fill = fill_graphic_list_at(&morphed, 0).expect("Morph should keep the fill paint at the midpoint");
|
||||||
|
|
||||||
|
// Interpolated color between red and blue should have >0 value on both R and B
|
||||||
|
let Some(Graphic::Color(colors)) = fill.element(0) else {
|
||||||
|
panic!("Expected a solid color fill, got {:?}", fill.element(0));
|
||||||
|
};
|
||||||
|
let color = *colors.element(0).expect("Color present");
|
||||||
|
assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn contains_segment(vector: Vector, target: PathSeg) {
|
fn contains_segment(vector: Vector, target: PathSeg) {
|
||||||
let segments = vector.segment_iter().map(|x| x.1);
|
let segments = vector.segment_iter().map(|x| x.1);
|
||||||
|
|||||||
Reference in New Issue
Block a user