Skip to content

Commit 42e246d

Browse files
committed
Collider editing, mesh colliders, scripting input actions, play-mode console
1 parent 3344d05 commit 42e246d

30 files changed

Lines changed: 1552 additions & 208 deletions

File tree

crates/renzora/src/core/keybindings.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub enum EditorAction {
3737
Duplicate,
3838
DuplicateAndMove,
3939
Deselect,
40+
/// Teleport the selected entity to the point under the mouse cursor.
41+
MoveSelectionToCursor,
4042

4143
// Edit operations
4244
Undo,
@@ -95,6 +97,7 @@ impl EditorAction {
9597
EditorAction::Duplicate => "Duplicate",
9698
EditorAction::DuplicateAndMove => "Duplicate & Move",
9799
EditorAction::Deselect => "Deselect",
100+
EditorAction::MoveSelectionToCursor => "Move Selection to Cursor",
98101
EditorAction::Undo => "Undo",
99102
EditorAction::Redo => "Redo",
100103
EditorAction::CreateNode => "Create Node",
@@ -141,7 +144,7 @@ impl EditorAction {
141144
| EditorAction::ModalRotate
142145
| EditorAction::ModalScale => "Transform",
143146

144-
EditorAction::SelectUnderCursor | EditorAction::Delete | EditorAction::Duplicate | EditorAction::DuplicateAndMove | EditorAction::Deselect => "Selection",
147+
EditorAction::SelectUnderCursor | EditorAction::Delete | EditorAction::Duplicate | EditorAction::DuplicateAndMove | EditorAction::Deselect | EditorAction::MoveSelectionToCursor => "Selection",
145148

146149
EditorAction::Undo
147150
| EditorAction::Redo
@@ -194,6 +197,7 @@ impl EditorAction {
194197
EditorAction::Duplicate,
195198
EditorAction::DuplicateAndMove,
196199
EditorAction::Deselect,
200+
EditorAction::MoveSelectionToCursor,
197201
EditorAction::Undo,
198202
EditorAction::Redo,
199203
EditorAction::CreateNode,
@@ -362,6 +366,7 @@ impl Default for KeyBindings {
362366
bindings.insert(EditorAction::ViewTop, KeyBinding::new(KeyCode::Numpad7));
363367
bindings.insert(EditorAction::ViewBottom, KeyBinding::new(KeyCode::Numpad7).ctrl());
364368
bindings.insert(EditorAction::ToggleProjection, KeyBinding::new(KeyCode::Numpad5));
369+
bindings.insert(EditorAction::MoveSelectionToCursor, KeyBinding::new(KeyCode::KeyV));
365370

366371
Self {
367372
bindings,

crates/renzora_animation/src/inspector.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,45 @@ fn animator_custom_ui(
129129
frame.show(ui, |ui| {
130130
ui.set_width(ui.available_width());
131131
ui.horizontal(|ui| {
132-
// Clip name (editable)
133-
ui.label(egui::RichText::new(&slot.name).size(11.0).color(text_color));
132+
// Clip name — editable. Persist the edit buffer across frames
133+
// in egui memory so keystrokes aren't lost when the inspector
134+
// rebuilds each frame. Commit on focus loss or Enter.
135+
let edit_id = egui::Id::new(("anim_clip_name", entity, i));
136+
let mut name_buf: String = ui.data(|d| d.get_temp::<String>(edit_id)).unwrap_or_else(|| slot.name.clone());
137+
// If the underlying slot name changed externally (e.g. undo),
138+
// reset the buffer unless this field is currently focused.
139+
let is_focused = ui.memory(|m| m.focused()) == Some(edit_id);
140+
if !is_focused && name_buf != slot.name {
141+
name_buf = slot.name.clone();
142+
}
143+
let response = ui.add(
144+
egui::TextEdit::singleline(&mut name_buf)
145+
.id(edit_id)
146+
.desired_width(140.0)
147+
.font(egui::FontId::proportional(11.0)),
148+
);
149+
ui.data_mut(|d| d.insert_temp(edit_id, name_buf.clone()));
150+
151+
let committed = response.lost_focus();
152+
if committed && name_buf != slot.name {
153+
let trimmed = name_buf.trim().to_string();
154+
let original = slot.name.clone();
155+
if !trimmed.is_empty() && !clips.iter().enumerate().any(|(j, s)| j != i && s.name == trimmed) {
156+
cmds.push(move |world: &mut World| {
157+
if let Some(mut a) = world.get_mut::<AnimatorComponent>(entity) {
158+
if let Some(slot) = a.clips.get_mut(i) {
159+
slot.name = trimmed.clone();
160+
}
161+
if a.default_clip.as_deref() == Some(original.as_str()) {
162+
a.default_clip = Some(trimmed);
163+
}
164+
}
165+
});
166+
} else {
167+
// Rejected — reset the persisted buffer so it reverts visually.
168+
ui.data_mut(|d| d.remove::<String>(edit_id));
169+
}
170+
}
134171

135172
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
136173
// Remove button

crates/renzora_animation/src/systems.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,14 @@ pub fn process_animation_commands(
370370
continue;
371371
}
372372

373+
// Idempotent: scripts re-call `play_animation` every frame
374+
// because the Lua state doesn't persist locals. Skip if the
375+
// clip is already the current one so looping clips don't
376+
// restart every frame.
377+
if state.current_clip.as_deref() == Some(name.as_str()) && !state.is_paused {
378+
continue;
379+
}
380+
373381
let Some(&node_idx) = state.node_indices.get(&name) else {
374382
warn!("AnimationCommand::Play: clip '{}' not found", name);
375383
continue;

crates/renzora_camera/src/lib.rs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use renzora::core::viewport_types::{
1919
ViewportSettings, ViewportState,
2020
};
2121
use renzora_editor_framework::EditorSelection;
22-
use renzora::core::EditorCamera;
22+
use renzora::core::{EditorCamera, PlayModeCamera};
2323

2424
/// Orbit camera state for the editor viewport.
2525
///
@@ -550,20 +550,11 @@ fn update_camera_projection(
550550
/// Apply orbit transform when the resource is replaced (e.g. after scene load).
551551
fn apply_orbit_on_change(
552552
orbit: Res<OrbitCameraState>,
553-
mut cameras: Query<(Entity, &mut Transform, &Camera), With<EditorCamera>>,
554-
play_mode: Option<Res<renzora::core::PlayModeState>>,
553+
mut cameras: Query<&mut Transform, (With<EditorCamera>, Without<PlayModeCamera>)>,
555554
) {
556555
if !orbit.is_changed() { return; }
557-
let is_playing = play_mode.as_ref().map_or(false, |pm| pm.is_in_play_mode());
558-
for (entity, mut transform, camera) in &mut cameras {
559-
let new_t = orbit.calculate_transform();
560-
renzora::core::console_log::console_info("Camera", format!(
561-
"apply_orbit_on_change: entity={:?} active={} playing={} pos={:?} -> {:?} focus={:?} dist={:.2} yaw={:.3} pitch={:.3}",
562-
entity, camera.is_active, is_playing,
563-
transform.translation, new_t.translation,
564-
orbit.focus, orbit.distance, orbit.yaw, orbit.pitch
565-
));
566-
*transform = new_t;
556+
for mut transform in &mut cameras {
557+
*transform = orbit.calculate_transform();
567558
}
568559
}
569560

crates/renzora_engine/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ron = "0.8"
2424
serde = "1"
2525
toml = "0.8"
2626
bevy_egui = { version = "0.39", optional = true }
27+
avian3d = { version = "0.6", default-features = false, features = ["3d", "f32", "default-collider", "parry-f32"] }
2728

2829
[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies]
2930
arboard = "3"

crates/renzora_engine/src/scene_io.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ pub fn save_scene(world: &mut World, path: &Path) -> Result<(), Box<dyn std::err
9393
.deny_component::<renzora_network::Networked>()
9494
.deny_component::<renzora_network::NetworkOwner>()
9595
.deny_component::<renzora_network::NetworkId>()
96+
// Avian runtime components are regenerated on load from our
97+
// serializable PhysicsBodyData + CollisionShapeData. Persisting them
98+
// causes duplicate-reflect-type errors during deserialization.
99+
.deny_component::<avian3d::prelude::Collider>()
100+
.deny_component::<avian3d::collision::collider::ColliderAabb>()
101+
.deny_component::<avian3d::prelude::RigidBody>()
102+
.deny_component::<avian3d::prelude::LinearVelocity>()
103+
.deny_component::<avian3d::prelude::AngularVelocity>()
104+
.deny_component::<avian3d::prelude::Mass>()
105+
.deny_component::<avian3d::prelude::Friction>()
106+
.deny_component::<avian3d::prelude::Restitution>()
107+
.deny_component::<avian3d::prelude::GravityScale>()
108+
.deny_component::<avian3d::prelude::LinearDamping>()
109+
.deny_component::<avian3d::prelude::AngularDamping>()
110+
.deny_component::<avian3d::prelude::LockedAxes>()
111+
.deny_component::<avian3d::prelude::Sensor>()
96112
.extract_entities(entities.into_iter())
97113
.build();
98114

@@ -106,6 +122,12 @@ pub fn save_scene(world: &mut World, path: &Path) -> Result<(), Box<dyn std::err
106122
if type_name.starts_with("bevy_mod_outline::") {
107123
return false;
108124
}
125+
// Never serialize avian runtime components — they're regenerated
126+
// on load from PhysicsBodyData + CollisionShapeData. Persisting
127+
// them causes duplicate-reflect-type errors on deserialize.
128+
if type_name.starts_with("avian3d::") {
129+
return false;
130+
}
109131
let serializer = bevy::reflect::serde::TypedReflectSerializer::new(
110132
component.as_partial_reflect(),
111133
&registry,
@@ -188,6 +210,22 @@ pub fn serialize_scene_to_string(world: &mut World) -> Result<String, Box<dyn st
188210
.deny_component::<renzora_network::Networked>()
189211
.deny_component::<renzora_network::NetworkOwner>()
190212
.deny_component::<renzora_network::NetworkId>()
213+
// Avian runtime components are regenerated on load from our
214+
// serializable PhysicsBodyData + CollisionShapeData. Persisting them
215+
// causes duplicate-reflect-type errors during deserialization.
216+
.deny_component::<avian3d::prelude::Collider>()
217+
.deny_component::<avian3d::collision::collider::ColliderAabb>()
218+
.deny_component::<avian3d::prelude::RigidBody>()
219+
.deny_component::<avian3d::prelude::LinearVelocity>()
220+
.deny_component::<avian3d::prelude::AngularVelocity>()
221+
.deny_component::<avian3d::prelude::Mass>()
222+
.deny_component::<avian3d::prelude::Friction>()
223+
.deny_component::<avian3d::prelude::Restitution>()
224+
.deny_component::<avian3d::prelude::GravityScale>()
225+
.deny_component::<avian3d::prelude::LinearDamping>()
226+
.deny_component::<avian3d::prelude::AngularDamping>()
227+
.deny_component::<avian3d::prelude::LockedAxes>()
228+
.deny_component::<avian3d::prelude::Sensor>()
191229
.extract_entities(entities.into_iter())
192230
.build();
193231

crates/renzora_gizmo/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ renzora_editor_framework = { path = "../renzora_editor_framework" }
1111
bevy = { workspace = true }
1212
renzora = { path = "../renzora", default-features = false, features = ["editor"] }
1313
bevy_mod_outline = { path = "../bevy_mod_outline" }
14+
renzora_physics = { path = "../renzora_physics" }
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//! Wireframe gizmos for `CollisionShapeData` so colliders are visible in the editor viewport.
2+
//!
3+
//! Drawn every frame for every entity with a `CollisionShapeData` + `GlobalTransform`.
4+
//! Uses the same `OverlayGizmoGroup` config as the other line-based gizmos so it
5+
//! respects depth bias and render layer 1.
6+
7+
use bevy::prelude::*;
8+
use bevy::camera::primitives::Aabb;
9+
10+
use renzora_editor_framework::EditorSelection;
11+
use renzora_physics::{CollisionShapeData, CollisionShapeType};
12+
13+
use crate::OverlayGizmoGroup;
14+
15+
const COLOR_STATIC: Color = Color::srgb(0.30, 0.85, 0.40);
16+
const COLOR_DYNAMIC: Color = Color::srgb(1.0, 0.55, 0.15);
17+
const COLOR_SENSOR: Color = Color::srgb(0.30, 0.70, 1.0);
18+
19+
pub fn draw_collider_gizmos(
20+
mut gizmos: Gizmos<OverlayGizmoGroup>,
21+
selection: Res<EditorSelection>,
22+
query: Query<(
23+
Entity,
24+
&CollisionShapeData,
25+
&GlobalTransform,
26+
Option<&renzora_physics::PhysicsBodyData>,
27+
Option<&Aabb>,
28+
)>,
29+
) {
30+
for (entity, shape, gt, body, aabb) in &query {
31+
if !selection.is_selected(entity) {
32+
continue;
33+
}
34+
let color = if shape.is_sensor {
35+
COLOR_SENSOR
36+
} else {
37+
match body.map(|b| b.body_type) {
38+
Some(renzora_physics::PhysicsBodyType::StaticBody) => COLOR_STATIC,
39+
_ => COLOR_DYNAMIC,
40+
}
41+
};
42+
43+
let (scale, rot, trans) = gt.to_scale_rotation_translation();
44+
let center = trans + rot * (scale * shape.offset);
45+
let iso = Isometry3d::new(center, rot);
46+
47+
match shape.shape_type {
48+
CollisionShapeType::Box => {
49+
let size = shape.half_extents * 2.0 * scale;
50+
let xform = Transform {
51+
translation: center,
52+
rotation: rot,
53+
scale: size,
54+
};
55+
gizmos.cube(xform, color);
56+
}
57+
CollisionShapeType::Sphere => {
58+
let r = shape.radius * scale.max_element();
59+
gizmos.sphere(iso, r, color);
60+
}
61+
CollisionShapeType::Capsule => {
62+
let r = shape.radius * scale.x.max(scale.z);
63+
let hh = shape.half_height * scale.y;
64+
draw_capsule(&mut gizmos, center, rot, r, hh, color);
65+
}
66+
CollisionShapeType::Cylinder => {
67+
let r = shape.radius * scale.x.max(scale.z);
68+
let hh = shape.half_height * scale.y;
69+
draw_cylinder(&mut gizmos, center, rot, r, hh, color);
70+
}
71+
CollisionShapeType::Mesh => {
72+
if let Some(aabb) = aabb {
73+
let size = Vec3::from(aabb.half_extents) * 2.0 * scale;
74+
let aabb_center = trans + rot * (scale * Vec3::from(aabb.center));
75+
gizmos.cube(Transform { translation: aabb_center, rotation: rot, scale: size }, color);
76+
}
77+
}
78+
}
79+
}
80+
}
81+
82+
fn draw_capsule(
83+
gizmos: &mut Gizmos<OverlayGizmoGroup>,
84+
center: Vec3,
85+
rot: Quat,
86+
radius: f32,
87+
half_height: f32,
88+
color: Color,
89+
) {
90+
let up = rot * Vec3::Y;
91+
let right = rot * Vec3::X;
92+
let fwd = rot * Vec3::Z;
93+
let top = center + up * half_height;
94+
let bot = center - up * half_height;
95+
96+
// Equator circles at the cap joins.
97+
gizmos.circle(Isometry3d::new(top, rot * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)), radius, color);
98+
gizmos.circle(Isometry3d::new(bot, rot * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)), radius, color);
99+
100+
// Vertical connecting lines between the cap joins.
101+
gizmos.line(top + right * radius, bot + right * radius, color);
102+
gizmos.line(top - right * radius, bot - right * radius, color);
103+
gizmos.line(top + fwd * radius, bot + fwd * radius, color);
104+
gizmos.line(top - fwd * radius, bot - fwd * radius, color);
105+
106+
// Hemisphere arcs — drawn by hand as line segments for reliability across
107+
// Bevy versions. Two arcs per cap (one in XY plane, one in ZY plane of the
108+
// capsule's local space), each spanning 180°.
109+
draw_hemi_arc(gizmos, top, up, right, radius, color);
110+
draw_hemi_arc(gizmos, top, up, fwd, radius, color);
111+
draw_hemi_arc(gizmos, bot, -up, right, radius, color);
112+
draw_hemi_arc(gizmos, bot, -up, fwd, radius, color);
113+
}
114+
115+
/// Draw a 180° arc from `center - side*radius` up over `center + up*radius` to
116+
/// `center + side*radius`, using segmented lines.
117+
fn draw_hemi_arc(
118+
gizmos: &mut Gizmos<OverlayGizmoGroup>,
119+
center: Vec3,
120+
up: Vec3,
121+
side: Vec3,
122+
radius: f32,
123+
color: Color,
124+
) {
125+
const SEGS: usize = 16;
126+
let mut prev = center - side * radius;
127+
for i in 1..=SEGS {
128+
let t = i as f32 / SEGS as f32;
129+
let angle = std::f32::consts::PI * t;
130+
// Starts at -side (angle=0) → +up at angle=PI/2 → +side at angle=PI.
131+
let p = center + (-side * angle.cos() + up * angle.sin()) * radius;
132+
gizmos.line(prev, p, color);
133+
prev = p;
134+
}
135+
}
136+
137+
fn draw_cylinder(
138+
gizmos: &mut Gizmos<OverlayGizmoGroup>,
139+
center: Vec3,
140+
rot: Quat,
141+
radius: f32,
142+
half_height: f32,
143+
color: Color,
144+
) {
145+
let up = rot * Vec3::Y;
146+
let top = center + up * half_height;
147+
let bot = center - up * half_height;
148+
149+
let cap_rot = rot * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2);
150+
gizmos.circle(Isometry3d::new(top, cap_rot), radius, color);
151+
gizmos.circle(Isometry3d::new(bot, cap_rot), radius, color);
152+
153+
let right = rot * Vec3::X;
154+
let fwd = rot * Vec3::Z;
155+
gizmos.line(top + right * radius, bot + right * radius, color);
156+
gizmos.line(top - right * radius, bot - right * radius, color);
157+
gizmos.line(top + fwd * radius, bot + fwd * radius, color);
158+
gizmos.line(top - fwd * radius, bot - fwd * radius, color);
159+
}

0 commit comments

Comments
 (0)