Add Path node differential edit summary widget, and store imported SVG content in that VectorModification struct (#4036)
* Make paste/import SVG store as the Path node's VectorModification not Table<Vector> * Add a migration from Table<Vector> to VectorModification for existing documents * Add a VectorModification widget to visualize change counts * Refactor VectorModification to compute per-category counts for additions, removals, and modifications
This commit is contained in:
parent
6c5e3c97f8
commit
c2e1208d82
|
|
@ -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)]
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
// ===============================
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
// ==================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue