Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, Stroke};
use graphene_std::vector::{PointId, VectorModificationType};
use graphene_std::vector::{PointId, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};

#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -211,11 +211,13 @@ impl<'a> ModifyInputsContext<'a> {
}

pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector = Table::new_from_element(Vector::from_subpaths(subpaths, true));
// Build a VectorModification that reproduces the geometry (same format the Pen tool uses)
let vector = Vector::from_subpaths(subpaths, true);
let modification = Box::new(VectorModification::create_from_vector(&vector));

let shape = resolve_network_node_type("Path")
.expect("Path node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::Vector(vector), false))]);
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VectorModification(modification), false))]);
let shape_id = NodeId::new();
self.network_interface.insert_node(shape_id, shape, &[]);
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[], self.import);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ use graphene_std::raster::{
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::QRCodeErrorCorrectionLevel;
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};

pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_instance();
Expand Down Expand Up @@ -230,6 +230,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info),
Some(x) if x == TypeId::of::<Footprint>() => footprint_widget(default_info, &mut extra_widgets),
Some(x) if x == TypeId::of::<Box<VectorModification>>() => vector_modification_widget(default_info).into(),
// ===============================
// MANUALLY IMPLEMENTED ENUM TYPES
// ===============================
Expand Down Expand Up @@ -398,6 +399,27 @@ pub fn reference_point_widget(parameter_widgets_info: ParameterWidgetsInfo, disa
widgets
}

pub fn vector_modification_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;

let mut widgets = start_widgets(parameter_widgets_info);

let Some(document_node) = document_node else { return widgets };
let Some(input) = document_node.inputs.get(index) else { return widgets };

if let Some(TaggedValue::VectorModification(modification)) = input.as_non_exposed_value() {
let label = modification.summary_label();
let tooltip = modification.summary_tooltip();

widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextLabel::new(label).tooltip_label("Summary of Differential Edits").tooltip_description(tooltip).widget_instance(),
]);
}

widgets
}

pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widgets: &mut Vec<LayoutGroup>) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;

Expand Down
30 changes: 25 additions & 5 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId;
Expand Down Expand Up @@ -1140,10 +1139,8 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
log::error!("Path node does not exist.");
return None;
};
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::Vector(Table::new_from_element(vector)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(&vector));
let path_node = path_node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VectorModification(modification), false))]);

// Get the "Spline" node definition and wire it up with the "Path" node as input
let Some(spline_node_type) = resolve_proto_node_type(graphene_std::vector::spline::IDENTIFIER) else {
Expand Down Expand Up @@ -1952,6 +1949,29 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}

// Migrate Path nodes that stored geometry directly in input 0 (as a Table<Vector>) to instead use a VectorModification in input 1
if reference == DefinitionIdentifier::Network("Path".into()) {
let input_0 = node.inputs.first()?;
if let NodeInput::Value { tagged_value, exposed } = input_0
&& !exposed
&& let TaggedValue::Vector(vector_table) = &**tagged_value
&& !vector_table.is_empty()
{
let vector = vector_table.iter().next()?.element;
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(vector));

// Reset input 0 to the default exposed state
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::Vector(Default::default()), true), network_path);

// Store the converted VectorModification in input 1
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::VectorModification(modification), false), network_path);
}
}

// ==================================
// PUT ALL MIGRATIONS ABOVE THIS LINE
// ==================================
Expand Down
101 changes: 101 additions & 0 deletions node-graph/libraries/vector-types/src/vector/vector_modification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,108 @@ pub enum VectorModificationType {
ApplyEndDelta { segment: SegmentId, delta: DVec2 },
}

/// Per-category `[added, removed, modified]` counts for a [`VectorModification`].
struct ModificationCategoryCounts {
points: [usize; 3],
segments: [usize; 3],
regions: [usize; 3],
smooth_handles: [usize; 3],
}

impl ModificationCategoryCounts {
/// Returns the `[added, removed, modified]` totals across all categories.
fn totals(&self) -> [usize; 3] {
let mut totals = [0; 3];
for [a, r, m] in [self.points, self.segments, self.regions, self.smooth_handles] {
totals[0] += a;
totals[1] += r;
totals[2] += m;
}
totals
}

/// Iterates over each named category and its `[added, removed, modified]` counts.
fn iter_categories(&self) -> impl Iterator<Item = (&str, [usize; 3])> {
[("Points", self.points), ("Segments", self.segments), ("Regions", self.regions), ("Smooth Handles", self.smooth_handles)].into_iter()
}
}

impl VectorModification {
/// Computes per-category counts of additions, removals, and modifications.
fn category_counts(&self) -> ModificationCategoryCounts {
// Build sets of added IDs so we can distinguish true modifications from initial values stored for newly added items
let add_points: HashSet<_> = self.points.add.iter().copied().collect();
let add_segments: HashSet<_> = self.segments.add.iter().copied().collect();
let add_regions: HashSet<_> = self.regions.add.iter().copied().collect();

let point_modifications = self.points.delta.keys().filter(|id| !add_points.contains(id)).count();

// Count unique modified segment IDs across all field maps
let mut modified_segments: HashSet<&SegmentId> = HashSet::with_capacity(self.segments.start_point.len());
let not_added_segment = |id: &&SegmentId| !add_segments.contains(id);
modified_segments.extend(self.segments.start_point.keys().filter(not_added_segment));
modified_segments.extend(self.segments.end_point.keys().filter(not_added_segment));
modified_segments.extend(self.segments.handle_primary.keys().filter(not_added_segment));
modified_segments.extend(self.segments.handle_end.keys().filter(not_added_segment));
modified_segments.extend(self.segments.stroke.keys().filter(not_added_segment));

// Count unique modified region IDs across all field maps
let mut modified_regions: HashSet<&RegionId> = HashSet::with_capacity(self.regions.segment_range.len());
let not_added_region = |id: &&RegionId| !add_regions.contains(id);
modified_regions.extend(self.regions.segment_range.keys().filter(not_added_region));
modified_regions.extend(self.regions.fill.keys().filter(not_added_region));

ModificationCategoryCounts {
points: [self.points.add.len(), self.points.remove.len(), point_modifications],
segments: [self.segments.add.len(), self.segments.remove.len(), modified_segments.len()],
regions: [self.regions.add.len(), self.regions.remove.len(), modified_regions.len()],
smooth_handles: [self.add_g1_continuous.len(), self.remove_g1_continuous.len(), 0],
}
}

/// Returns a short human-readable summary string like "+6 / −1 / Δ1".
pub fn summary_label(&self) -> String {
let counts = self.category_counts();
let [additions, removals, modifications] = counts.totals();

let mut parts = Vec::new();
if additions > 0 {
parts.push(format!("+{additions}"));
}
if removals > 0 {
parts.push(format!("\u{2212}{removals}"));
}
if modifications > 0 {
parts.push(format!("\u{0394}{modifications}"));
}
if parts.is_empty() { "No Differential Edits".to_string() } else { parts.join(" / ") }
}

/// Returns a detailed multi-line tooltip describing all the changes.
pub fn summary_tooltip(&self) -> String {
let counts = self.category_counts();

let mut lines = Vec::new();

for (name, [added, removed, modified]) in counts.iter_categories() {
let mut parts = Vec::new();
if added > 0 {
parts.push(format!("+{added}"));
}
if removed > 0 {
parts.push(format!("\u{2212}{removed}"));
}
if modified > 0 {
parts.push(format!("\u{0394}{modified}"));
}
if !parts.is_empty() {
lines.push(format!("{name}: {}", parts.join(" / ")));
}
}

if lines.is_empty() { "None".to_string() } else { lines.join("\n") }
}

/// Apply this modification to the specified [`Vector`].
pub fn apply<Upstream>(&self, vector: &mut Vector<Upstream>) {
self.points.apply(&mut vector.point_domain, &mut vector.segment_domain);
Expand Down
Loading