@bornengine/engine 0.4.16 → 0.5.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 (62) hide show
  1. package/README.md +24 -14
  2. package/native/android/Cargo.lock +577 -28
  3. package/native/android/Cargo.toml +2 -0
  4. package/native/android/src/lib.rs +39 -3
  5. package/native/ios/Cargo.lock +573 -24
  6. package/native/ios/Cargo.toml +2 -0
  7. package/native/ios/src/lib.rs +72 -9
  8. package/native/ios/src/text_input.rs +48 -0
  9. package/native/linux/Cargo.lock +623 -35
  10. package/native/linux/Cargo.toml +3 -0
  11. package/native/macos/Cargo.lock +630 -35
  12. package/native/macos/Cargo.toml +3 -0
  13. package/native/shared/Cargo.lock +430 -27
  14. package/native/shared/Cargo.toml +5 -0
  15. package/native/shared/build.rs +41 -1
  16. package/native/shared/src/engine.rs +54 -2
  17. package/native/shared/src/ffi.rs +54 -1
  18. package/native/shared/src/ffi_core/mod.rs +2 -0
  19. package/native/shared/src/ffi_core/ui.rs +182 -0
  20. package/native/shared/src/input.rs +317 -12
  21. package/native/shared/src/lib.rs +1 -0
  22. package/native/shared/src/renderer/mod.rs +47 -201
  23. package/native/shared/src/renderer/texture_store.rs +5 -0
  24. package/native/shared/src/renderer/ui_pass.rs +742 -0
  25. package/native/shared/src/textures.rs +2 -0
  26. package/native/shared/src/ui/commands.rs +213 -0
  27. package/native/shared/src/ui/egui.rs +1282 -0
  28. package/native/shared/src/ui/imgui.rs +1056 -0
  29. package/native/shared/src/ui/input.rs +173 -0
  30. package/native/shared/src/ui/mod.rs +252 -0
  31. package/native/shared/src/ui/responses.rs +61 -0
  32. package/native/tvos/Cargo.lock +576 -27
  33. package/native/tvos/Cargo.toml +2 -0
  34. package/native/visionos/Cargo.lock +571 -22
  35. package/native/visionos/Cargo.toml +2 -0
  36. package/native/watchos/src/ffi_stubs.rs +27 -0
  37. package/native/web/Cargo.lock +346 -11
  38. package/native/web/Cargo.toml +6 -0
  39. package/native/web/bloom_glue.js +84 -11
  40. package/native/web/build.sh +150 -34
  41. package/native/web/src/lib.rs +2 -0
  42. package/native/web/src/ui_ffi.rs +148 -0
  43. package/native/windows/Cargo.lock +638 -38
  44. package/native/windows/Cargo.toml +3 -0
  45. package/native/windows/src/lib.rs +50 -1
  46. package/native/windows/src/windows_text.rs +69 -0
  47. package/package.json +91 -2
  48. package/src/debug-ui/index.ts +4 -0
  49. package/src/debug-ui/types.ts +1 -0
  50. package/src/game/adapters/audio-source-component.ts +67 -0
  51. package/src/game/adapters/rigid-body-component.ts +69 -0
  52. package/src/game/adapters/scene-node-component.ts +49 -0
  53. package/src/game/game-component.ts +89 -0
  54. package/src/game/game-object.ts +338 -0
  55. package/src/game/game-scene.ts +303 -0
  56. package/src/game/index.ts +7 -0
  57. package/src/game/transform.ts +315 -0
  58. package/src/index.ts +27 -0
  59. package/src/ui/api.ts +216 -0
  60. package/src/ui/index.ts +5 -0
  61. package/src/ui/opcodes.ts +66 -0
  62. package/src/ui/types.ts +82 -0
@@ -0,0 +1,173 @@
1
+ use std::collections::VecDeque;
2
+
3
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
4
+ pub struct UiPointerButtonEvent {
5
+ pub button: u8,
6
+ pub pressed: bool,
7
+ }
8
+
9
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
10
+ pub struct UiKeyEvent {
11
+ pub key: u32,
12
+ pub pressed: bool,
13
+ pub repeated: bool,
14
+ }
15
+
16
+ #[derive(Clone, Debug, Eq, PartialEq)]
17
+ pub enum UiInputEvent {
18
+ Key(UiKeyEvent),
19
+ Text(String),
20
+ }
21
+
22
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23
+ pub struct UiModifiers {
24
+ pub shift: bool,
25
+ pub ctrl: bool,
26
+ pub alt: bool,
27
+ pub super_key: bool,
28
+ }
29
+
30
+ #[derive(Clone, Copy, Debug, PartialEq)]
31
+ pub struct UiTouchEvent {
32
+ pub slot: usize,
33
+ pub x: f64,
34
+ pub y: f64,
35
+ pub active: bool,
36
+ pub released: bool,
37
+ }
38
+
39
+ #[derive(Clone, Debug, Default, PartialEq)]
40
+ pub struct UiInputSnapshot {
41
+ pub pointer_position: Option<[f64; 2]>,
42
+ pub pointer_delta: [f64; 2],
43
+ pub pointer_buttons: Vec<UiPointerButtonEvent>,
44
+ pub scroll_x: f64,
45
+ pub scroll_y: f64,
46
+ /// Text and keyboard events in their original platform delivery order.
47
+ pub ordered_events: Vec<UiInputEvent>,
48
+ pub keys: Vec<UiKeyEvent>,
49
+ pub modifiers: UiModifiers,
50
+ pub text: Vec<String>,
51
+ pub touches: Vec<UiTouchEvent>,
52
+ }
53
+
54
+ impl UiInputSnapshot {
55
+ pub fn ordered_key_text_events(&self) -> Vec<UiInputEvent> {
56
+ if !self.ordered_events.is_empty() {
57
+ return self.ordered_events.clone();
58
+ }
59
+
60
+ self.keys
61
+ .iter()
62
+ .copied()
63
+ .map(UiInputEvent::Key)
64
+ .chain(self.text.iter().cloned().map(UiInputEvent::Text))
65
+ .collect()
66
+ }
67
+ }
68
+
69
+ #[derive(Default)]
70
+ pub struct UiInputBridge {
71
+ pending_text: Vec<String>,
72
+ text_focused: bool,
73
+ keyboard_requests: VecDeque<bool>,
74
+ }
75
+
76
+ impl UiInputBridge {
77
+ pub fn inject_text(&mut self, text: String) {
78
+ if !text.is_empty() {
79
+ self.pending_text.push(text);
80
+ }
81
+ }
82
+
83
+ pub fn merge_snapshot(&mut self, mut snapshot: UiInputSnapshot) -> UiInputSnapshot {
84
+ if snapshot.ordered_events.is_empty() {
85
+ snapshot.ordered_events.extend(
86
+ snapshot
87
+ .keys
88
+ .iter()
89
+ .copied()
90
+ .map(UiInputEvent::Key)
91
+ .chain(snapshot.text.iter().cloned().map(UiInputEvent::Text)),
92
+ );
93
+ }
94
+ for text in self.pending_text.drain(..) {
95
+ snapshot
96
+ .ordered_events
97
+ .push(UiInputEvent::Text(text.clone()));
98
+ snapshot.text.push(text);
99
+ }
100
+ snapshot
101
+ }
102
+
103
+ pub fn set_text_focus(&mut self, focused: bool) {
104
+ if focused != self.text_focused {
105
+ self.text_focused = focused;
106
+ self.keyboard_requests.push_back(focused);
107
+ }
108
+ }
109
+
110
+ pub fn take_keyboard_request(&mut self) -> Option<bool> {
111
+ self.keyboard_requests.pop_front()
112
+ }
113
+
114
+ #[cfg(test)]
115
+ pub fn take_keyboard_requests(&mut self) -> Vec<bool> {
116
+ self.keyboard_requests.drain(..).collect()
117
+ }
118
+ }
119
+
120
+ #[cfg(test)]
121
+ mod tests {
122
+ use super::{UiInputBridge, UiInputEvent, UiInputSnapshot, UiKeyEvent};
123
+
124
+ #[test]
125
+ fn ui_text_injection_preserves_a_complete_unicode_event() {
126
+ let mut bridge = UiInputBridge::default();
127
+ bridge.inject_text("東京".to_owned());
128
+ let snapshot = bridge.merge_snapshot(UiInputSnapshot::default());
129
+
130
+ assert_eq!(snapshot.text, ["東京"]);
131
+ assert_eq!(
132
+ snapshot.ordered_events,
133
+ [UiInputEvent::Text("東京".to_owned())]
134
+ );
135
+ }
136
+
137
+ #[test]
138
+ fn bridge_appends_injected_text_after_existing_key_events() {
139
+ let mut bridge = UiInputBridge::default();
140
+ bridge.inject_text("x".to_owned());
141
+
142
+ let snapshot = bridge.merge_snapshot(UiInputSnapshot {
143
+ keys: vec![UiKeyEvent {
144
+ key: 8,
145
+ pressed: true,
146
+ repeated: false,
147
+ }],
148
+ ..Default::default()
149
+ });
150
+
151
+ assert_eq!(
152
+ snapshot.ordered_events,
153
+ [
154
+ UiInputEvent::Key(UiKeyEvent {
155
+ key: 8,
156
+ pressed: true,
157
+ repeated: false,
158
+ }),
159
+ UiInputEvent::Text("x".to_owned()),
160
+ ]
161
+ );
162
+ }
163
+
164
+ #[test]
165
+ fn keyboard_focus_requests_only_on_transitions() {
166
+ let mut bridge = UiInputBridge::default();
167
+ bridge.set_text_focus(true);
168
+ bridge.set_text_focus(true);
169
+ bridge.set_text_focus(false);
170
+
171
+ assert_eq!(bridge.take_keyboard_requests(), [true, false]);
172
+ }
173
+ }
@@ -0,0 +1,252 @@
1
+ mod commands;
2
+ mod egui;
3
+ #[cfg(feature = "debug-ui")]
4
+ mod imgui;
5
+ mod input;
6
+ mod responses;
7
+
8
+ pub use commands::{UiCommand, UiOpcode};
9
+ pub use egui::{EguiFrameOutput, EguiUi};
10
+ #[cfg(feature = "debug-ui")]
11
+ pub(crate) use imgui::with_dear_imgui;
12
+ #[cfg(feature = "debug-ui")]
13
+ pub use imgui::DearImGuiUi;
14
+ pub use input::{
15
+ UiInputBridge, UiInputEvent, UiInputSnapshot, UiKeyEvent, UiModifiers, UiPointerButtonEvent,
16
+ UiTouchEvent,
17
+ };
18
+ pub use responses::UiResponse;
19
+
20
+ use std::collections::HashMap;
21
+
22
+ #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23
+ #[repr(u32)]
24
+ pub enum UiBackend {
25
+ Egui = 0,
26
+ DearImGui = 1,
27
+ }
28
+
29
+ impl UiBackend {
30
+ pub fn from_abi(value: f64) -> Option<Self> {
31
+ if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value > u32::MAX as f64 {
32
+ return None;
33
+ }
34
+ match value as u32 {
35
+ 0 => Some(Self::Egui),
36
+ 1 => Some(Self::DearImGui),
37
+ _ => None,
38
+ }
39
+ }
40
+ }
41
+
42
+ pub fn id_from_abi(value: f64) -> Option<u32> {
43
+ if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value > u32::MAX as f64 {
44
+ return None;
45
+ }
46
+ Some(value as u32)
47
+ }
48
+
49
+ #[derive(Clone, Copy, Debug, Default, PartialEq)]
50
+ pub struct UiCaptureState {
51
+ pub pointer: bool,
52
+ pub keyboard: bool,
53
+ }
54
+
55
+ const MAX_QUEUED_COMMANDS: usize = 16_384;
56
+ const MAX_SCRATCH_VALUES: usize = 1_048_576;
57
+
58
+ fn color_channel(value: f64) -> u8 {
59
+ if value.is_finite() {
60
+ value.clamp(0.0, 255.0).round() as u8
61
+ } else {
62
+ 255
63
+ }
64
+ }
65
+
66
+ #[derive(Default)]
67
+ pub struct UiSystem {
68
+ commands: Vec<UiCommand>,
69
+ completed_responses: HashMap<UiBackend, HashMap<u32, UiResponse>>,
70
+ capture: HashMap<UiBackend, UiCaptureState>,
71
+ scratch: HashMap<UiBackend, Vec<f64>>,
72
+ input_bridge: UiInputBridge,
73
+ input_snapshot: UiInputSnapshot,
74
+ egui: EguiUi,
75
+ }
76
+
77
+ impl UiSystem {
78
+ /// Start a new TypeScript command frame without clearing readback values
79
+ /// from the most recently completed UI frame.
80
+ pub fn begin_frame(&mut self) {
81
+ self.commands.clear();
82
+ self.scratch.clear();
83
+ }
84
+
85
+ pub fn queue_command(&mut self, command: UiCommand) -> bool {
86
+ if self.commands.len() >= MAX_QUEUED_COMMANDS {
87
+ return false;
88
+ }
89
+ self.commands.push(command);
90
+ true
91
+ }
92
+
93
+ pub fn take_commands(&mut self, backend: UiBackend) -> Vec<UiCommand> {
94
+ let mut selected = Vec::new();
95
+ self.commands.retain(|command| {
96
+ if command.backend == backend {
97
+ selected.push(command.clone());
98
+ false
99
+ } else {
100
+ true
101
+ }
102
+ });
103
+ selected
104
+ }
105
+
106
+ pub fn response(&self, backend: UiBackend, id: u32) -> UiResponse {
107
+ self.completed_responses
108
+ .get(&backend)
109
+ .and_then(|responses| responses.get(&id))
110
+ .cloned()
111
+ .unwrap_or_default()
112
+ }
113
+
114
+ pub fn has_response(&self, backend: UiBackend, id: u32) -> bool {
115
+ self.completed_responses
116
+ .get(&backend)
117
+ .is_some_and(|responses| responses.contains_key(&id))
118
+ }
119
+
120
+ pub fn response_text(&self, backend: UiBackend, id: u32) -> String {
121
+ self.response(backend, id).text
122
+ }
123
+
124
+ pub fn set_input_snapshot(&mut self, snapshot: UiInputSnapshot) {
125
+ self.input_snapshot = self.input_bridge.merge_snapshot(snapshot);
126
+ }
127
+
128
+ pub fn input_snapshot(&self) -> &UiInputSnapshot {
129
+ &self.input_snapshot
130
+ }
131
+
132
+ pub fn evaluate_egui(
133
+ &mut self,
134
+ screen_rect: [f32; 4],
135
+ pixels_per_point: f32,
136
+ dt: f64,
137
+ ) -> EguiFrameOutput {
138
+ self.egui.set_native_pixels_per_point(pixels_per_point);
139
+ let commands = self.take_commands(UiBackend::Egui);
140
+ let output = self
141
+ .egui
142
+ .run_frame(&commands, self.input_snapshot.clone(), screen_rect, dt);
143
+ self.publish_completed_responses(
144
+ UiBackend::Egui,
145
+ output
146
+ .responses
147
+ .iter()
148
+ .map(|(&id, response)| (id, response.clone()))
149
+ .collect(),
150
+ );
151
+ self.set_capture(
152
+ UiBackend::Egui,
153
+ UiCaptureState {
154
+ pointer: output.wants_pointer_input,
155
+ keyboard: output.wants_keyboard_input,
156
+ },
157
+ );
158
+ self.set_text_focus(output.text_edit_focused);
159
+ output
160
+ }
161
+
162
+ pub fn publish_completed_responses(
163
+ &mut self,
164
+ backend: UiBackend,
165
+ responses: Vec<(u32, UiResponse)>,
166
+ ) {
167
+ self.completed_responses
168
+ .insert(backend, responses.into_iter().collect());
169
+ }
170
+
171
+ pub fn set_capture(&mut self, backend: UiBackend, capture: UiCaptureState) {
172
+ self.capture.insert(backend, capture);
173
+ }
174
+
175
+ pub fn wants_input(&self, backend: UiBackend, kind: u32) -> bool {
176
+ let capture = self.capture.get(&backend).copied().unwrap_or_default();
177
+ match kind {
178
+ 0 => capture.pointer,
179
+ 1 => capture.keyboard,
180
+ _ => false,
181
+ }
182
+ }
183
+
184
+ pub fn is_available(&self, backend: UiBackend) -> bool {
185
+ match backend {
186
+ UiBackend::Egui => true,
187
+ UiBackend::DearImGui => cfg!(feature = "debug-ui"),
188
+ }
189
+ }
190
+
191
+ pub(crate) fn registered_texture_handles(&self, backend: UiBackend) -> Vec<u64> {
192
+ self.commands
193
+ .iter()
194
+ .filter(|command| {
195
+ command.backend == backend && command.opcode == UiOpcode::RegisterTexture
196
+ })
197
+ .filter_map(|command| {
198
+ let handle = command.args[0];
199
+ (handle.is_finite()
200
+ && handle >= 0.0
201
+ && handle.fract() == 0.0
202
+ && handle <= u64::MAX as f64)
203
+ .then_some(handle as u64)
204
+ })
205
+ .collect()
206
+ }
207
+
208
+ pub fn inject_text(&mut self, text: String) {
209
+ self.input_bridge.inject_text(text);
210
+ }
211
+
212
+ pub fn set_text_focus(&mut self, focused: bool) {
213
+ self.input_bridge.set_text_focus(focused);
214
+ }
215
+
216
+ pub fn take_keyboard_request(&mut self) -> Option<bool> {
217
+ self.input_bridge.take_keyboard_request()
218
+ }
219
+
220
+ pub fn reset_scratch(&mut self, backend: UiBackend) {
221
+ self.scratch.insert(backend, Vec::new());
222
+ }
223
+
224
+ pub fn push_scratch(&mut self, backend: UiBackend, value: f64) -> bool {
225
+ let values = self.scratch.entry(backend).or_default();
226
+ if values.len() >= MAX_SCRATCH_VALUES || !value.is_finite() {
227
+ return false;
228
+ }
229
+ values.push(value);
230
+ true
231
+ }
232
+
233
+ pub fn queue_scratch_command(
234
+ &mut self,
235
+ backend: UiBackend,
236
+ opcode: UiOpcode,
237
+ id: u32,
238
+ count: usize,
239
+ text: impl Into<String>,
240
+ ) -> bool {
241
+ let Some(values) = self.scratch.get_mut(&backend) else {
242
+ return false;
243
+ };
244
+ if count > values.len() || self.commands.len() >= MAX_QUEUED_COMMANDS {
245
+ return false;
246
+ }
247
+ let scratch = values.drain(..count).collect();
248
+ self.commands
249
+ .push(UiCommand::new(backend, opcode, id, [0.0; 4], text).with_scratch(scratch));
250
+ true
251
+ }
252
+ }
@@ -0,0 +1,61 @@
1
+ #[derive(Clone, Debug, Default, PartialEq)]
2
+ pub struct UiResponse {
3
+ pub clicked: bool,
4
+ pub changed: bool,
5
+ pub hovered: bool,
6
+ pub focused: bool,
7
+ pub dragged: bool,
8
+ pub value: f64,
9
+ pub text: String,
10
+ }
11
+
12
+ #[cfg(test)]
13
+ mod tests {
14
+ use crate::ui::{UiBackend, UiResponse, UiSystem};
15
+
16
+ #[test]
17
+ fn completed_responses_survive_the_next_game_callback_then_replace() {
18
+ let mut ui = UiSystem::default();
19
+ ui.begin_frame();
20
+ ui.publish_completed_responses(
21
+ UiBackend::Egui,
22
+ vec![(
23
+ 42,
24
+ UiResponse {
25
+ clicked: true,
26
+ ..Default::default()
27
+ },
28
+ )],
29
+ );
30
+
31
+ assert!(ui.response(UiBackend::Egui, 42).clicked);
32
+ assert!(!ui.response(UiBackend::DearImGui, 42).clicked);
33
+
34
+ // begin_frame runs before the TypeScript callback; preserve the last
35
+ // completed response so that callback can read it.
36
+ ui.begin_frame();
37
+ assert!(ui.response(UiBackend::Egui, 42).clicked);
38
+
39
+ // The response set is replaced only after the next UI evaluation.
40
+ ui.publish_completed_responses(UiBackend::Egui, vec![]);
41
+ assert!(!ui.response(UiBackend::Egui, 42).clicked);
42
+ }
43
+
44
+ #[test]
45
+ fn missing_response_returns_default_for_backend_and_id() {
46
+ let mut ui = UiSystem::default();
47
+ ui.publish_completed_responses(
48
+ UiBackend::Egui,
49
+ vec![(
50
+ 42,
51
+ UiResponse {
52
+ clicked: true,
53
+ ..Default::default()
54
+ },
55
+ )],
56
+ );
57
+
58
+ assert_eq!(ui.response(UiBackend::Egui, 404), UiResponse::default());
59
+ assert_eq!(ui.response(UiBackend::DearImGui, 42), UiResponse::default());
60
+ }
61
+ }