Make the Transform node use angles in degrees instead of radians for Rotation and Skew (#3160)

* Make the Transform node use degrees not radians

* Migration script

* Migrate skew value input to store degrees

* Add comments

* Fix migrations to account for the old deprecated "Pivot" parameter

* Fix tooling interactions with degrees-based transforms

* Upgrade demo art

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant
2025-09-11 00:19:10 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent c51967384f
commit 332088bce1
14 changed files with 143 additions and 38 deletions
@@ -5,7 +5,7 @@ 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::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use glam::IVec2;
use glam::{DVec2, IVec2};
use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
@@ -16,6 +16,7 @@ use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
use std::collections::HashMap;
use std::f64::consts::PI;
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
@@ -1051,14 +1052,80 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.add_import(TaggedValue::U32(0), false, 1, "Loop Level", "TODO", &node_path);
}
// Add context features to nodes that don't have them (fine-grained context caching migration)
if node.context_features == graphene_std::ContextDependencies::default() {
if let Some(reference) = document.network_interface.reference(node_id, network_path).cloned().flatten() {
if let Some(node_definition) = resolve_document_node_type(&reference) {
let context_features = node_definition.node_template.document_node.context_features;
document.network_interface.set_context_features(node_id, network_path, context_features);
// Migrate the Transform node to use degrees instead of radians
if reference == "Transform" && node.inputs.get(6).is_none() {
// Migrate rotation from radians to degrees
match node.inputs.get(2)? {
NodeInput::Value { tagged_value, exposed } => {
// Read the existing Properties panel number value, which used to be in radians
let TaggedValue::F64(radians) = *tagged_value.clone().into_inner() else { return None };
// Convert the radians to degrees and set it back as the new input value
let degrees = NodeInput::value(TaggedValue::F64(radians.to_degrees()), *exposed);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), degrees, network_path);
}
NodeInput::Node { .. } => {
// Construct a new Multiply node for converting from degrees to radians
let Some(multiply_node) = resolve_document_node_type("Multiply") else {
log::error!("Could not get multiply node from definition when upgrading transform");
return None;
};
let mut multiply_template = multiply_node.default_node_template();
multiply_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false);
// Decide on the placement position of the new Multiply node
let multiply_node_id = NodeId::new();
let Some(transform_position) = document.network_interface.position_from_downstream_node(node_id, network_path) else {
log::error!("Could not get positon for transform node {node_id}");
return None;
};
let multiply_position = transform_position + IVec2::new(-7, 1);
// Insert the new Multiply node into the network directly before it's used
document.network_interface.insert_node(multiply_node_id, multiply_template, network_path);
document.network_interface.shift_absolute_node_position(&multiply_node_id, multiply_position, network_path);
document.network_interface.insert_node_between(&multiply_node_id, &InputConnector::node(*node_id, 2), 0, network_path);
}
_ => {}
};
// Migrate skew from radians to degrees
if let NodeInput::Value { tagged_value, exposed } = node.inputs.get(4)? {
// Read the existing Properties panel number value, which used to be in radians
let TaggedValue::DVec2(old_value) = *tagged_value.clone().into_inner() else { return None };
// The previous value stored the tangent of the displayed degrees. Now it stores the degrees, so take the arctan of it and convert to degrees.
let new_value = DVec2::new(old_value.x.atan().to_degrees(), old_value.y.atan().to_degrees());
let new_input = NodeInput::value(TaggedValue::DVec2(new_value), *exposed);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), new_input, network_path);
}
// Remove the possible existence of the old "Pivot" hidden value input that was removed in #2730
let nested_transform_network = [network_path, &[*node_id]].concat();
if node.inputs.get(5).is_some() {
document.network_interface.remove_import(5, &nested_transform_network);
}
// Add the Origin Offset parameter as a hidden input, which will be given actual functionality in the future but is currently used as a marker to detect not-yet-upgraded Transform nodes
document
.network_interface
.add_import(TaggedValue::DVec2(DVec2::ZERO), false, 5, "Origin Offset", "", &nested_transform_network);
document.network_interface.set_input_override(node_id, 5, Some("hidden".to_string()), network_path); // Hide it while we're not yet using it
// Add the Scale Appearance parameter as a hidden input, which will be given actual functionality in the future but is currently used as a marker to detect not-yet-upgraded Transform nodes
document
.network_interface
.add_import(TaggedValue::Bool(true), false, 6, "Scale Appearance", "", &nested_transform_network);
document.network_interface.set_input_override(node_id, 6, Some("hidden".to_string()), network_path); // Hide it while we're not yet using it
}
// Add context features to nodes that don't have them (fine-grained context caching migration)
if node.context_features == graphene_std::ContextDependencies::default()
&& let Some(reference) = document.network_interface.reference(node_id, network_path).cloned().flatten()
&& let Some(node_definition) = resolve_document_node_type(&reference)
{
let context_features = node_definition.node_template.document_node.context_features;
document.network_interface.set_context_features(node_id, network_path, context_features);
}
// ==================================