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
@@ -32,10 +32,13 @@ pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64
/// Update the inputs of the transform node to match a new transform
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
let (scale, angle, translation, shear) = compute_scale_angle_translation_shear(transform);
let (scale, rotation, translation, shear) = compute_scale_angle_translation_shear(transform);
let rotation = rotation.to_degrees();
let shear = DVec2::new(shear.x.atan().to_degrees(), shear.y.atan().to_degrees());
network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(translation), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(angle), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(rotation), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::DVec2(scale), false), &[]);
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(shear), false), &[]);
}
@@ -76,14 +79,14 @@ pub fn get_current_transform(inputs: &[NodeInput]) -> DAffine2 {
} else {
DVec2::ZERO
};
let angle = if let Some(&TaggedValue::F64(angle)) = inputs[2].as_value() { angle } else { 0. };
let rotation = if let Some(&TaggedValue::F64(rotation)) = inputs[2].as_value() { rotation } else { 0. };
let scale = if let Some(&TaggedValue::DVec2(scale)) = inputs[3].as_value() { scale } else { DVec2::ONE };
let shear = if let Some(&TaggedValue::DVec2(shear)) = inputs[4].as_value() { shear } else { DVec2::ZERO };
DAffine2::from_scale_angle_translation(scale, angle, translation) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.])
let rotation = rotation.to_radians();
let shear = DVec2::new(shear.x.to_radians().tan(), shear.y.to_radians().tan());
DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.])
}
/// Extract the current normalized pivot from the layer
@@ -1351,31 +1351,52 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
inputs: vec![
// Value
NodeInput::value(TaggedValue::DAffine2(DAffine2::default()), true),
// Translation
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
// Rotation
NodeInput::value(TaggedValue::F64(0.), false),
// Scale
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
// Skew
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
// Origin Offset
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
// Scale Appearance
NodeInput::value(TaggedValue::Bool(true), false),
],
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
exports: vec![
// From the Transform node
NodeInput::node(NodeId(1), 0),
],
nodes: [
// Monitor node
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
inputs: vec![
// From the Value import
NodeInput::network(generic!(T), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
call_argument: generic!(T),
skip_deduplication: true,
..Default::default()
},
// Transform node
DocumentNode {
inputs: vec![
// From the Monitor node
NodeInput::node(NodeId(0), 0),
// From the Translation import
NodeInput::network(concrete!(DVec2), 1),
// From the Rotation import
NodeInput::network(concrete!(f64), 2),
// From the Scale import
NodeInput::network(concrete!(DVec2), 3),
// From the Skew import
NodeInput::network(concrete!(DVec2), 4),
],
call_argument: concrete!(Context),
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform::IDENTIFIER),
..Default::default()
},
@@ -1441,6 +1462,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}),
),
InputMetadata::with_name_description_override("Skew", "TODO", WidgetOverride::Custom("transform_skew".to_string())),
InputMetadata::with_name_description_override("Origin Offset", "TODO", WidgetOverride::Custom("hidden".to_string())),
InputMetadata::with_name_description_override("Scale Appearance", "TODO", WidgetOverride::Custom("hidden".to_string())),
],
output_names: vec!["Data".to_string()],
..Default::default()
@@ -2190,13 +2213,13 @@ fn static_input_properties() -> InputProperties {
if let Some(&TaggedValue::F64(val)) = input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(val.to_degrees()))
NumberInput::new(Some(val))
.unit("°")
.mode(NumberInputMode::Range)
.range_min(Some(-180.))
.range_max(Some(180.))
.on_update(node_properties::update_value(
|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()),
|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap()),
node_id,
index,
))
@@ -2219,29 +2242,28 @@ fn static_input_properties() -> InputProperties {
return Err("Input not found in transform skew input override".to_string());
};
if let Some(&TaggedValue::DVec2(val)) = input.as_non_exposed_value() {
let to_skew = |input: &NumberInput| input.value.unwrap().to_radians().tan();
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(val.x.atan().to_degrees()))
NumberInput::new(Some(val.x))
.label("X")
.unit("°")
.min(-89.9)
.max(89.9)
.on_update(node_properties::update_value(
move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(to_skew(input), val.y)),
move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), val.y)),
node_id,
index,
))
.on_commit(node_properties::commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(val.y.atan().to_degrees()))
NumberInput::new(Some(val.y))
.label("Y")
.unit("°")
.min(-89.9)
.max(89.9)
.on_update(node_properties::update_value(
move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(val.x, to_skew(input))),
move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(val.x, input.value.unwrap())),
node_id,
index,
))
@@ -1232,6 +1232,17 @@ impl NodeNetworkInterface {
Some(&metadata.transient_metadata)
}
pub fn set_input_override(&mut self, node_id: &NodeId, index: usize, widget_override: Option<String>, network_path: &[NodeId]) {
let Some(metadata) = self
.node_metadata_mut(node_id, network_path)
.and_then(|node_metadata| node_metadata.persistent_metadata.input_metadata.get_mut(index))
else {
log::error!("Could not get input metadata for {node_id} index {index} in set_input_override");
return;
};
metadata.persistent_metadata.widget_override = widget_override;
}
/// Returns the input name to display in the properties panel. If the name is empty then the type is used.
pub fn displayed_input_name_and_description(&mut self, node_id: &NodeId, input_index: usize, network_path: &[NodeId]) -> (String, String) {
let Some(input_metadata) = self.persistent_input_metadata(node_id, input_index, network_path) else {