diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index ead30891b..b3ceab826 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -722,14 +722,22 @@ impl PropertyEditable for Wipeout { let u = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z); let v = glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z); let normal = u.cross(v).normalize_or_zero(); - if normal.length_squared() <= 1e-18 { + if normal.length_squared() <= 1e-18 || normal.z.abs() <= 1e-12 { return; } - let current_radians = u.y.atan2(u.x); - let rotation = glam::DQuat::from_axis_angle( - normal, - target_degrees.to_radians() - current_radians, - ); + let target_radians = target_degrees.to_radians(); + let horizontal = glam::DVec3::new(target_radians.cos(), target_radians.sin(), 0.0); + let target = (normal.z * horizontal - normal.dot(horizontal) * glam::DVec3::Z) + * normal.z.signum(); + let direction = u.normalize_or_zero(); + let target = target.normalize_or_zero(); + if direction == glam::DVec3::ZERO || target == glam::DVec3::ZERO { + return; + } + let angle = normal + .dot(direction.cross(target)) + .atan2(direction.dot(target)); + let rotation = glam::DQuat::from_axis_angle(normal, angle); let u = rotation * u; let v = rotation * v; self.u_vector = acadrust::types::Vector3::new(u.x, u.y, u.z); @@ -786,3 +794,33 @@ impl Transformable for Wipeout { }); } } + +#[cfg(test)] +mod wipeout_property_tests { + use super::*; + + #[test] + fn rotation_reaches_the_requested_angle_for_mirrored_and_tilted_frames() { + for (u, v, target) in [ + ([1.0, 0.0, 0.0], [0.0, -1.0, 0.0], 90.0), + ([1.0, 0.0, 1.0], [0.0, 1.0, 0.0], 45.0), + ] { + let mut wipeout = Wipeout::new(); + wipeout.u_vector = acadrust::types::Vector3::new(u[0], u[1], u[2]); + wipeout.v_vector = acadrust::types::Vector3::new(v[0], v[1], v[2]); + let lengths = (wipeout.u_vector.length(), wipeout.v_vector.length()); + + wipeout.apply_geom_prop("wo_rotation", &target.to_string()); + + let angle = wipeout + .u_vector + .y + .atan2(wipeout.u_vector.x) + .to_degrees() + .rem_euclid(360.0); + assert!((angle - target).abs() < 1e-9, "target={target} actual={angle}"); + assert!((wipeout.u_vector.length() - lengths.0).abs() < 1e-12); + assert!((wipeout.v_vector.length() - lengths.1).abs() < 1e-12); + } + } +} diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index d71c15d58..e58ebd8fb 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -1495,6 +1495,19 @@ mod block_wire_storage_tests { assert_eq!(names(&packed), names(&storage)); } + #[test] + fn both_block_wire_shaders_honor_fixed_screen_width() { + for source in [ + include_str!("../../shaders/block_wire.wgsl"), + include_str!("../../shaders/block_wire_storage.wgsl"), + ] { + assert!(source.contains("if world_hw < 0.0")); + assert!(source.contains( + "resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width)" + )); + } + } + #[test] fn wire_shader_validates_with_naga() { let source = include_str!("../../shaders/wire.wgsl"); diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index ba3adb899..55963405f 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -69,6 +69,7 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); } + if world_hw < 0.0 { return max(-world_hw, 0.5); } var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; @@ -185,7 +186,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut let perpendicular = vec2(-direction.y, direction.x); let clip_position = mix(clip_a, clip_b, which_end); - let half_width = resolve_hw(0.0, 0.0, wire_const.half_width); + let half_width = resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width); let extension = which_end * 2.0 - 1.0; let offset_px = perpendicular * half_width * side + direction * half_width * extension; diff --git a/src/shaders/block_wire_storage.wgsl b/src/shaders/block_wire_storage.wgsl index ea952bca2..f168504e6 100644 --- a/src/shaders/block_wire_storage.wgsl +++ b/src/shaders/block_wire_storage.wgsl @@ -93,6 +93,7 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); } + if world_hw < 0.0 { return max(-world_hw, 0.5); } var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; @@ -216,7 +217,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut let perpendicular = vec2(-direction.y, direction.x); let clip_position = mix(clip_a, clip_b, which_end); - let half_width = resolve_hw(0.0, 0.0, wire_const.half_width); + let half_width = resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width); let extension = which_end * 2.0 - 1.0; let offset_px = perpendicular * half_width * side + direction * half_width * extension;