bevy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +4279 -0
  3. data/Cargo.toml +36 -0
  4. data/README.md +226 -0
  5. data/crates/bevy/Cargo.toml +52 -0
  6. data/crates/bevy/src/app.rs +43 -0
  7. data/crates/bevy/src/component.rs +111 -0
  8. data/crates/bevy/src/entity.rs +30 -0
  9. data/crates/bevy/src/error.rs +32 -0
  10. data/crates/bevy/src/event.rs +190 -0
  11. data/crates/bevy/src/input_bridge.rs +300 -0
  12. data/crates/bevy/src/lib.rs +42 -0
  13. data/crates/bevy/src/mesh_renderer.rs +328 -0
  14. data/crates/bevy/src/query.rs +53 -0
  15. data/crates/bevy/src/render_app.rs +689 -0
  16. data/crates/bevy/src/resource.rs +28 -0
  17. data/crates/bevy/src/schedule.rs +186 -0
  18. data/crates/bevy/src/sprite_renderer.rs +355 -0
  19. data/crates/bevy/src/system.rs +44 -0
  20. data/crates/bevy/src/text_renderer.rs +258 -0
  21. data/crates/bevy/src/types/color.rs +114 -0
  22. data/crates/bevy/src/types/dynamic.rs +131 -0
  23. data/crates/bevy/src/types/math.rs +260 -0
  24. data/crates/bevy/src/types/mod.rs +9 -0
  25. data/crates/bevy/src/types/transform.rs +166 -0
  26. data/crates/bevy/src/world.rs +163 -0
  27. data/crates/bevy_ruby_render/Cargo.toml +22 -0
  28. data/crates/bevy_ruby_render/src/asset.rs +360 -0
  29. data/crates/bevy_ruby_render/src/audio.rs +511 -0
  30. data/crates/bevy_ruby_render/src/camera.rs +365 -0
  31. data/crates/bevy_ruby_render/src/gamepad.rs +398 -0
  32. data/crates/bevy_ruby_render/src/lib.rs +26 -0
  33. data/crates/bevy_ruby_render/src/material.rs +310 -0
  34. data/crates/bevy_ruby_render/src/mesh.rs +491 -0
  35. data/crates/bevy_ruby_render/src/sprite.rs +289 -0
  36. data/ext/bevy/Cargo.toml +20 -0
  37. data/ext/bevy/extconf.rb +6 -0
  38. data/ext/bevy/src/conversions.rs +137 -0
  39. data/ext/bevy/src/lib.rs +29 -0
  40. data/ext/bevy/src/ruby_app.rs +65 -0
  41. data/ext/bevy/src/ruby_color.rs +149 -0
  42. data/ext/bevy/src/ruby_component.rs +189 -0
  43. data/ext/bevy/src/ruby_entity.rs +33 -0
  44. data/ext/bevy/src/ruby_math.rs +384 -0
  45. data/ext/bevy/src/ruby_query.rs +64 -0
  46. data/ext/bevy/src/ruby_render_app.rs +779 -0
  47. data/ext/bevy/src/ruby_system.rs +122 -0
  48. data/ext/bevy/src/ruby_world.rs +107 -0
  49. data/lib/bevy/animation.rb +597 -0
  50. data/lib/bevy/app.rb +675 -0
  51. data/lib/bevy/asset.rb +613 -0
  52. data/lib/bevy/audio.rb +545 -0
  53. data/lib/bevy/audio_effects.rb +224 -0
  54. data/lib/bevy/camera.rb +412 -0
  55. data/lib/bevy/component.rb +91 -0
  56. data/lib/bevy/diagnostics.rb +227 -0
  57. data/lib/bevy/ecs_advanced.rb +296 -0
  58. data/lib/bevy/event.rb +199 -0
  59. data/lib/bevy/gizmos.rb +158 -0
  60. data/lib/bevy/gltf.rb +227 -0
  61. data/lib/bevy/hierarchy.rb +444 -0
  62. data/lib/bevy/input.rb +514 -0
  63. data/lib/bevy/lighting.rb +369 -0
  64. data/lib/bevy/material.rb +248 -0
  65. data/lib/bevy/mesh.rb +257 -0
  66. data/lib/bevy/navigation.rb +344 -0
  67. data/lib/bevy/networking.rb +335 -0
  68. data/lib/bevy/particle.rb +337 -0
  69. data/lib/bevy/physics.rb +396 -0
  70. data/lib/bevy/plugins/default_plugins.rb +34 -0
  71. data/lib/bevy/plugins/input_plugin.rb +49 -0
  72. data/lib/bevy/reflect.rb +361 -0
  73. data/lib/bevy/render_graph.rb +210 -0
  74. data/lib/bevy/resource.rb +185 -0
  75. data/lib/bevy/scene.rb +254 -0
  76. data/lib/bevy/shader.rb +319 -0
  77. data/lib/bevy/shape.rb +195 -0
  78. data/lib/bevy/skeletal.rb +248 -0
  79. data/lib/bevy/sprite.rb +152 -0
  80. data/lib/bevy/sprite_sheet.rb +444 -0
  81. data/lib/bevy/state.rb +277 -0
  82. data/lib/bevy/system.rb +206 -0
  83. data/lib/bevy/text.rb +99 -0
  84. data/lib/bevy/text_advanced.rb +455 -0
  85. data/lib/bevy/timer.rb +147 -0
  86. data/lib/bevy/transform.rb +158 -0
  87. data/lib/bevy/ui.rb +454 -0
  88. data/lib/bevy/ui_advanced.rb +568 -0
  89. data/lib/bevy/version.rb +5 -0
  90. data/lib/bevy/visibility.rb +250 -0
  91. data/lib/bevy/window.rb +302 -0
  92. data/lib/bevy.rb +390 -0
  93. metadata +150 -0
@@ -0,0 +1,779 @@
1
+ //! Ruby bindings for the RenderApp and input handling.
2
+
3
+ use bevy_ruby::{
4
+ GamepadRumbleCommand, InputState, MeshData, MeshSync, MeshTransformData, PickingEventData,
5
+ RenderApp, ShapeType, SpriteData, SpriteSync, TextData, TextSync, TextTransformData,
6
+ TransformData, WindowConfig,
7
+ };
8
+ use magnus::{
9
+ Error, RArray, RHash, Ruby, TryConvert, Value, block::Proc, function, method, prelude::*,
10
+ };
11
+ use std::cell::RefCell;
12
+
13
+ struct RenderState {
14
+ render_app: RenderApp,
15
+ sprite_sync: SpriteSync,
16
+ }
17
+
18
+ thread_local! {
19
+ static RENDER_STATE: RefCell<Option<RenderState>> = const { RefCell::new(None) };
20
+ static RUBY_CALLBACK: RefCell<Option<Proc>> = const { RefCell::new(None) };
21
+ static SHARED_INPUT: RefCell<InputState> = RefCell::new(InputState::new());
22
+ static SHOULD_STOP: RefCell<bool> = const { RefCell::new(false) };
23
+ static PENDING_SPRITES: RefCell<SpriteSync> = RefCell::new(SpriteSync::new());
24
+ static PENDING_TEXTS: RefCell<TextSync> = RefCell::new(TextSync::new());
25
+ static PENDING_MESHES: RefCell<MeshSync> = RefCell::new(MeshSync::new());
26
+ static CAMERA_POSITION: RefCell<(f32, f32, f32)> = RefCell::new((0.0, 0.0, 0.0));
27
+ static CAMERA_SCALE: RefCell<f32> = RefCell::new(1.0);
28
+ static CAMERA_DIRTY: RefCell<bool> = const { RefCell::new(false) };
29
+ static PENDING_GAMEPAD_RUMBLE: RefCell<Vec<GamepadRumbleCommand>> = const { RefCell::new(Vec::new()) };
30
+ static SHARED_PICKING_EVENTS: RefCell<Vec<PickingEventData>> = const { RefCell::new(Vec::new()) };
31
+ }
32
+
33
+ #[magnus::wrap(class = "Bevy::RenderApp", free_immediately, size)]
34
+ pub struct RubyRenderApp {
35
+ _marker: (),
36
+ }
37
+
38
+ impl RubyRenderApp {
39
+ fn new(args: &[Value]) -> Result<Self, Error> {
40
+ let ruby = Ruby::get().expect("Ruby runtime not available");
41
+
42
+ let config = if args.is_empty() {
43
+ WindowConfig::default()
44
+ } else {
45
+ let hash: RHash = TryConvert::try_convert(args[0])?;
46
+ let title: Option<String> = get_hash_value(&ruby, &hash, "title")?;
47
+ let width: Option<f64> = get_hash_value(&ruby, &hash, "width")?;
48
+ let height: Option<f64> = get_hash_value(&ruby, &hash, "height")?;
49
+ let resizable: Option<bool> = get_hash_value(&ruby, &hash, "resizable")?;
50
+
51
+ WindowConfig {
52
+ title: title.unwrap_or_else(|| "Bevy Ruby".to_string()),
53
+ width: width.unwrap_or(800.0) as f32,
54
+ height: height.unwrap_or(600.0) as f32,
55
+ resizable: resizable.unwrap_or(true),
56
+ }
57
+ };
58
+
59
+ RENDER_STATE.with(|state| {
60
+ let mut state = state.borrow_mut();
61
+ if state.is_some() {
62
+ return Err(Error::new(
63
+ ruby.exception_runtime_error(),
64
+ "RenderApp already exists. Only one instance is allowed.",
65
+ ));
66
+ }
67
+ *state = Some(RenderState {
68
+ render_app: RenderApp::new(config),
69
+ sprite_sync: SpriteSync::new(),
70
+ });
71
+ Ok(())
72
+ })?;
73
+
74
+ Ok(Self { _marker: () })
75
+ }
76
+
77
+ fn initialize(&self) -> Result<(), Error> {
78
+ Ok(())
79
+ }
80
+
81
+ fn run_with_block(&self) -> Result<(), Error> {
82
+ let ruby = Ruby::get().expect("Ruby runtime not available");
83
+
84
+ if !ruby.block_given() {
85
+ return Err(Error::new(
86
+ ruby.exception_arg_error(),
87
+ "run requires a block",
88
+ ));
89
+ }
90
+
91
+ let proc = ruby.block_proc()?;
92
+ RUBY_CALLBACK.with(|cb| {
93
+ *cb.borrow_mut() = Some(proc);
94
+ });
95
+
96
+ RENDER_STATE.with(|state| {
97
+ let mut state = state.borrow_mut();
98
+ if let Some(ref mut s) = *state {
99
+ #[cfg(feature = "rendering")]
100
+ {
101
+ s.render_app.set_callback(move |bridge_state| {
102
+ SHARED_INPUT.with(|input| {
103
+ *input.borrow_mut() = bridge_state.input_state.clone();
104
+ });
105
+ SHARED_PICKING_EVENTS.with(|events| {
106
+ *events.borrow_mut() = bridge_state.picking_events.clone();
107
+ });
108
+
109
+ RUBY_CALLBACK.with(|cb| {
110
+ if let Some(ref proc) = *cb.borrow() {
111
+ let _ = proc.call::<_, Value>(());
112
+ }
113
+ });
114
+
115
+ PENDING_SPRITES.with(|sprites| {
116
+ let mut pending = sprites.borrow_mut();
117
+ for op in pending.pending_operations.drain(..) {
118
+ bridge_state.sprite_sync.pending_operations.push(op);
119
+ }
120
+ });
121
+
122
+ PENDING_TEXTS.with(|texts| {
123
+ let mut pending = texts.borrow_mut();
124
+ for op in pending.pending_operations.drain(..) {
125
+ bridge_state.text_sync.pending_operations.push(op);
126
+ }
127
+ });
128
+
129
+ PENDING_MESHES.with(|meshes| {
130
+ let mut pending = meshes.borrow_mut();
131
+ for op in pending.pending_operations.drain(..) {
132
+ bridge_state.mesh_sync.pending_operations.push(op);
133
+ }
134
+ });
135
+
136
+ PENDING_GAMEPAD_RUMBLE.with(|rumbles| {
137
+ let mut pending = rumbles.borrow_mut();
138
+ for command in pending.drain(..) {
139
+ bridge_state.pending_gamepad_rumble.push(command);
140
+ }
141
+ });
142
+
143
+ let camera_dirty = CAMERA_DIRTY.with(|d| {
144
+ let dirty = *d.borrow();
145
+ *d.borrow_mut() = false;
146
+ dirty
147
+ });
148
+ if camera_dirty {
149
+ bridge_state.camera_position = CAMERA_POSITION.with(|p| *p.borrow());
150
+ bridge_state.camera_scale = CAMERA_SCALE.with(|s| *s.borrow());
151
+ bridge_state.camera_dirty = true;
152
+ }
153
+
154
+ let should_stop = SHOULD_STOP.with(|s| *s.borrow());
155
+ if should_stop {
156
+ bridge_state.should_exit = true;
157
+ }
158
+ });
159
+
160
+ s.render_app.run();
161
+ }
162
+ }
163
+ });
164
+
165
+ RUBY_CALLBACK.with(|cb| {
166
+ *cb.borrow_mut() = None;
167
+ });
168
+
169
+ RENDER_STATE.with(|state| {
170
+ *state.borrow_mut() = None;
171
+ });
172
+
173
+ Ok(())
174
+ }
175
+
176
+ fn stop(&self) -> Result<(), Error> {
177
+ SHOULD_STOP.with(|s| {
178
+ *s.borrow_mut() = true;
179
+ });
180
+ Ok(())
181
+ }
182
+
183
+ fn should_close(&self) -> bool {
184
+ RENDER_STATE.with(|state| {
185
+ let state = state.borrow();
186
+ if let Some(ref s) = *state {
187
+ s.render_app.should_exit()
188
+ } else {
189
+ true
190
+ }
191
+ })
192
+ }
193
+
194
+ fn key_pressed(&self, key: String) -> bool {
195
+ SHARED_INPUT.with(|input| input.borrow().key_pressed(&key))
196
+ }
197
+
198
+ fn key_just_pressed(&self, key: String) -> bool {
199
+ SHARED_INPUT.with(|input| input.borrow().key_just_pressed(&key))
200
+ }
201
+
202
+ fn key_just_released(&self, key: String) -> bool {
203
+ SHARED_INPUT.with(|input| input.borrow().key_just_released(&key))
204
+ }
205
+
206
+ fn mouse_button_pressed(&self, button: String) -> bool {
207
+ SHARED_INPUT.with(|input| input.borrow().mouse_button_pressed(&button))
208
+ }
209
+
210
+ fn mouse_button_just_pressed(&self, button: String) -> bool {
211
+ SHARED_INPUT.with(|input| input.borrow().mouse_button_just_pressed(&button))
212
+ }
213
+
214
+ fn mouse_position(&self) -> RArray {
215
+ let ruby = Ruby::get().expect("Ruby runtime not available");
216
+ let (x, y) = SHARED_INPUT.with(|input| input.borrow().mouse_position);
217
+ let array = ruby.ary_new_capa(2);
218
+ let _ = array.push(x);
219
+ let _ = array.push(y);
220
+ array
221
+ }
222
+
223
+ fn mouse_delta(&self) -> RArray {
224
+ let ruby = Ruby::get().expect("Ruby runtime not available");
225
+ let (dx, dy) = SHARED_INPUT.with(|input| input.borrow().mouse_delta);
226
+ let array = ruby.ary_new_capa(2);
227
+ let _ = array.push(dx);
228
+ let _ = array.push(dy);
229
+ array
230
+ }
231
+
232
+ fn pressed_keys(&self) -> RArray {
233
+ let ruby = Ruby::get().expect("Ruby runtime not available");
234
+ let keys = SHARED_INPUT.with(|input| input.borrow().get_pressed_keys());
235
+ let array = ruby.ary_new_capa(keys.len());
236
+ for key in keys {
237
+ let _ = array.push(key);
238
+ }
239
+ array
240
+ }
241
+
242
+ fn gamepads_state(&self) -> Result<RArray, Error> {
243
+ let ruby = Ruby::get().expect("Ruby runtime not available");
244
+ let mut states = SHARED_INPUT.with(|input| input.borrow().gamepad_states());
245
+ states.sort_by_key(|state| state.id);
246
+
247
+ let id_sym = ruby.to_symbol("id");
248
+ let name_sym = ruby.to_symbol("name");
249
+ let buttons_pressed_sym = ruby.to_symbol("buttons_pressed");
250
+ let buttons_just_pressed_sym = ruby.to_symbol("buttons_just_pressed");
251
+ let buttons_just_released_sym = ruby.to_symbol("buttons_just_released");
252
+ let axes_sym = ruby.to_symbol("axes");
253
+
254
+ let result = ruby.ary_new_capa(states.len());
255
+
256
+ for state in states {
257
+ let hash = ruby.hash_new();
258
+ hash.aset(id_sym, state.id)?;
259
+ hash.aset(name_sym, state.name)?;
260
+
261
+ let mut buttons_pressed: Vec<_> = state.buttons_pressed.into_iter().collect();
262
+ buttons_pressed.sort();
263
+ let buttons_pressed_array = ruby.ary_new_capa(buttons_pressed.len());
264
+ for button in buttons_pressed {
265
+ buttons_pressed_array.push(button)?;
266
+ }
267
+ hash.aset(buttons_pressed_sym, buttons_pressed_array)?;
268
+
269
+ let mut buttons_just_pressed: Vec<_> = state.buttons_just_pressed.into_iter().collect();
270
+ buttons_just_pressed.sort();
271
+ let buttons_just_pressed_array = ruby.ary_new_capa(buttons_just_pressed.len());
272
+ for button in buttons_just_pressed {
273
+ buttons_just_pressed_array.push(button)?;
274
+ }
275
+ hash.aset(buttons_just_pressed_sym, buttons_just_pressed_array)?;
276
+
277
+ let mut buttons_just_released: Vec<_> =
278
+ state.buttons_just_released.into_iter().collect();
279
+ buttons_just_released.sort();
280
+ let buttons_just_released_array = ruby.ary_new_capa(buttons_just_released.len());
281
+ for button in buttons_just_released {
282
+ buttons_just_released_array.push(button)?;
283
+ }
284
+ hash.aset(buttons_just_released_sym, buttons_just_released_array)?;
285
+
286
+ let axes_hash = ruby.hash_new();
287
+ let mut axes_entries: Vec<_> = state.axes.into_iter().collect();
288
+ axes_entries.sort_by(|left, right| left.0.cmp(&right.0));
289
+ for (axis, value) in axes_entries {
290
+ axes_hash.aset(axis, value as f64)?;
291
+ }
292
+ hash.aset(axes_sym, axes_hash)?;
293
+
294
+ result.push(hash)?;
295
+ }
296
+
297
+ Ok(result)
298
+ }
299
+
300
+ fn sync_sprite(
301
+ &self,
302
+ ruby_entity_id: u64,
303
+ sprite_hash: RHash,
304
+ transform_hash: RHash,
305
+ ) -> Result<(), Error> {
306
+ let ruby = Ruby::get().expect("Ruby runtime not available");
307
+ let sprite_data = parse_sprite_data(&ruby, &sprite_hash)?;
308
+ let transform_data = parse_transform_data(&ruby, &transform_hash)?;
309
+
310
+ PENDING_SPRITES.with(|sprites| {
311
+ sprites.borrow_mut().sync_sprite_standalone(
312
+ ruby_entity_id,
313
+ &sprite_data,
314
+ &transform_data,
315
+ );
316
+ });
317
+
318
+ Ok(())
319
+ }
320
+
321
+ fn remove_sprite(&self, ruby_entity_id: u64) -> Result<(), Error> {
322
+ PENDING_SPRITES.with(|sprites| {
323
+ sprites
324
+ .borrow_mut()
325
+ .remove_sprite_standalone(ruby_entity_id);
326
+ });
327
+
328
+ Ok(())
329
+ }
330
+
331
+ fn clear_sprites(&self) -> Result<(), Error> {
332
+ PENDING_SPRITES.with(|sprites| {
333
+ sprites.borrow_mut().clear_standalone();
334
+ });
335
+
336
+ Ok(())
337
+ }
338
+
339
+ fn sync_text(
340
+ &self,
341
+ ruby_entity_id: u64,
342
+ text_hash: RHash,
343
+ transform_hash: RHash,
344
+ ) -> Result<(), Error> {
345
+ let ruby = Ruby::get().expect("Ruby runtime not available");
346
+ let text_data = parse_text_data(&ruby, &text_hash)?;
347
+ let transform_data = parse_text_transform_data(&ruby, &transform_hash)?;
348
+
349
+ PENDING_TEXTS.with(|texts| {
350
+ texts
351
+ .borrow_mut()
352
+ .sync_text_standalone(ruby_entity_id, &text_data, &transform_data);
353
+ });
354
+
355
+ Ok(())
356
+ }
357
+
358
+ fn remove_text(&self, ruby_entity_id: u64) -> Result<(), Error> {
359
+ PENDING_TEXTS.with(|texts| {
360
+ texts.borrow_mut().remove_text_standalone(ruby_entity_id);
361
+ });
362
+
363
+ Ok(())
364
+ }
365
+
366
+ fn clear_texts(&self) -> Result<(), Error> {
367
+ PENDING_TEXTS.with(|texts| {
368
+ texts.borrow_mut().clear_standalone();
369
+ });
370
+
371
+ Ok(())
372
+ }
373
+
374
+ fn sync_mesh(
375
+ &self,
376
+ ruby_entity_id: u64,
377
+ mesh_hash: RHash,
378
+ transform_hash: RHash,
379
+ ) -> Result<(), Error> {
380
+ let ruby = Ruby::get().expect("Ruby runtime not available");
381
+ let mesh_data = parse_mesh_data(&ruby, &mesh_hash)?;
382
+ let transform_data = parse_mesh_transform_data(&ruby, &transform_hash)?;
383
+
384
+ PENDING_MESHES.with(|meshes| {
385
+ meshes
386
+ .borrow_mut()
387
+ .sync_mesh_standalone(ruby_entity_id, &mesh_data, &transform_data);
388
+ });
389
+
390
+ Ok(())
391
+ }
392
+
393
+ fn remove_mesh(&self, ruby_entity_id: u64) -> Result<(), Error> {
394
+ PENDING_MESHES.with(|meshes| {
395
+ meshes.borrow_mut().remove_mesh_standalone(ruby_entity_id);
396
+ });
397
+
398
+ Ok(())
399
+ }
400
+
401
+ fn clear_meshes(&self) -> Result<(), Error> {
402
+ PENDING_MESHES.with(|meshes| {
403
+ meshes.borrow_mut().clear_standalone();
404
+ });
405
+
406
+ Ok(())
407
+ }
408
+
409
+ fn is_initialized(&self) -> bool {
410
+ RENDER_STATE.with(|state| state.borrow().is_some())
411
+ }
412
+
413
+ fn set_camera_position(&self, x: f64, y: f64, z: f64) -> Result<(), Error> {
414
+ CAMERA_POSITION.with(|p| {
415
+ *p.borrow_mut() = (x as f32, y as f32, z as f32);
416
+ });
417
+ CAMERA_DIRTY.with(|d| {
418
+ *d.borrow_mut() = true;
419
+ });
420
+ Ok(())
421
+ }
422
+
423
+ fn get_camera_position(&self) -> RArray {
424
+ let ruby = Ruby::get().expect("Ruby runtime not available");
425
+ let (x, y, z) = CAMERA_POSITION.with(|p| *p.borrow());
426
+ let array = ruby.ary_new_capa(3);
427
+ let _ = array.push(x as f64);
428
+ let _ = array.push(y as f64);
429
+ let _ = array.push(z as f64);
430
+ array
431
+ }
432
+
433
+ fn set_camera_scale(&self, scale: f64) -> Result<(), Error> {
434
+ CAMERA_SCALE.with(|s| {
435
+ *s.borrow_mut() = scale as f32;
436
+ });
437
+ CAMERA_DIRTY.with(|d| {
438
+ *d.borrow_mut() = true;
439
+ });
440
+ Ok(())
441
+ }
442
+
443
+ fn get_camera_scale(&self) -> f64 {
444
+ CAMERA_SCALE.with(|s| *s.borrow()) as f64
445
+ }
446
+
447
+ fn queue_gamepad_rumble(
448
+ &self,
449
+ gamepad_id: u64,
450
+ strong_motor: f64,
451
+ weak_motor: f64,
452
+ duration_secs: f64,
453
+ ) -> Result<(), Error> {
454
+ let stop = strong_motor <= 0.0 && weak_motor <= 0.0;
455
+ let command = GamepadRumbleCommand {
456
+ gamepad_id,
457
+ strong_motor: strong_motor as f32,
458
+ weak_motor: weak_motor as f32,
459
+ duration_secs: duration_secs as f32,
460
+ stop,
461
+ };
462
+
463
+ PENDING_GAMEPAD_RUMBLE.with(|rumbles| {
464
+ rumbles.borrow_mut().push(command);
465
+ });
466
+ Ok(())
467
+ }
468
+
469
+ fn drain_picking_events(&self) -> Result<RArray, Error> {
470
+ let ruby = Ruby::get().expect("Ruby runtime not available");
471
+ let kind_sym = ruby.to_symbol("kind");
472
+ let target_id_sym = ruby.to_symbol("target_id");
473
+ let pointer_id_sym = ruby.to_symbol("pointer_id");
474
+ let position_sym = ruby.to_symbol("position");
475
+ let button_sym = ruby.to_symbol("button");
476
+ let camera_id_sym = ruby.to_symbol("camera_id");
477
+ let depth_sym = ruby.to_symbol("depth");
478
+ let hit_position_sym = ruby.to_symbol("hit_position");
479
+ let hit_normal_sym = ruby.to_symbol("hit_normal");
480
+
481
+ let events = SHARED_PICKING_EVENTS.with(|picking_events| {
482
+ let mut picking_events = picking_events.borrow_mut();
483
+ picking_events.drain(..).collect::<Vec<_>>()
484
+ });
485
+
486
+ let result = ruby.ary_new_capa(events.len());
487
+
488
+ for event in events {
489
+ let hash = ruby.hash_new();
490
+ hash.aset(kind_sym, event.kind)?;
491
+ hash.aset(target_id_sym, event.target_id)?;
492
+ hash.aset(pointer_id_sym, event.pointer_id)?;
493
+
494
+ let position = ruby.ary_new_capa(2);
495
+ position.push(event.pointer_position.0 as f64)?;
496
+ position.push(event.pointer_position.1 as f64)?;
497
+ hash.aset(position_sym, position)?;
498
+
499
+ if let Some(button) = event.button {
500
+ hash.aset(button_sym, button)?;
501
+ }
502
+
503
+ if let Some(camera_id) = event.camera_id {
504
+ hash.aset(camera_id_sym, camera_id)?;
505
+ }
506
+
507
+ if let Some(depth) = event.depth {
508
+ hash.aset(depth_sym, depth as f64)?;
509
+ }
510
+
511
+ if let Some((x, y, z)) = event.hit_position {
512
+ let hit_position = ruby.ary_new_capa(3);
513
+ hit_position.push(x as f64)?;
514
+ hit_position.push(y as f64)?;
515
+ hit_position.push(z as f64)?;
516
+ hash.aset(hit_position_sym, hit_position)?;
517
+ }
518
+
519
+ if let Some((x, y, z)) = event.hit_normal {
520
+ let hit_normal = ruby.ary_new_capa(3);
521
+ hit_normal.push(x as f64)?;
522
+ hit_normal.push(y as f64)?;
523
+ hit_normal.push(z as f64)?;
524
+ hash.aset(hit_normal_sym, hit_normal)?;
525
+ }
526
+
527
+ result.push(hash)?;
528
+ }
529
+
530
+ Ok(result)
531
+ }
532
+ }
533
+
534
+ fn get_hash_value<T: TryConvert>(ruby: &Ruby, hash: &RHash, key: &str) -> Result<Option<T>, Error> {
535
+ let sym = ruby.to_symbol(key);
536
+ match hash.get(sym) {
537
+ Some(val) => {
538
+ if val.is_nil() {
539
+ Ok(None)
540
+ } else {
541
+ Ok(Some(TryConvert::try_convert(val)?))
542
+ }
543
+ }
544
+ None => Ok(None),
545
+ }
546
+ }
547
+
548
+ fn parse_sprite_data(ruby: &Ruby, hash: &RHash) -> Result<SpriteData, Error> {
549
+ let color_r: Option<f64> = get_hash_value(ruby, hash, "color_r")?;
550
+ let color_g: Option<f64> = get_hash_value(ruby, hash, "color_g")?;
551
+ let color_b: Option<f64> = get_hash_value(ruby, hash, "color_b")?;
552
+ let color_a: Option<f64> = get_hash_value(ruby, hash, "color_a")?;
553
+ let flip_x: Option<bool> = get_hash_value(ruby, hash, "flip_x")?;
554
+ let flip_y: Option<bool> = get_hash_value(ruby, hash, "flip_y")?;
555
+ let anchor_x: Option<f64> = get_hash_value(ruby, hash, "anchor_x")?;
556
+ let anchor_y: Option<f64> = get_hash_value(ruby, hash, "anchor_y")?;
557
+ let custom_size_x: Option<f64> = get_hash_value(ruby, hash, "custom_size_x")?;
558
+ let custom_size_y: Option<f64> = get_hash_value(ruby, hash, "custom_size_y")?;
559
+
560
+ let has_custom_size = custom_size_x.is_some() || custom_size_y.is_some();
561
+
562
+ Ok(SpriteData {
563
+ color_r: color_r.unwrap_or(1.0) as f32,
564
+ color_g: color_g.unwrap_or(1.0) as f32,
565
+ color_b: color_b.unwrap_or(1.0) as f32,
566
+ color_a: color_a.unwrap_or(1.0) as f32,
567
+ flip_x: flip_x.unwrap_or(false),
568
+ flip_y: flip_y.unwrap_or(false),
569
+ anchor_x: anchor_x.unwrap_or(0.5) as f32,
570
+ anchor_y: anchor_y.unwrap_or(0.5) as f32,
571
+ has_custom_size,
572
+ custom_size_x: custom_size_x.unwrap_or(0.0) as f32,
573
+ custom_size_y: custom_size_y.unwrap_or(0.0) as f32,
574
+ })
575
+ }
576
+
577
+ fn parse_transform_data(ruby: &Ruby, hash: &RHash) -> Result<TransformData, Error> {
578
+ let x: Option<f64> = get_hash_value(ruby, hash, "x")?;
579
+ let y: Option<f64> = get_hash_value(ruby, hash, "y")?;
580
+ let z: Option<f64> = get_hash_value(ruby, hash, "z")?;
581
+ let rotation: Option<f64> = get_hash_value(ruby, hash, "rotation")?;
582
+ let scale_x: Option<f64> = get_hash_value(ruby, hash, "scale_x")?;
583
+ let scale_y: Option<f64> = get_hash_value(ruby, hash, "scale_y")?;
584
+ let scale_z: Option<f64> = get_hash_value(ruby, hash, "scale_z")?;
585
+
586
+ let angle = rotation.unwrap_or(0.0) as f32;
587
+ let half_angle = angle / 2.0;
588
+ let (sin_half, cos_half) = half_angle.sin_cos();
589
+
590
+ Ok(TransformData {
591
+ translation_x: x.unwrap_or(0.0) as f32,
592
+ translation_y: y.unwrap_or(0.0) as f32,
593
+ translation_z: z.unwrap_or(0.0) as f32,
594
+ rotation_x: 0.0,
595
+ rotation_y: 0.0,
596
+ rotation_z: sin_half,
597
+ rotation_w: cos_half,
598
+ scale_x: scale_x.unwrap_or(1.0) as f32,
599
+ scale_y: scale_y.unwrap_or(1.0) as f32,
600
+ scale_z: scale_z.unwrap_or(1.0) as f32,
601
+ })
602
+ }
603
+
604
+ fn parse_text_data(ruby: &Ruby, hash: &RHash) -> Result<TextData, Error> {
605
+ let content: Option<String> = get_hash_value(ruby, hash, "content")?;
606
+ let font_size: Option<f64> = get_hash_value(ruby, hash, "font_size")?;
607
+ let color_r: Option<f64> = get_hash_value(ruby, hash, "color_r")?;
608
+ let color_g: Option<f64> = get_hash_value(ruby, hash, "color_g")?;
609
+ let color_b: Option<f64> = get_hash_value(ruby, hash, "color_b")?;
610
+ let color_a: Option<f64> = get_hash_value(ruby, hash, "color_a")?;
611
+
612
+ Ok(TextData {
613
+ content: content.unwrap_or_default(),
614
+ font_size: font_size.unwrap_or(24.0) as f32,
615
+ color_r: color_r.unwrap_or(1.0) as f32,
616
+ color_g: color_g.unwrap_or(1.0) as f32,
617
+ color_b: color_b.unwrap_or(1.0) as f32,
618
+ color_a: color_a.unwrap_or(1.0) as f32,
619
+ })
620
+ }
621
+
622
+ fn parse_text_transform_data(ruby: &Ruby, hash: &RHash) -> Result<TextTransformData, Error> {
623
+ let x: Option<f64> = get_hash_value(ruby, hash, "x")?;
624
+ let y: Option<f64> = get_hash_value(ruby, hash, "y")?;
625
+ let z: Option<f64> = get_hash_value(ruby, hash, "z")?;
626
+ let scale_x: Option<f64> = get_hash_value(ruby, hash, "scale_x")?;
627
+ let scale_y: Option<f64> = get_hash_value(ruby, hash, "scale_y")?;
628
+ let scale_z: Option<f64> = get_hash_value(ruby, hash, "scale_z")?;
629
+
630
+ Ok(TextTransformData {
631
+ translation_x: x.unwrap_or(0.0) as f32,
632
+ translation_y: y.unwrap_or(0.0) as f32,
633
+ translation_z: z.unwrap_or(0.0) as f32,
634
+ scale_x: scale_x.unwrap_or(1.0) as f32,
635
+ scale_y: scale_y.unwrap_or(1.0) as f32,
636
+ scale_z: scale_z.unwrap_or(1.0) as f32,
637
+ })
638
+ }
639
+
640
+ fn parse_mesh_data(ruby: &Ruby, hash: &RHash) -> Result<MeshData, Error> {
641
+ let shape_type_val: Option<i64> = get_hash_value(ruby, hash, "shape_type")?;
642
+ let shape_type = match shape_type_val.unwrap_or(0) {
643
+ 0 => ShapeType::Rectangle,
644
+ 1 => ShapeType::Circle,
645
+ 2 => ShapeType::RegularPolygon,
646
+ 3 => ShapeType::Line,
647
+ 4 => ShapeType::Ellipse,
648
+ _ => ShapeType::Rectangle,
649
+ };
650
+
651
+ let color_r: Option<f64> = get_hash_value(ruby, hash, "color_r")?;
652
+ let color_g: Option<f64> = get_hash_value(ruby, hash, "color_g")?;
653
+ let color_b: Option<f64> = get_hash_value(ruby, hash, "color_b")?;
654
+ let color_a: Option<f64> = get_hash_value(ruby, hash, "color_a")?;
655
+ let width: Option<f64> = get_hash_value(ruby, hash, "width")?;
656
+ let height: Option<f64> = get_hash_value(ruby, hash, "height")?;
657
+ let radius: Option<f64> = get_hash_value(ruby, hash, "radius")?;
658
+ let sides: Option<i64> = get_hash_value(ruby, hash, "sides")?;
659
+ let line_start_x: Option<f64> = get_hash_value(ruby, hash, "line_start_x")?;
660
+ let line_start_y: Option<f64> = get_hash_value(ruby, hash, "line_start_y")?;
661
+ let line_end_x: Option<f64> = get_hash_value(ruby, hash, "line_end_x")?;
662
+ let line_end_y: Option<f64> = get_hash_value(ruby, hash, "line_end_y")?;
663
+ let thickness: Option<f64> = get_hash_value(ruby, hash, "thickness")?;
664
+ let fill: Option<bool> = get_hash_value(ruby, hash, "fill")?;
665
+
666
+ Ok(MeshData {
667
+ shape_type,
668
+ color_r: color_r.unwrap_or(1.0) as f32,
669
+ color_g: color_g.unwrap_or(1.0) as f32,
670
+ color_b: color_b.unwrap_or(1.0) as f32,
671
+ color_a: color_a.unwrap_or(1.0) as f32,
672
+ width: width.unwrap_or(100.0) as f32,
673
+ height: height.unwrap_or(100.0) as f32,
674
+ radius: radius.unwrap_or(50.0) as f32,
675
+ sides: sides.unwrap_or(6) as u32,
676
+ line_start_x: line_start_x.unwrap_or(0.0) as f32,
677
+ line_start_y: line_start_y.unwrap_or(0.0) as f32,
678
+ line_end_x: line_end_x.unwrap_or(100.0) as f32,
679
+ line_end_y: line_end_y.unwrap_or(0.0) as f32,
680
+ thickness: thickness.unwrap_or(2.0) as f32,
681
+ fill: fill.unwrap_or(true),
682
+ })
683
+ }
684
+
685
+ fn parse_mesh_transform_data(ruby: &Ruby, hash: &RHash) -> Result<MeshTransformData, Error> {
686
+ let x: Option<f64> = get_hash_value(ruby, hash, "x")?;
687
+ let y: Option<f64> = get_hash_value(ruby, hash, "y")?;
688
+ let z: Option<f64> = get_hash_value(ruby, hash, "z")?;
689
+ let rotation: Option<f64> = get_hash_value(ruby, hash, "rotation")?;
690
+ let scale_x: Option<f64> = get_hash_value(ruby, hash, "scale_x")?;
691
+ let scale_y: Option<f64> = get_hash_value(ruby, hash, "scale_y")?;
692
+ let scale_z: Option<f64> = get_hash_value(ruby, hash, "scale_z")?;
693
+
694
+ let angle = rotation.unwrap_or(0.0) as f32;
695
+ let half_angle = angle / 2.0;
696
+ let (sin_half, cos_half) = half_angle.sin_cos();
697
+
698
+ Ok(MeshTransformData {
699
+ translation_x: x.unwrap_or(0.0) as f32,
700
+ translation_y: y.unwrap_or(0.0) as f32,
701
+ translation_z: z.unwrap_or(0.0) as f32,
702
+ rotation_x: 0.0,
703
+ rotation_y: 0.0,
704
+ rotation_z: sin_half,
705
+ rotation_w: cos_half,
706
+ scale_x: scale_x.unwrap_or(1.0) as f32,
707
+ scale_y: scale_y.unwrap_or(1.0) as f32,
708
+ scale_z: scale_z.unwrap_or(1.0) as f32,
709
+ })
710
+ }
711
+
712
+ pub fn define(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
713
+ let class = module.define_class("RenderApp", ruby.class_object())?;
714
+
715
+ class.define_singleton_method("new", function!(RubyRenderApp::new, -1))?;
716
+ class.define_method("initialize!", method!(RubyRenderApp::initialize, 0))?;
717
+ class.define_method("run", method!(RubyRenderApp::run_with_block, 0))?;
718
+ class.define_method("stop!", method!(RubyRenderApp::stop, 0))?;
719
+ class.define_method("should_close?", method!(RubyRenderApp::should_close, 0))?;
720
+ class.define_method("initialized?", method!(RubyRenderApp::is_initialized, 0))?;
721
+
722
+ class.define_method("key_pressed?", method!(RubyRenderApp::key_pressed, 1))?;
723
+ class.define_method(
724
+ "key_just_pressed?",
725
+ method!(RubyRenderApp::key_just_pressed, 1),
726
+ )?;
727
+ class.define_method(
728
+ "key_just_released?",
729
+ method!(RubyRenderApp::key_just_released, 1),
730
+ )?;
731
+ class.define_method(
732
+ "mouse_button_pressed?",
733
+ method!(RubyRenderApp::mouse_button_pressed, 1),
734
+ )?;
735
+ class.define_method(
736
+ "mouse_button_just_pressed?",
737
+ method!(RubyRenderApp::mouse_button_just_pressed, 1),
738
+ )?;
739
+ class.define_method("mouse_position", method!(RubyRenderApp::mouse_position, 0))?;
740
+ class.define_method("mouse_delta", method!(RubyRenderApp::mouse_delta, 0))?;
741
+ class.define_method("pressed_keys", method!(RubyRenderApp::pressed_keys, 0))?;
742
+ class.define_method("gamepads_state", method!(RubyRenderApp::gamepads_state, 0))?;
743
+
744
+ class.define_method("sync_sprite", method!(RubyRenderApp::sync_sprite, 3))?;
745
+ class.define_method("remove_sprite", method!(RubyRenderApp::remove_sprite, 1))?;
746
+ class.define_method("clear_sprites", method!(RubyRenderApp::clear_sprites, 0))?;
747
+
748
+ class.define_method("sync_text", method!(RubyRenderApp::sync_text, 3))?;
749
+ class.define_method("remove_text", method!(RubyRenderApp::remove_text, 1))?;
750
+ class.define_method("clear_texts", method!(RubyRenderApp::clear_texts, 0))?;
751
+
752
+ class.define_method("sync_mesh", method!(RubyRenderApp::sync_mesh, 3))?;
753
+ class.define_method("remove_mesh", method!(RubyRenderApp::remove_mesh, 1))?;
754
+ class.define_method("clear_meshes", method!(RubyRenderApp::clear_meshes, 0))?;
755
+
756
+ class.define_method(
757
+ "set_camera_position",
758
+ method!(RubyRenderApp::set_camera_position, 3),
759
+ )?;
760
+ class.define_method(
761
+ "camera_position",
762
+ method!(RubyRenderApp::get_camera_position, 0),
763
+ )?;
764
+ class.define_method(
765
+ "set_camera_scale",
766
+ method!(RubyRenderApp::set_camera_scale, 1),
767
+ )?;
768
+ class.define_method("camera_scale", method!(RubyRenderApp::get_camera_scale, 0))?;
769
+ class.define_method(
770
+ "queue_gamepad_rumble",
771
+ method!(RubyRenderApp::queue_gamepad_rumble, 4),
772
+ )?;
773
+ class.define_method(
774
+ "drain_picking_events",
775
+ method!(RubyRenderApp::drain_picking_events, 0),
776
+ )?;
777
+
778
+ Ok(())
779
+ }