Skip to content

Commit a3ea6ab

Browse files
authored
Refactor transform decomposition API with skew support, add 'Decompose Skew' node, and fix stroke transform interpolation (#3973)
* Refactor transform decomposition API with skew support, add Decompose Skew node, and fix stroke transform interpolation * Fix bug in master with skew changing Area node calculated value * Code review simplification * More code review fixes * Rename cases where "shear" terminology was used in place of "skew"
1 parent e2a1423 commit a3ea6ab

File tree

15 files changed

+160
-70
lines changed

15 files changed

+160
-70
lines changed

editor/src/messages/portfolio/document/graph_operation/transform_utils.rs

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,21 @@ use glam::{DAffine2, DVec2};
33
use graph_craft::document::value::TaggedValue;
44
use graph_craft::document::{NodeId, NodeInput};
55
use graphene_std::subpath::Subpath;
6+
use graphene_std::transform::Transform;
67
use graphene_std::vector::PointId;
78

8-
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
9-
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
10-
let x_axis = transform.matrix2.x_axis;
11-
let y_axis = transform.matrix2.y_axis;
12-
13-
// Assuming there is no vertical shear
14-
let angle = x_axis.y.atan2(x_axis.x);
15-
let (sin, cos) = angle.sin_cos();
16-
let scale_x = if cos.abs() > 1e-10 { x_axis.x / cos } else { x_axis.y / sin };
17-
18-
let mut shear_x = (sin * y_axis.y + cos * y_axis.x) / (sin * sin * scale_x + cos * cos * scale_x);
19-
if !shear_x.is_finite() {
20-
shear_x = 0.;
21-
}
22-
let scale_y = if cos.abs() > 1e-10 {
23-
(y_axis.y - scale_x * sin * shear_x) / cos
24-
} else {
25-
(scale_x * cos * shear_x - y_axis.x) / sin
26-
};
27-
let translation = transform.translation;
28-
let scale = DVec2::new(scale_x, scale_y);
29-
let shear = DVec2::new(shear_x, 0.);
30-
(scale, angle, translation, shear)
31-
}
32-
339
/// Update the inputs of the transform node to match a new transform
3410
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
35-
let (scale, rotation, translation, shear) = compute_scale_angle_translation_shear(transform);
11+
let (rotation, scale, skew) = transform.decompose_rotation_scale_skew();
12+
let translation = transform.translation;
3613

3714
let rotation = rotation.to_degrees();
38-
let shear = DVec2::new(shear.x.atan().to_degrees(), shear.y.atan().to_degrees());
15+
let skew = DVec2::new(skew.atan().to_degrees(), 0.);
3916

4017
network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(translation), false), &[]);
4118
network_interface.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(rotation), false), &[]);
4219
network_interface.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::DVec2(scale), false), &[]);
43-
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(shear), false), &[]);
20+
network_interface.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::DVec2(skew), false), &[]);
4421
}
4522

4623
// TODO: This should be extracted from the graph at the location of the transform node.
@@ -81,12 +58,12 @@ pub fn get_current_transform(inputs: &[NodeInput]) -> DAffine2 {
8158
};
8259
let rotation = if let Some(&TaggedValue::F64(rotation)) = inputs[2].as_value() { rotation } else { 0. };
8360
let scale = if let Some(&TaggedValue::DVec2(scale)) = inputs[3].as_value() { scale } else { DVec2::ONE };
84-
let shear = if let Some(&TaggedValue::DVec2(shear)) = inputs[4].as_value() { shear } else { DVec2::ZERO };
61+
let skew = if let Some(&TaggedValue::DVec2(skew)) = inputs[4].as_value() { skew } else { DVec2::ZERO };
8562

8663
let rotation = rotation.to_radians();
87-
let shear = DVec2::new(shear.x.to_radians().tan(), shear.y.to_radians().tan());
64+
let skew = DVec2::new(skew.x.to_radians().tan(), skew.y.to_radians().tan());
8865

89-
DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.])
66+
DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.])
9067
}
9168

9269
/// Extract the current normalized pivot from the layer
@@ -135,31 +112,32 @@ mod tests {
135112
/// ```
136113
#[test]
137114
fn derive_transform() {
138-
for shear_x in -10..=10 {
139-
let shear_x = (shear_x as f64) / 2.;
115+
for skew_x in -10..=10 {
116+
let skew_x = (skew_x as f64) / 2.;
140117
for angle in (0..=360).step_by(15) {
141118
let angle = (angle as f64).to_radians();
142119
for scale_x in 1..10 {
143120
let scale_x = (scale_x as f64) / 5.;
144121
for scale_y in 1..10 {
145122
let scale_y = (scale_y as f64) / 5.;
146123

147-
let shear = DVec2::new(shear_x, 0.);
124+
let skew = DVec2::new(skew_x, 0.);
148125
let scale = DVec2::new(scale_x, scale_y);
149126
let translate = DVec2::new(5666., 644.);
150127

151128
let original_transform = DAffine2::from_cols(
152-
DVec2::new(scale.x * angle.cos() - scale.y * angle.sin() * shear.y, scale.x * angle.sin() + scale.y * angle.cos() * shear.y),
153-
DVec2::new(scale.x * angle.cos() * shear.x - scale.y * angle.sin(), scale.x * angle.sin() * shear.x + scale.y * angle.cos()),
129+
DVec2::new(scale.x * angle.cos() - scale.y * angle.sin() * skew.y, scale.x * angle.sin() + scale.y * angle.cos() * skew.y),
130+
DVec2::new(scale.x * angle.cos() * skew.x - scale.y * angle.sin(), scale.x * angle.sin() * skew.x + scale.y * angle.cos()),
154131
translate,
155132
);
156133

157-
let (new_scale, new_angle, new_translation, new_shear) = compute_scale_angle_translation_shear(original_transform);
158-
let new_transform = DAffine2::from_scale_angle_translation(new_scale, new_angle, new_translation) * DAffine2::from_cols_array(&[1., new_shear.y, new_shear.x, 1., 0., 0.]);
134+
let (new_angle, new_scale, new_skew) = original_transform.decompose_rotation_scale_skew();
135+
let new_translation = original_transform.translation;
136+
let new_transform = DAffine2::from_scale_angle_translation(new_scale, new_angle, new_translation) * DAffine2::from_cols_array(&[1., 0., new_skew, 1., 0., 0.]);
159137

160138
assert!(
161139
new_transform.abs_diff_eq(original_transform, 1e-10),
162-
"original_transform {original_transform} new_transform {new_transform} / scale {scale} new_scale {new_scale} / angle {angle} new_angle {new_angle} / shear {shear} / new_shear {new_shear}",
140+
"original_transform {original_transform} new_transform {new_transform} / scale {scale} new_scale {new_scale} / angle {angle} new_angle {new_angle} / skew {skew} / new_skew {new_skew}",
163141
);
164142
}
165143
}

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use graphene_std::raster::{
2323
};
2424
use graphene_std::table::{Table, TableRow};
2525
use graphene_std::text::{Font, TextAlign};
26-
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
26+
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
2727
use graphene_std::vector::QRCodeErrorCorrectionLevel;
2828
use graphene_std::vector::misc::BooleanOperation;
2929
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
@@ -265,6 +265,7 @@ pub(crate) fn property_from_type(
265265
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
266266
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
267267
Some(x) if x == TypeId::of::<QRCodeErrorCorrectionLevel>() => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
268+
Some(x) if x == TypeId::of::<ScaleType>() => enum_choice::<ScaleType>().for_socket(default_info).property_row(),
268269
// =====
269270
// OTHER
270271
// =====
@@ -566,8 +567,8 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
566567

567568
let widgets = if let Some(&TaggedValue::DAffine2(transform)) = input.as_non_exposed_value() {
568569
let translation = transform.translation;
569-
let rotation = transform.decompose_rotation();
570-
let scale = transform.decompose_scale();
570+
let (rotation, scale, skew) = transform.decompose_rotation_scale_skew();
571+
let skew_matrix = DAffine2::from_cols_array(&[1., 0., skew, 1., 0., 0.]);
571572

572573
location_widgets.extend_from_slice(&[
573574
NumberInput::new(Some(translation.x))
@@ -608,7 +609,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
608609
.range_max(Some(180.))
609610
.on_update(update_value(
610611
move |r: &NumberInput| {
611-
let transform = DAffine2::from_scale_angle_translation(scale, r.value.map(|r| r.to_radians()).unwrap_or(rotation), translation);
612+
let transform = DAffine2::from_scale_angle_translation(scale, r.value.map(|r| r.to_radians()).unwrap_or(rotation), translation) * skew_matrix;
612613
TaggedValue::DAffine2(transform)
613614
},
614615
node_id,
@@ -623,7 +624,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
623624
.unit("x")
624625
.on_update(update_value(
625626
move |w: &NumberInput| {
626-
let transform = DAffine2::from_scale_angle_translation(DVec2::new(w.value.unwrap_or(scale.x), scale.y), rotation, translation);
627+
let transform = DAffine2::from_scale_angle_translation(DVec2::new(w.value.unwrap_or(scale.x), scale.y), rotation, translation) * skew_matrix;
627628
TaggedValue::DAffine2(transform)
628629
},
629630
node_id,
@@ -637,7 +638,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
637638
.unit("x")
638639
.on_update(update_value(
639640
move |h: &NumberInput| {
640-
let transform = DAffine2::from_scale_angle_translation(DVec2::new(scale.x, h.value.unwrap_or(scale.y)), rotation, translation);
641+
let transform = DAffine2::from_scale_angle_translation(DVec2::new(scale.x, h.value.unwrap_or(scale.y)), rotation, translation) * skew_matrix;
641642
TaggedValue::DAffine2(transform)
642643
},
643644
node_id,

editor/src/messages/portfolio/document_migration.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use graphene_std::ProtoNodeIdentifier;
1212
use graphene_std::subpath::Subpath;
1313
use graphene_std::table::Table;
1414
use graphene_std::text::{TextAlign, TypesettingConfig};
15+
use graphene_std::transform::ScaleType;
1516
use graphene_std::uuid::NodeId;
1617
use graphene_std::vector::Vector;
1718
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
@@ -1848,6 +1849,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
18481849
document.network_interface.set_context_features(node_id, network_path, context_features);
18491850
}
18501851

1852+
// Add the "Scale Type" parameter to the "Decompose Scale" node
1853+
if reference == DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::decompose_scale::IDENTIFIER) && inputs_count == 1 {
1854+
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
1855+
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
1856+
1857+
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
1858+
document
1859+
.network_interface
1860+
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::ScaleType(ScaleType::Magnitude), false), network_path);
1861+
}
1862+
18511863
// ==================================
18521864
// PUT ALL MIGRATIONS ABOVE THIS LINE
18531865
// ==================================

node-graph/graph-craft/src/document/value.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ tagged_value! {
272272
CentroidType(vector::misc::CentroidType),
273273
BooleanOperation(vector::misc::BooleanOperation),
274274
TextAlign(text_nodes::TextAlign),
275+
ScaleType(core_types::transform::ScaleType),
275276
}
276277

277278
impl TaggedValue {

node-graph/interpreted-executor/src/node_registry.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
134134
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
135135
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
136136
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
137+
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
137138
// Context nullification
138139
#[cfg(feature = "gpu")]
139140
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi, Context => graphene_std::ContextFeatures]),
@@ -227,6 +228,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
227228
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
228229
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
229230
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
231+
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
230232
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
231233
];
232234
// =============

node-graph/libraries/core-types/src/transform.rs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,90 @@
11
use crate::math::bbox::AxisAlignedBbox;
22
use core::f64;
3+
use dyn_any::DynAny;
34
use glam::{DAffine2, DMat2, DVec2, UVec2};
45

6+
/// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors.
7+
#[repr(C)]
8+
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
9+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
10+
#[widget(Radio)]
11+
pub enum ScaleType {
12+
/// The visual length of each axis (always positive, includes any skew contribution).
13+
#[default]
14+
Magnitude,
15+
/// The isolated scale factors with rotation and skew stripped away (can be negative for flipped axes).
16+
Pure,
17+
}
18+
519
pub trait Transform {
620
fn transform(&self) -> DAffine2;
721

822
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
923
pivot
1024
}
1125

12-
fn decompose_scale(&self) -> DVec2 {
13-
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
26+
/// Decomposes the full transform into `(rotation, signed_scale, skew)` using a TRS+Skew factorization.
27+
///
28+
/// - `rotation`: angle in radians
29+
/// - `signed_scale`: the algebraic scale factors (can be negative for reflections, excludes skew)
30+
/// - `skew`: the horizontal shear coefficient (the raw matrix value, not an angle)
31+
///
32+
/// The original transform can be reconstructed as:
33+
/// ```
34+
/// DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., 0., skew, 1., 0., 0.])
35+
/// ```
36+
#[inline(always)]
37+
fn decompose_rotation_scale_skew(&self) -> (f64, DVec2, f64) {
38+
let t = self.transform();
39+
let x_axis = t.matrix2.x_axis;
40+
let y_axis = t.matrix2.y_axis;
41+
42+
let angle = x_axis.y.atan2(x_axis.x);
43+
let (sin, cos) = angle.sin_cos();
44+
45+
let scale_x = if cos.abs() > 1e-10 { x_axis.x / cos } else { x_axis.y / sin };
46+
47+
let mut skew = (sin * y_axis.y + cos * y_axis.x) / scale_x;
48+
if !skew.is_finite() {
49+
skew = 0.;
50+
}
51+
52+
let scale_y = if cos.abs() > 1e-10 {
53+
(y_axis.y - scale_x * sin * skew) / cos
54+
} else {
55+
(scale_x * cos * skew - y_axis.x) / sin
56+
};
57+
58+
(angle, DVec2::new(scale_x, scale_y), skew)
1459
}
1560

16-
/// Requires that the transform does not contain any skew.
61+
/// Extracts the rotation angle (in radians) from the transform.
62+
/// This is the angle of the x-axis and is correct regardless of skew, negative scale, or non-uniform scale.
1763
fn decompose_rotation(&self) -> f64 {
18-
let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2;
19-
let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X);
64+
let x_axis = self.transform().matrix2.x_axis;
65+
let rotation = x_axis.y.atan2(x_axis.x);
2066
if rotation == -0. { 0. } else { rotation }
2167
}
2268

69+
/// Returns the signed scale components from the TRS+Skew decomposition.
70+
/// Unlike [`Self::scale_magnitudes`] which returns positive axis-length magnitudes,
71+
/// this returns the algebraic scale factors which can be negative for reflections and exclude skew.
72+
fn decompose_scale(&self) -> DVec2 {
73+
self.decompose_rotation_scale_skew().1
74+
}
75+
76+
/// Returns the unsigned scale as the lengths of each axis (always positive, includes skew contribution).
77+
/// Use this for magnitude-based queries like stroke width scaling, zoom level, or bounding box inflation.
78+
fn scale_magnitudes(&self) -> DVec2 {
79+
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
80+
}
81+
82+
/// Returns the horizontal skew (shear) coefficient from the TRS+Skew decomposition.
83+
/// This is the raw matrix coefficient. To convert to degrees: `skew.atan().to_degrees()`.
84+
fn decompose_skew(&self) -> f64 {
85+
self.decompose_rotation_scale_skew().2
86+
}
87+
2388
/// Detects if the transform contains skew by checking if the transformation matrix
2489
/// deviates from a pure rotation + uniform scale + translation.
2590
///
@@ -135,7 +200,7 @@ impl Footprint {
135200
}
136201

137202
pub fn scale(&self) -> DVec2 {
138-
self.transform.decompose_scale()
203+
self.transform.scale_magnitudes()
139204
}
140205

141206
pub fn offset(&self) -> DVec2 {

node-graph/libraries/vector-types/src/vector/click_target.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ impl ClickTarget {
179179

180180
// Decompose transform into rotation, scale, translation for caching strategy
181181
let rotation = transform.decompose_rotation();
182-
let scale = transform.decompose_scale();
182+
let scale = transform.scale_magnitudes();
183183
let translation = transform.translation;
184184

185185
// Generate fingerprint for cache lookup

node-graph/libraries/vector-types/src/vector/style.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ pub use crate::gradient::*;
44
use core_types::Color;
55
use core_types::color::Alpha;
66
use core_types::table::Table;
7+
use core_types::transform::Transform;
78
use dyn_any::DynAny;
89
use glam::DAffine2;
10+
use std::f64::consts::{PI, TAU};
911

1012
/// Describes the fill of a layer.
1113
///
@@ -364,10 +366,30 @@ impl Stroke {
364366
join: if time < 0.5 { self.join } else { other.join },
365367
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time,
366368
align: if time < 0.5 { self.align } else { other.align },
367-
transform: DAffine2::from_mat2_translation(
368-
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
369-
self.transform.translation * time + other.transform.translation * (1. - time),
370-
),
369+
transform: {
370+
// Decompose into scale/rotation/skew and interpolate each component separately.
371+
// We do this instead of linear matrix interpolation because that passes through a zero matrix
372+
// (and thus a division by 0 when rendering) when transforms have opposing rotations (e.g. 0° vs 180°).
373+
374+
let (s_angle, s_scale, s_skew) = self.transform.decompose_rotation_scale_skew();
375+
let (t_angle, t_scale, t_skew) = other.transform.decompose_rotation_scale_skew();
376+
377+
let lerp = |a: f64, b: f64| a + (b - a) * time;
378+
let lerped_translation = self.transform.translation * (1. - time) + other.transform.translation * time;
379+
380+
// Shortest-arc rotation interpolation
381+
let mut rotation_diff = t_angle - s_angle;
382+
if rotation_diff > PI {
383+
rotation_diff -= TAU;
384+
} else if rotation_diff < -PI {
385+
rotation_diff += TAU;
386+
}
387+
let lerped_angle = s_angle + rotation_diff * time;
388+
389+
let trs = DAffine2::from_scale_angle_translation(s_scale.lerp(t_scale, time), lerped_angle, lerped_translation);
390+
let skew = DAffine2::from_cols_array(&[1., 0., lerp(s_skew, t_skew), 1., 0., 0.]);
391+
trs * skew
392+
},
371393
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
372394
}
373395
}

node-graph/libraries/vector-types/src/vector/vector_types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ impl<Upstream> BoundingBox for Vector<Upstream> {
483483
// Include stroke by adding offset based on stroke width
484484
let stroke_width = self.style.stroke().map(|s| s.weight()).unwrap_or_default();
485485
let miter_limit = self.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
486-
let scale = transform.decompose_scale();
486+
let scale = transform.scale_magnitudes();
487487

488488
// Use the full line width to account for different styles of stroke caps
489489
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);

node-graph/nodes/gstd/src/pixel_preview.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub async fn pixel_preview<'a: 'n>(
2222
let physical_scale = render_params.scale;
2323

2424
let footprint = *ctx.footprint();
25-
let viewport_zoom = footprint.decompose_scale().x * physical_scale;
25+
let viewport_zoom = footprint.scale_magnitudes().x * physical_scale;
2626

2727
if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. {
2828
let context = OwnedContextImpl::from(ctx).into_context();

0 commit comments

Comments
 (0)