Skip to content

Commit 157f99a

Browse files
Add bidirectional scaling to the Gizmo
Allows scaling a single side at once, as opposed to scaling in both directions. Implementation is a bit hacky. To avoid making the gizmo sad, we need three of 'em, 'cause of coordinate handedness.
1 parent e14aa42 commit 157f99a

File tree

5 files changed

+175
-63
lines changed

5 files changed

+175
-63
lines changed

crates/alkahest-renderer/src/ecs/transform.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ bitflags! {
119119
const IGNORE_SCALE = 1 << 2;
120120

121121
const SCALE_IS_RADIUS = 1 << 3;
122+
const SCALE_IS_BIDIRECTIONAL = 1 << 4;
122123
}
123124
}
124125

crates/alkahest-renderer/src/ecs/utility.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use super::{
1414
common::{Icon, Label, Mutable, RenderCommonBundle},
1515
route::{Route, RouteNode},
1616
tags::{EntityTag, NodeFilter, Tags},
17+
transform::TransformFlags,
1718
visibility::VisibilityHelper,
1819
MapInfo,
1920
};
@@ -152,7 +153,8 @@ pub struct CuboidBundle {
152153
}
153154

154155
impl CuboidBundle {
155-
pub fn new(transform: Transform, cuboid: Cuboid) -> Self {
156+
pub fn new(mut transform: Transform, cuboid: Cuboid) -> Self {
157+
transform.flags |= TransformFlags::SCALE_IS_BIDIRECTIONAL;
156158
Self {
157159
transform,
158160
cuboid,

crates/alkahest/src/app.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use bevy_ecs::system::RunSystemOnce;
1919
use bevy_tasks::{ComputeTaskPool, TaskPool};
2020
use egui::{Key, KeyboardShortcut, Modifiers};
2121
use gilrs::{EventType, Gilrs};
22-
use glam::Vec2;
22+
use glam::{Vec2, Vec3};
2323
use strum::IntoEnumIterator;
24-
use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoOrientation};
24+
use transform_gizmo_egui::{enum_set, EnumSet, Gizmo, GizmoConfig, GizmoMode, GizmoOrientation};
2525
use windows::core::HRESULT;
2626
use winit::{
2727
dpi::{PhysicalPosition, PhysicalSize},
@@ -36,7 +36,7 @@ use crate::{
3636
activity_select::{get_map_name, set_activity, ActivityBrowser, CurrentActivity},
3737
console,
3838
context::{GuiContext, GuiViewManager, HiddenWindows},
39-
gizmo::draw_transform_gizmos,
39+
gizmo::{draw_transform_gizmos, GizmoInfo},
4040
hotkeys,
4141
updater::{ChannelSelector, UpdateDownload},
4242
SelectionGizmoMode,
@@ -129,12 +129,35 @@ impl AlkahestApp {
129129
let stringmap = Arc::new(StringContainer::load_all_global());
130130
resources.insert(stringmap);
131131

132-
let gizmo = Gizmo::new(GizmoConfig {
133-
modes: EnumSet::all(),
134-
orientation: GizmoOrientation::Local,
135-
..Default::default()
136-
});
137-
resources.insert(gizmo);
132+
let gizmos = vec![
133+
GizmoInfo {
134+
gizmo: Gizmo::new(GizmoConfig {
135+
modes: EnumSet::all(),
136+
orientation: GizmoOrientation::Local,
137+
..Default::default()
138+
}),
139+
..Default::default()
140+
},
141+
GizmoInfo {
142+
gizmo: Gizmo::new(GizmoConfig {
143+
modes: EnumSet::all(),
144+
orientation: GizmoOrientation::Local,
145+
..Default::default()
146+
}),
147+
mode_filter: Some(GizmoMode::ScaleX | GizmoMode::ScaleY),
148+
rotation_axis: Some(Vec3::Z),
149+
},
150+
GizmoInfo {
151+
gizmo: Gizmo::new(GizmoConfig {
152+
modes: EnumSet::all(),
153+
orientation: GizmoOrientation::Local,
154+
..Default::default()
155+
}),
156+
mode_filter: Some(enum_set!(GizmoMode::ScaleZ)),
157+
rotation_axis: Some(Vec3::X),
158+
},
159+
];
160+
resources.insert(gizmos);
138161

139162
resources
140163
.get_mut::<GuiViewManager>()

crates/alkahest/src/gui/gizmo.rs

Lines changed: 134 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
use std::f32::consts::PI;
2+
13
use alkahest_renderer::{
24
camera::Camera,
3-
ecs::{resources::SelectedEntity, transform::Transform},
5+
ecs::{
6+
resources::SelectedEntity,
7+
transform::{Transform, TransformFlags},
8+
},
49
icons::{ICON_AXIS_ARROW, ICON_CURSOR_DEFAULT, ICON_RESIZE, ICON_ROTATE_ORBIT},
510
renderer::Renderer,
611
resources::AppResources,
@@ -9,9 +14,10 @@ use egui::{
914
epaint::Vertex, Context, LayerId, Mesh, PointerButton, Pos2, Rgba, RichText, Rounding, Ui,
1015
UiStackInfo,
1116
};
12-
use glam::{DQuat, DVec3};
17+
use glam::{DQuat, DVec3, Quat, Vec3};
1318
use transform_gizmo_egui::{
14-
math::Transform as GTransform, Gizmo, GizmoConfig, GizmoInteraction, GizmoResult,
19+
math::Transform as GTransform, EnumSet, Gizmo, GizmoConfig, GizmoInteraction, GizmoMode,
20+
GizmoResult,
1521
};
1622
use winit::window::Window;
1723

@@ -111,6 +117,13 @@ impl GuiView for GizmoSelector {
111117
}
112118
}
113119

120+
#[derive(Default)]
121+
pub struct GizmoInfo {
122+
pub gizmo: Gizmo,
123+
pub mode_filter: Option<EnumSet<GizmoMode>>,
124+
pub rotation_axis: Option<Vec3>,
125+
}
126+
114127
pub fn draw_transform_gizmos(renderer: &Renderer, ctx: &egui::Context, resources: &AppResources) {
115128
let Some(selected) = resources.get::<SelectedEntity>().selected() else {
116129
return;
@@ -134,26 +147,69 @@ pub fn draw_transform_gizmos(renderer: &Renderer, ctx: &egui::Context, resources
134147
};
135148
let camera = resources.get::<Camera>();
136149

137-
let mut gizmo = resources.get_mut::<Gizmo>();
138-
let old_config = *gizmo.config();
139-
gizmo.update_config(GizmoConfig {
140-
view_matrix: camera.world_to_camera.as_dmat4().into(),
141-
projection_matrix: camera.camera_to_projective.as_dmat4().into(),
142-
modes: gizmo_mode.to_enumset(),
143-
..old_config
144-
});
150+
let mut gizmo_info = resources.get_mut::<Vec<GizmoInfo>>();
145151

146-
if let Some((_result, new_transform)) = gizmo_interact(
147-
&mut gizmo,
152+
for (i, info) in gizmo_info.iter_mut().enumerate() {
153+
let old_config = *info.gizmo.config();
154+
155+
if transform
156+
.flags
157+
.contains(TransformFlags::SCALE_IS_BIDIRECTIONAL)
158+
|| i == 1
159+
{
160+
info.gizmo.update_config(GizmoConfig {
161+
view_matrix: camera.world_to_camera.as_dmat4().into(),
162+
projection_matrix: camera.camera_to_projective.as_dmat4().into(),
163+
modes: match info.mode_filter {
164+
Some(filter) => gizmo_mode.to_enumset().intersection(filter),
165+
None => gizmo_mode.to_enumset(),
166+
},
167+
..old_config
168+
});
169+
} else {
170+
info.gizmo.update_config(GizmoConfig {
171+
view_matrix: camera.world_to_camera.as_dmat4().into(),
172+
projection_matrix: camera.camera_to_projective.as_dmat4().into(),
173+
modes: EnumSet::empty(),
174+
..old_config
175+
});
176+
}
177+
}
178+
179+
let end = if transform
180+
.flags
181+
.contains(TransformFlags::SCALE_IS_BIDIRECTIONAL)
182+
{
183+
gizmo_info.len()
184+
} else {
185+
gizmo_info.len().min(1)
186+
};
187+
188+
if let Some((_result, new_transform, i)) = gizmo_interact(
189+
&mut gizmo_info[..end],
148190
ctx,
149191
&[GTransform {
150192
scale: transform.scale.as_dvec3().into(),
151193
rotation: transform.rotation.as_dquat().into(),
152194
translation: transform.translation.as_dvec3().into(),
153195
}],
196+
&mut [GTransform::default()],
154197
) {
155198
renderer.pickbuffer.cancel_request();
156-
transform.translation = DVec3::from(new_transform[0].translation).as_vec3();
199+
200+
transform.translation = DVec3::from(new_transform[0].translation).as_vec3()
201+
+ if transform
202+
.flags
203+
.contains(TransformFlags::SCALE_IS_BIDIRECTIONAL)
204+
{
205+
if i == 0 {
206+
DVec3::from(new_transform[0].scale).as_vec3() - transform.scale
207+
} else {
208+
transform.scale - DVec3::from(new_transform[0].scale).as_vec3()
209+
}
210+
} else {
211+
Vec3::ZERO
212+
};
157213
transform.rotation = DQuat::from(new_transform[0].rotation).as_quat().normalize();
158214
transform.scale = DVec3::from(new_transform[0].scale).as_vec3();
159215
}
@@ -162,52 +218,77 @@ pub fn draw_transform_gizmos(renderer: &Renderer, ctx: &egui::Context, resources
162218

163219
#[must_use]
164220
fn gizmo_interact(
165-
gizmo: &mut Gizmo,
221+
gizmo_info: &mut [GizmoInfo],
166222
ctx: &egui::Context,
167223
targets: &[GTransform],
168-
) -> Option<(GizmoResult, Vec<GTransform>)> {
224+
targets_scratch: &mut [GTransform],
225+
) -> Option<(GizmoResult, Vec<GTransform>, usize)> {
169226
let cursor_pos = ctx
170227
.input(|input| input.pointer.hover_pos())
171228
.unwrap_or_default();
172229

173-
let mut viewport = gizmo.config().viewport;
174-
if !viewport.is_finite() {
175-
viewport = ctx.screen_rect();
230+
let mut active_result = None;
231+
for (i, info) in gizmo_info.iter_mut().enumerate() {
232+
let mut viewport = info.gizmo.config().viewport;
233+
if !viewport.is_finite() {
234+
viewport = ctx.screen_rect();
235+
}
236+
info.gizmo.update_config(GizmoConfig {
237+
viewport,
238+
pixels_per_point: ctx.pixels_per_point(),
239+
..*info.gizmo.config()
240+
});
241+
if active_result.is_none() {
242+
for (target, target_scratch) in targets.iter().zip(targets_scratch.iter_mut()) {
243+
target_scratch.rotation = if let Some(axis) = info.rotation_axis {
244+
DQuat::from(target.rotation)
245+
.as_quat()
246+
.mul_quat(Quat::from_axis_angle(axis, PI))
247+
.as_dquat()
248+
.into()
249+
} else {
250+
target.rotation
251+
};
252+
target_scratch.translation = target.translation;
253+
target_scratch.scale = target.scale;
254+
}
255+
let gizmo_result = info.gizmo.update(
256+
GizmoInteraction {
257+
cursor_pos: (cursor_pos.x, cursor_pos.y),
258+
drag_started: ctx
259+
.input(|input| input.pointer.button_pressed(PointerButton::Primary)),
260+
dragging: ctx.input(|input| input.pointer.button_down(PointerButton::Primary)),
261+
},
262+
targets_scratch,
263+
);
264+
active_result = gizmo_result.map(|(g, v)| (g, v, i));
265+
}
176266
}
177267

178-
gizmo.update_config(GizmoConfig {
179-
viewport,
180-
pixels_per_point: ctx.pixels_per_point(),
181-
..*gizmo.config()
182-
});
183-
184-
let gizmo_result = gizmo.update(
185-
GizmoInteraction {
186-
cursor_pos: (cursor_pos.x, cursor_pos.y),
187-
drag_started: ctx.input(|input| input.pointer.button_pressed(PointerButton::Primary)),
188-
dragging: ctx.input(|input| input.pointer.button_down(PointerButton::Primary)),
189-
},
190-
targets,
191-
);
192-
193-
let draw_data = gizmo.draw();
194-
195-
ctx.layer_painter(egui::LayerId::background())
196-
// .with_clip_rect(egui_viewport)
197-
.add(Mesh {
198-
indices: draw_data.indices,
199-
vertices: draw_data
200-
.vertices
201-
.into_iter()
202-
.zip(draw_data.colors)
203-
.map(|(pos, [r, g, b, a])| Vertex {
204-
pos: pos.into(),
205-
uv: Pos2::default(),
206-
color: Rgba::from_rgba_premultiplied(r, g, b, a).into(),
207-
})
208-
.collect(),
209-
..Default::default()
210-
});
268+
for (i, info) in gizmo_info.iter_mut().enumerate() {
269+
if active_result
270+
.as_ref()
271+
.map_or(true, |(_, _, active)| *active == i)
272+
{
273+
let draw_data = info.gizmo.draw();
211274

212-
gizmo_result
275+
ctx.layer_painter(egui::LayerId::background())
276+
// .with_clip_rect(egui_viewport)
277+
.add(Mesh {
278+
indices: draw_data.indices,
279+
vertices: draw_data
280+
.vertices
281+
.into_iter()
282+
.zip(draw_data.colors)
283+
.map(|(pos, [r, g, b, a])| Vertex {
284+
pos: pos.into(),
285+
uv: Pos2::default(),
286+
color: Rgba::from_rgba_premultiplied(r, g, b, a).into(),
287+
})
288+
.collect(),
289+
..Default::default()
290+
});
291+
}
292+
}
293+
active_result
213294
}

crates/alkahest/src/gui/inspector/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ impl ComponentPanel for Transform {
465465
(self.translation - camera.position()).length().max(0.1),
466466
);
467467
}
468+
} else if self.flags.contains(TransformFlags::SCALE_IS_BIDIRECTIONAL) {
469+
let mut scale = self.scale * 2.0;
470+
if input_float3!(ui, format!("{ICON_RESIZE} Sides"), &mut scale).inner {
471+
self.scale = scale / 2.0;
472+
}
468473
} else {
469474
input_float3!(ui, format!("{ICON_RESIZE} Scale"), &mut self.scale).inner;
470475
}

0 commit comments

Comments
 (0)