gizmo_app/windowed.rs
1use crate::dev_console;
2use gizmo_core::system::Schedule;
3use gizmo_core::world::World;
4use gizmo_editor::gui::EditorContext;
5use gizmo_renderer::renderer::Renderer;
6use gizmo_renderer::RenderContext;
7use std::sync::atomic::{AtomicPtr, Ordering};
8use std::sync::Arc;
9use winit::{
10 event::{Event, WindowEvent},
11 event_loop::{ControlFlow, EventLoop},
12 window::WindowBuilder,
13};
14
15static WORLD_PTR: AtomicPtr<gizmo_core::world::World> = AtomicPtr::new(std::ptr::null_mut());
16
17// setup_panic_hook ve Plugin lib.rs ve plugin.rs'ye taşındı.
18
19pub struct AssetPlugin;
20
21impl<State: 'static> super::Plugin<State> for AssetPlugin {
22 fn build(&self, app: &mut App<State>) {
23 app.world
24 .insert_resource(gizmo_core::asset::Assets::<gizmo_renderer::components::Mesh>::new());
25 app.world.insert_resource(gizmo_core::asset::Assets::<
26 gizmo_renderer::components::Material,
27 >::new());
28 }
29}
30
31pub struct App<State: 'static = ()> {
32 pub world: World,
33 pub schedule: Schedule,
34 window_title: String,
35 window_size: (u32, u32),
36
37 setup_fn: Option<Box<dyn FnOnce(&mut World, &Renderer) -> State + 'static>>,
38 update_fn: Option<Box<dyn FnMut(&mut World, &mut State, f32, &gizmo_core::input::Input)>>, // dt, input
39 render_fn: Option<
40 Box<
41 dyn FnMut(
42 &mut World,
43 &State,
44 &mut wgpu::CommandEncoder,
45 &wgpu::TextureView,
46 &mut Renderer,
47 f32,
48 ),
49 >,
50 >, // light_time
51 simple_render_fn: Option<Box<dyn for<'a> FnMut(&mut World, &State, &mut RenderContext<'a>)>>,
52 input_fn: Option<Box<dyn FnMut(&mut World, &mut State, &winit::event::Event<()>) -> bool>>, // Input handler
53 ui_fn: Option<Box<dyn FnMut(&mut World, &mut State, &egui::Context)>>, // Editor UI handler
54 pub input: gizmo_core::input::Input,
55 #[allow(clippy::type_complexity)]
56 event_updaters: Vec<Box<dyn FnMut(&mut World)>>,
57 initial_scene: Option<String>,
58 window_icon: Option<&'static [u8]>,
59 pub record_mode: bool,
60 pub playback_file: Option<String>,
61 record_data: Option<gizmo_core::input::PlaybackData>,
62 playback_data: Option<gizmo_core::input::PlaybackData>,
63 playback_frame_index: usize,
64 runner: Option<Box<dyn FnOnce(App<State>)>>,
65 embedded_assets: std::collections::HashMap<String, std::borrow::Cow<'static, [u8]>>,
66}
67
68impl<State: 'static> App<State> {
69 pub fn new(title: &str, width: u32, height: u32) -> Self {
70 super::setup_panic_hook();
71 let mut app = Self {
72 world: World::new(),
73 schedule: Schedule::new(),
74 window_title: title.to_string(),
75 window_size: (width, height),
76 setup_fn: None,
77 update_fn: None,
78 render_fn: None,
79 simple_render_fn: None,
80 input_fn: None,
81 ui_fn: None,
82 input: gizmo_core::input::Input::new(),
83 event_updaters: Vec::new(),
84 initial_scene: None,
85 window_icon: None,
86 record_mode: false,
87 playback_file: None,
88 record_data: None,
89 playback_data: None,
90 playback_frame_index: 0,
91 runner: None,
92 embedded_assets: std::collections::HashMap::new(),
93 };
94 app = app.add_plugin(AssetPlugin);
95 app
96 }
97
98 pub fn set_runner<F>(mut self, f: F) -> Self
99 where
100 F: FnOnce(App<State>) + 'static,
101 {
102 self.runner = Some(Box::new(f));
103 self
104 }
105
106 pub fn set_runner_mut<F>(&mut self, f: F)
107 where
108 F: FnOnce(App<State>) + 'static,
109 {
110 self.runner = Some(Box::new(f));
111 }
112
113 pub fn start_recording(mut self) -> Self {
114 self.record_mode = true;
115 self.record_data = Some(gizmo_core::input::PlaybackData { frames: Vec::new() });
116 self
117 }
118
119 pub fn start_playback(mut self, path: &str) -> Self {
120 self.playback_file = Some(path.to_string());
121 self
122 }
123
124 /// Sisteme yeni bir Olay (Event) türü kaydeder.
125 /// Bu işlem sayesinde her kare bitişinde çift-buffer `update()` otomatik çalışır.
126 pub fn add_event<T: 'static + Send + Sync>(mut self) -> Self {
127 self.world
128 .insert_resource(gizmo_core::event::Events::<T>::new());
129 self.event_updaters.push(Box::new(|world| {
130 if let Some(mut events) = world.get_resource_mut::<gizmo_core::event::Events<T>>() {
131 events.update();
132 }
133 }));
134 self
135 }
136
137 pub fn with_icon(mut self, icon_bytes: &'static [u8]) -> Self {
138 self.window_icon = Some(icon_bytes);
139 self
140 }
141
142 pub fn add_plugin<P: super::Plugin<State>>(mut self, plugin: P) -> Self {
143 plugin.build(&mut self);
144 self
145 }
146
147 pub fn add_embedded_asset(mut self, path: &str, data: std::borrow::Cow<'static, [u8]>) -> Self {
148 self.embedded_assets.insert(path.to_string(), data);
149 self
150 }
151
152 pub fn set_setup<F>(mut self, f: F) -> Self
153 where
154 F: FnOnce(&mut World, &Renderer) -> State + 'static,
155 {
156 self.setup_fn = Some(Box::new(f));
157 self
158 }
159
160 pub fn set_update<F>(mut self, f: F) -> Self
161 where
162 F: FnMut(&mut World, &mut State, f32, &gizmo_core::input::Input) + 'static,
163 {
164 self.update_fn = Some(Box::new(f));
165 self
166 }
167
168 pub fn set_render<F>(mut self, f: F) -> Self
169 where
170 F: FnMut(
171 &mut World,
172 &State,
173 &mut wgpu::CommandEncoder,
174 &wgpu::TextureView,
175 &mut Renderer,
176 f32,
177 ) + 'static,
178 {
179 self.render_fn = Some(Box::new(f));
180 self
181 }
182
183 /// Yeni, basit Render fonksiyonunu (RenderContext) ekler
184 pub fn set_simple_render<F>(mut self, f: F) -> Self
185 where
186 F: for<'a> FnMut(&mut World, &State, &mut RenderContext<'a>) + 'static,
187 {
188 self.simple_render_fn = Some(Box::new(f));
189 self
190 }
191
192 pub fn set_input<F>(mut self, f: F) -> Self
193 where
194 F: FnMut(&mut World, &mut State, &Event<()>) -> bool + 'static,
195 {
196 self.input_fn = Some(Box::new(f));
197 self
198 }
199
200 pub fn set_ui<F>(mut self, f: F) -> Self
201 where
202 F: FnMut(&mut World, &mut State, &egui::Context) + 'static,
203 {
204 self.ui_fn = Some(Box::new(f));
205 self
206 }
207
208 pub fn add_system<Params, S: gizmo_core::system::IntoSystemConfig<Params>>(
209 mut self,
210 system: S,
211 ) -> Self {
212 self.schedule.add_di_system(system);
213 self
214 }
215
216 pub fn configure_set(mut self, config: gizmo_core::system::SetConfig) -> Self {
217 self.schedule.configure_set(config);
218 self
219 }
220
221 pub fn load_scene(mut self, path: &str) -> Self {
222 self.initial_scene = Some(path.to_string());
223 self
224 }
225
226 pub fn run(mut self) {
227 if let Some(runner) = self.runner.take() {
228 runner(self);
229 return;
230 }
231 self.run_default();
232 }
233
234 fn run_default(mut self) {
235 super::setup_panic_hook();
236 WORLD_PTR.store(&mut self.world as *mut _, Ordering::Release);
237
238 if let Some(ref path) = self.playback_file {
239 match gizmo_core::input::PlaybackData::load(path) {
240 Ok(data) => {
241 self.playback_data = Some(data);
242 tracing::info!("Playback loaded from: {}", path);
243 }
244 Err(e) => {
245 tracing::error!("Failed to load playback data: {}", e);
246 }
247 }
248 }
249
250 let event_loop = EventLoop::new().expect("Event Loop başlatılamadı");
251 let mut builder = WindowBuilder::new()
252 .with_title(&self.window_title)
253 .with_inner_size(winit::dpi::LogicalSize::new(
254 self.window_size.0,
255 self.window_size.1,
256 ));
257
258 #[cfg(target_arch = "wasm32")]
259 {
260 use wasm_bindgen::JsCast;
261 use winit::platform::web::WindowBuilderExtWebSys;
262 // Canvas'ı body'ye ekle ve boyutu ayarla
263 let canvas = web_sys::window()
264 .and_then(|win| win.document())
265 .and_then(|doc| {
266 let canvas = doc.create_element("canvas").ok()?;
267 let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into().ok()?;
268 canvas.set_width(1280);
269 canvas.set_height(720);
270 canvas.style().set_property("width", "100%").ok()?;
271 canvas.style().set_property("height", "100%").ok()?;
272 doc.body()?.append_child(&canvas).ok()?;
273 Some(canvas)
274 });
275 if let Some(canvas) = canvas {
276 builder = builder.with_canvas(Some(canvas));
277 } else {
278 builder = builder.with_append(true);
279 }
280 }
281
282 if let Some(icon_bytes) = self.window_icon {
283 if let Ok(image) = image::load_from_memory(icon_bytes) {
284 let rgba = image.into_rgba8();
285 let (width, height) = rgba.dimensions();
286 if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
287 builder = builder.with_window_icon(Some(icon));
288 }
289 }
290 }
291
292 let window = Arc::new(builder.build(&event_loop).expect("Pencere oluşturulamadı!"));
293
294 #[cfg(not(target_arch = "wasm32"))]
295 {
296 pollster::block_on(self.run_internal(event_loop, window));
297 }
298 #[cfg(target_arch = "wasm32")]
299 {
300 wasm_bindgen_futures::spawn_local(self.run_internal(event_loop, window));
301 }
302 }
303
304 async fn run_internal(mut self, event_loop: EventLoop<()>, window: Arc<winit::window::Window>) {
305 // Initialize Core Dev Console Systems BEFORE setup so setup can register cvars
306 self.world
307 .insert_resource(gizmo_core::cvar::CVarRegistry::new());
308 // Window Resource oluştur ve World'e ekle
309 self.world.insert_resource(gizmo_core::window::WindowInfo {
310 width: self.window_size.0 as f32,
311 height: self.window_size.1 as f32,
312 });
313
314 // Renderer Resource oluştur ve World'e ekle
315 let renderer = Renderer::new(window.clone()).await;
316 renderer.asset_manager.write().unwrap().embedded_assets =
317 std::mem::take(&mut self.embedded_assets);
318 self.world.insert_resource(renderer);
319
320 let mut state = if let Some(setup) = self.setup_fn.take() {
321 let r = self.world.remove_resource::<Renderer>().unwrap();
322 let state = setup(&mut self.world, &r);
323 self.world.insert_resource(r);
324 state
325 } else {
326 panic!("setup() fonksiyonu atanmadi! Lütfen set_setup çağırın veya State yapılandırmanızı kontrol edin.");
327 };
328
329 if let Some(scene_path) = self.initial_scene.take() {
330 if let Some(asset_manager) = self
331 .world
332 .remove_resource::<gizmo_renderer::asset::AssetManager>()
333 {
334 let dummy_rgba = [255, 255, 255, 255];
335 let r = self.world.remove_resource::<Renderer>().unwrap();
336 let _dummy_bg = r.create_texture(&dummy_rgba, 1, 1);
337
338 {
339 gizmo_scene::scene::SceneData::load_into(
340 &scene_path,
341 &mut self.world,
342 &gizmo_scene::registry::default_scene_registry(),
343 );
344 }
345
346 self.world.insert_resource(r);
347 self.world.insert_resource(asset_manager);
348 } else {
349 tracing::error!("[App::run] AssetManager bulunamadı, sahne yüklenemiyor!");
350 }
351 }
352
353 let mut editor = {
354 let r = self.world.get_resource::<Renderer>().unwrap();
355 EditorContext::new(&r.device, r.config.format, &window, 1)
356 };
357
358 #[cfg(not(target_arch = "wasm32"))]
359 let mut last_frame_time = std::time::Instant::now();
360 #[cfg(target_arch = "wasm32")]
361 let mut last_frame_time = web_time::Instant::now();
362 let mut light_time = 0.0;
363
364 event_loop
365 .run(move |event, current_window| {
366 current_window.set_control_flow(ControlFlow::Poll);
367
368 let mut consumes_input = false;
369
370 // UI Entegrasyonu: Winit Olaylarını EGUI'ye Gönder
371 if let Event::WindowEvent {
372 ref event,
373 window_id,
374 } = event
375 {
376 if window_id == window.id() {
377 consumes_input = editor.handle_event(&window, event);
378 }
379 }
380
381 // Eğer UI girdiyi yakalamadıysa Kullanıcı Input Hook'a Yolla
382 if !consumes_input {
383 if let Some(input_hk) = self.input_fn.as_mut() {
384 let _ = input_hk(&mut self.world, &mut state, &event);
385 }
386 }
387
388 match event {
389 Event::WindowEvent {
390 ref event,
391 window_id,
392 } if window_id == window.id() => {
393 match event {
394 WindowEvent::CloseRequested => {
395 if let Some(record) = &self.record_data {
396 let _ = record.save("gizmo_record.ron");
397 tracing::info!(
398 "Kayit basariyla 'gizmo_record.ron' dosyasina kaydedildi."
399 );
400 }
401 // TLS'teki GPU kaynaklarını temizlemekle uğraşmak yerine direkt çık
402 #[cfg(not(target_arch = "wasm32"))]
403 std::process::exit(0);
404 #[cfg(target_arch = "wasm32")]
405 current_window.exit();
406 }
407 WindowEvent::Resized(physical_size) => {
408 {
409 let mut r = self.world.get_resource_mut::<Renderer>().unwrap();
410 r.resize(*physical_size);
411 }
412 let mut win_info = self
413 .world
414 .get_resource_mut_or_default::<gizmo_core::window::WindowInfo>(
415 );
416 win_info.width = physical_size.width as f32;
417 win_info.height = physical_size.height as f32;
418 }
419 WindowEvent::KeyboardInput {
420 event: kb_event, ..
421 } => {
422 let mut codes_to_press = Vec::new();
423 // Fiziksel Tuş (PhysicalKey)
424 if let winit::keyboard::PhysicalKey::Code(keycode) =
425 kb_event.physical_key
426 {
427 codes_to_press.push(keycode as u32);
428 }
429 // Mantıksal Tuş (LogicalKey Fallback)
430 if codes_to_press.is_empty() {
431 if let winit::keyboard::Key::Character(c) =
432 kb_event.logical_key.as_ref()
433 {
434 match c.to_lowercase().as_str() {
435 "w" => codes_to_press
436 .push(winit::keyboard::KeyCode::KeyW as u32),
437 "a" => codes_to_press
438 .push(winit::keyboard::KeyCode::KeyA as u32),
439 "s" => codes_to_press
440 .push(winit::keyboard::KeyCode::KeyS as u32),
441 "d" => codes_to_press
442 .push(winit::keyboard::KeyCode::KeyD as u32),
443 _ => {}
444 }
445 } else if let winit::keyboard::Key::Named(named) =
446 kb_event.logical_key
447 {
448 match named {
449 winit::keyboard::NamedKey::ArrowUp => codes_to_press
450 .push(winit::keyboard::KeyCode::ArrowUp as u32),
451 winit::keyboard::NamedKey::ArrowDown => codes_to_press
452 .push(winit::keyboard::KeyCode::ArrowDown as u32),
453 winit::keyboard::NamedKey::ArrowLeft => codes_to_press
454 .push(winit::keyboard::KeyCode::ArrowLeft as u32),
455 winit::keyboard::NamedKey::ArrowRight => codes_to_press
456 .push(winit::keyboard::KeyCode::ArrowRight as u32),
457 winit::keyboard::NamedKey::Space => codes_to_press
458 .push(winit::keyboard::KeyCode::Space as u32),
459 winit::keyboard::NamedKey::Escape => codes_to_press
460 .push(winit::keyboard::KeyCode::Escape as u32),
461 _ => {}
462 }
463 }
464 } // Ends the 'if codes_to_press.is_empty()' block
465
466 for code in codes_to_press {
467 if kb_event.state == winit::event::ElementState::Pressed {
468 self.input.on_key_pressed(code);
469 } else {
470 self.input.on_key_released(code);
471 }
472 }
473 }
474 WindowEvent::MouseInput {
475 state: m_state,
476 button,
477 ..
478 } => {
479 let btn_code = match button {
480 winit::event::MouseButton::Left => {
481 gizmo_core::input::mouse::LEFT
482 }
483 winit::event::MouseButton::Right => {
484 gizmo_core::input::mouse::RIGHT
485 }
486 winit::event::MouseButton::Middle => {
487 gizmo_core::input::mouse::MIDDLE
488 }
489 _ => u32::MAX,
490 };
491 if btn_code != u32::MAX {
492 if *m_state == winit::event::ElementState::Pressed {
493 self.input.on_mouse_button_pressed(btn_code);
494 } else {
495 self.input.on_mouse_button_released(btn_code);
496 }
497 }
498 }
499 WindowEvent::CursorMoved { position, .. } => {
500 self.input
501 .on_mouse_moved(position.x as f32, position.y as f32);
502 }
503 _ => {}
504 }
505 if let WindowEvent::RedrawRequested = event {
506 #[cfg(not(target_arch = "wasm32"))]
507 let now = std::time::Instant::now();
508 #[cfg(target_arch = "wasm32")]
509 let now = web_time::Instant::now();
510 let mut dt = now.duration_since(last_frame_time).as_secs_f32();
511 dt = dt.min(0.05); // Güvenlik çemberi: Frame takılırsa 50ms'den fazla zıplamayacak, yerçekiminden düşme engellenecek.
512 last_frame_time = now;
513
514 // Playback / Record mantigi
515 if let Some(playback) = &self.playback_data {
516 if self.playback_frame_index < playback.frames.len() {
517 let frame = &playback.frames[self.playback_frame_index];
518 dt = frame.dt;
519 self.input = frame.input.clone();
520 self.playback_frame_index += 1;
521 } else {
522 tracing::info!("Playback bitti. Uygulama kapaniyor...");
523 current_window.exit();
524 }
525 } else if self.record_mode {
526 if let Some(record) = &mut self.record_data {
527 record.frames.push(gizmo_core::input::FrameRecord {
528 dt,
529 input: self.input.clone(),
530 });
531 }
532 }
533
534 light_time += dt;
535
536 // Update
537 let full_output = editor.run(&window, |ctx| {
538 if let Some(ui_hk) = self.ui_fn.as_mut() {
539 ui_hk(&mut self.world, &mut state, ctx);
540 }
541
542 // Render Global Dev Console on top of everything
543 dev_console::ui_dev_console(&mut self.world, ctx, &self.input);
544 });
545
546 // --- Scene View RTT (Render To Texture) YÖNETİMİ ---
547 if self
548 .world
549 .get_resource::<gizmo_editor::EditorState>()
550 .is_some()
551 {
552 let mut ed_state_ref = self
553 .world
554 .get_resource_mut::<gizmo_editor::EditorState>()
555 .unwrap();
556 let (rw, rh) = {
557 let r = self.world.get_resource::<Renderer>().unwrap();
558 (r.size.width, r.size.height)
559 };
560 let scene_w = ed_state_ref
561 .scene_view_size
562 .map(|s| s.x as u32)
563 .unwrap_or(rw);
564 let scene_h = ed_state_ref
565 .scene_view_size
566 .map(|s| s.y as u32)
567 .unwrap_or(rh);
568 let game_w = ed_state_ref
569 .game_view_size
570 .map(|s| s.x as u32)
571 .unwrap_or(rw);
572 let game_h = ed_state_ref
573 .game_view_size
574 .map(|s| s.y as u32)
575 .unwrap_or(rh);
576
577 let mut new_scene_target = None;
578 let mut new_game_target = None;
579
580 // Scene View RTT
581 let mut needs_recreate_scene = false;
582 if let Some(target) = self
583 .world
584 .get_resource::<gizmo_renderer::components::EditorRenderTarget>(
585 ) {
586 if target.0.width != scene_w || target.0.height != scene_h {
587 needs_recreate_scene = true;
588 }
589 } else {
590 needs_recreate_scene = true;
591 }
592
593 if needs_recreate_scene && scene_w > 0 && scene_h > 0 {
594 if let Some(old_id) = ed_state_ref.scene_texture_id {
595 editor.renderer.free_texture(&old_id);
596 }
597 let tex_id;
598 {
599 let r = self.world.get_resource::<Renderer>().unwrap();
600 let texture =
601 r.device.create_texture(&wgpu::TextureDescriptor {
602 label: Some("Editor RTT"),
603 size: wgpu::Extent3d {
604 width: scene_w,
605 height: scene_h,
606 depth_or_array_layers: 1,
607 },
608 mip_level_count: 1,
609 sample_count: 1,
610 dimension: wgpu::TextureDimension::D2,
611 format: r.config.format,
612 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
613 | wgpu::TextureUsages::TEXTURE_BINDING,
614 view_formats: &[],
615 });
616 let view = texture
617 .create_view(&wgpu::TextureViewDescriptor::default());
618 tex_id = Some(editor.renderer.register_native_texture(
619 &r.device,
620 &view,
621 wgpu::FilterMode::Linear,
622 ));
623 new_scene_target =
624 Some((std::sync::Arc::new(view), scene_w, scene_h));
625 }
626 ed_state_ref.scene_texture_id = tex_id;
627 }
628
629 // Game View RTT
630 let mut needs_recreate_game = false;
631 if let Some(target) = self
632 .world
633 .get_resource::<gizmo_renderer::components::GameRenderTarget>(
634 ) {
635 if target.0.width != game_w || target.0.height != game_h {
636 needs_recreate_game = true;
637 }
638 } else {
639 needs_recreate_game = true;
640 }
641
642 if needs_recreate_game && game_w > 0 && game_h > 0 {
643 if let Some(old_id) = ed_state_ref.game_texture_id {
644 editor.renderer.free_texture(&old_id);
645 }
646 let tex_id;
647 {
648 let r = self.world.get_resource::<Renderer>().unwrap();
649 let texture =
650 r.device.create_texture(&wgpu::TextureDescriptor {
651 label: Some("Game RTT"),
652 size: wgpu::Extent3d {
653 width: game_w,
654 height: game_h,
655 depth_or_array_layers: 1,
656 },
657 mip_level_count: 1,
658 sample_count: 1,
659 dimension: wgpu::TextureDimension::D2,
660 format: r.config.format,
661 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
662 | wgpu::TextureUsages::TEXTURE_BINDING,
663 view_formats: &[],
664 });
665 let view = texture
666 .create_view(&wgpu::TextureViewDescriptor::default());
667 tex_id = Some(editor.renderer.register_native_texture(
668 &r.device,
669 &view,
670 wgpu::FilterMode::Linear,
671 ));
672 new_game_target =
673 Some((std::sync::Arc::new(view), game_w, game_h));
674 }
675 ed_state_ref.game_texture_id = tex_id;
676 }
677
678 drop(ed_state_ref);
679
680 if let Some((view, w, h)) = new_scene_target {
681 self.world.insert_resource(
682 gizmo_renderer::components::EditorRenderTarget(
683 gizmo_renderer::components::RenderTarget {
684 view,
685 width: w,
686 height: h,
687 },
688 ),
689 );
690 }
691 if let Some((view, w, h)) = new_game_target {
692 self.world.insert_resource(
693 gizmo_renderer::components::GameRenderTarget(
694 gizmo_renderer::components::RenderTarget {
695 view,
696 width: w,
697 height: h,
698 },
699 ),
700 );
701 }
702 }
703
704 // --- EDITOR SCENE REQUESTS ---
705 // 1. Poll the file-dialog channel and promote result to save/load request.
706 let maybe_dialog_result = {
707 let mut st =
708 self.world.get_resource_mut::<gizmo_editor::EditorState>();
709 if let Some(ref mut ed) = st {
710 if let Some(rx_mutex) = ed.pending_dialog_rx.take() {
711 match rx_mutex.into_inner() {
712 Ok(rx) => match rx.try_recv() {
713 Ok((is_save, Some(path))) => {
714 Some((is_save, Some(path)))
715 }
716 Ok((_, None)) => None, // dialog dismissed
717 Err(std::sync::mpsc::TryRecvError::Empty) => {
718 // still waiting — put it back
719 ed.pending_dialog_rx =
720 Some(std::sync::Mutex::new(rx));
721 None
722 }
723 Err(_) => None,
724 },
725 Err(_) => None,
726 }
727 } else {
728 None
729 }
730 } else {
731 None
732 }
733 };
734 if let Some((is_save, Some(path))) = maybe_dialog_result {
735 if let Some(mut ed) =
736 self.world.get_resource_mut::<gizmo_editor::EditorState>()
737 {
738 ed.scene_path = path.clone();
739 if is_save {
740 ed.scene.save_request = Some(path);
741 } else {
742 ed.scene.load_request = Some(path);
743 }
744 }
745 }
746
747 // 2. Extract requests before borrowing world mutably.
748 let (save_req, load_req, clear_req) = {
749 if let Some(mut ed) =
750 self.world.get_resource_mut::<gizmo_editor::EditorState>()
751 {
752 (
753 ed.scene.save_request.take(),
754 ed.scene.load_request.take(),
755 std::mem::replace(&mut ed.scene.clear_request, false),
756 )
757 } else {
758 (None, None, false)
759 }
760 };
761
762 // 3. Save
763 if let Some(ref path) = save_req {
764 let registry = gizmo_scene::registry::default_scene_registry();
765 match gizmo_scene::scene::SceneData::save(
766 &self.world,
767 path,
768 ®istry,
769 ) {
770 Ok(()) => {
771 if let Some(mut ed) = self
772 .world
773 .get_resource_mut::<gizmo_editor::EditorState>()
774 {
775 ed.has_unsaved_changes = false;
776 ed.status_message = format!("Kaydedildi: {}", path);
777 }
778 }
779 Err(e) => tracing::error!("[App] Sahne kayıt hatası: {}", e),
780 }
781 }
782
783 // 4. Clear + Load
784 if clear_req || load_req.is_some() {
785 let editor_entities: std::collections::HashSet<u32> = {
786 let names = self.world.borrow::<gizmo_core::EntityName>();
787 names
788 .iter()
789 .filter_map(|(id, _)| {
790 names.get(id).and_then(|n| {
791 if n.0.starts_with("Editor ")
792 || n.0 == "Highlight Box"
793 {
794 Some(id)
795 } else {
796 None
797 }
798 })
799 })
800 .collect()
801 };
802 let to_despawn: Vec<_> = self
803 .world
804 .iter_alive_entities()
805 .into_iter()
806 .filter(|e| !editor_entities.contains(&e.id()))
807 .collect();
808 for e in to_despawn {
809 self.world.despawn(e);
810 }
811 }
812 if let Some(ref path) = load_req {
813 if let Some(asset_manager) =
814 self.world
815 .remove_resource::<gizmo_renderer::asset::AssetManager>()
816 {
817 let r = self.world.remove_resource::<Renderer>().unwrap();
818 let dummy_rgba = [255u8, 255, 255, 255];
819 let _dummy_bg = r.create_texture(&dummy_rgba, 1, 1);
820 let registry = gizmo_scene::registry::default_scene_registry();
821 let ok = gizmo_scene::scene::SceneData::load_into(
822 path,
823 &mut self.world,
824 ®istry,
825 );
826 self.world.insert_resource(r);
827 self.world.insert_resource(asset_manager);
828 if let Some(mut ed) =
829 self.world.get_resource_mut::<gizmo_editor::EditorState>()
830 {
831 ed.status_message = if ok {
832 format!("Yüklendi: {}", path)
833 } else {
834 format!("Sahne yüklenemedi: {}", path)
835 };
836 ed.has_unsaved_changes = false;
837 }
838 }
839 }
840
841 // ECS Sistemlerini Çalıştırmadan önce DI için Core Resource'ları Güncelle
842 self.world.insert_resource(self.input.clone());
843 {
844 let has_time = self
845 .world
846 .get_resource::<gizmo_core::time::Time>()
847 .is_some();
848 if has_time {
849 let mut time = self
850 .world
851 .get_resource_mut::<gizmo_core::time::Time>()
852 .unwrap();
853 time.update(dt);
854 } else {
855 let mut time = gizmo_core::time::Time::new();
856 time.update(dt);
857 self.world.insert_resource(time);
858 }
859 }
860
861 // ═══ Fixed Timestep Fizik Döngüsü ═══
862 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
863 profiler.begin_scope("physics");
864 }
865 // PhysicsTime resource'u yoksa oluştur
866 if self
867 .world
868 .get_resource::<gizmo_core::time::PhysicsTime>()
869 .is_none()
870 {
871 self.world
872 .insert_resource(gizmo_core::time::PhysicsTime::default());
873 }
874 {
875 let mut phys_time = self
876 .world
877 .get_resource_mut::<gizmo_core::time::PhysicsTime>()
878 .unwrap();
879 phys_time.accumulate(dt);
880 }
881
882 #[cfg(feature = "network")]
883 {
884 let mut rollback_needed = false;
885 let mut rm = self.world.remove_resource::<gizmo_network::RollbackManager>();
886
887 if let Some(ref mut manager) = rm {
888 rollback_needed = manager.begin_frame(&mut self.world);
889 }
890
891 if rollback_needed {
892 let target_tick = rm.as_ref().unwrap().latest_tick;
893
894 // Desync'i temizlemek için fiziği World'den zorla kopyalattır
895 if let Some(mut pw) = self.world.get_resource_mut::<gizmo_physics_rigid::world::PhysicsWorld>() {
896 pw.clear_bodies();
897 }
898
899 let fixed_dt = self
900 .world
901 .get_resource::<gizmo_core::time::PhysicsTime>()
902 .map(|pt| pt.fixed_dt())
903 .unwrap_or(1.0 / 60.0);
904
905 loop {
906 let curr = rm.as_ref().unwrap().current_tick;
907 if curr >= target_tick {
908 break;
909 }
910
911 // Simüle et (Geçmişten Günümüze Fast-Forward)
912 // Dikkat: rm World'de olmadığı için içeride rollback.rs çalışmayabilir.
913 // Ama physics_step_system RollbackManager'i kullanmıyor!
914 self.schedule.run(&mut self.world, fixed_dt);
915
916 // Yeni geçmiş karesini hafızaya al
917 if let Some(ref mut manager) = rm {
918 let snapshot = gizmo_network::PhysicsStateSnapshot::capture(&self.world, manager.current_tick);
919 manager.state_buffer.save(snapshot);
920 manager.current_tick += 1;
921 // latest_tick güncellenmeyecek çünkü zaten eskiyi simüle ediyoruz
922 }
923 }
924 }
925
926 // Tekrar yerine koy
927 if let Some(manager) = rm {
928 self.world.insert_resource(manager);
929 }
930 }
931
932 // Sabit dt'de normal fizik adımları — frame rate'ten bağımsız
933 loop {
934 let should = self
935 .world
936 .get_resource::<gizmo_core::time::PhysicsTime>()
937 .map(|pt| pt.should_step())
938 .unwrap_or(false);
939 if !should {
940 break;
941 }
942
943 let fixed_dt = self
944 .world
945 .get_resource::<gizmo_core::time::PhysicsTime>()
946 .map(|pt| pt.fixed_dt())
947 .unwrap_or(1.0 / 60.0);
948
949 // ECS fizik sistemlerini sabit dt ile çalıştır
950 self.schedule.run(&mut self.world, fixed_dt);
951
952 #[cfg(feature = "network")]
953 {
954 if let Some(mut rm) = self.world.get_resource_mut::<gizmo_network::RollbackManager>() {
955 rm.end_frame(&self.world);
956 }
957 }
958
959 let mut phys_time = self
960 .world
961 .get_resource_mut::<gizmo_core::time::PhysicsTime>()
962 .unwrap();
963 phys_time.consume_step();
964 }
965
966 // İnterpolasyon alpha'sını hesapla (render için)
967 {
968 let mut phys_time = self
969 .world
970 .get_resource_mut::<gizmo_core::time::PhysicsTime>()
971 .unwrap();
972 phys_time.compute_alpha();
973 }
974
975 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
976 profiler.end_scope("physics");
977 }
978
979 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
980 profiler.begin_scope("update");
981 }
982
983 // Kullanıcı update hook'u (render dt ile — kamera, UI, vb.)
984 if let Some(update_hk) = self.update_fn.as_mut() {
985 update_hk(&mut self.world, &mut state, dt, &self.input);
986 }
987
988 // Update sonrası olası ertelenmiş komutları (CommandQueue) hemen işle
989 self.world.apply_commands();
990
991 // Asset Loading System (Lazy Load)
992 if let Some(mut asset_manager) = self.world.remove_resource::<gizmo_renderer::asset::AssetManager>() {
993 let r = self.world.remove_resource::<Renderer>().unwrap();
994 gizmo_renderer::asset_loading::run_asset_loading_system(
995 &mut self.world,
996 &r.device,
997 &r.queue,
998 &r.scene.texture_bind_group_layout,
999 &mut asset_manager,
1000 );
1001 self.world.insert_resource(r);
1002 self.world.insert_resource(asset_manager);
1003 }
1004
1005 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1006 profiler.end_scope("update");
1007 }
1008
1009 // --- DYNAMIC FRACTURE & PARTICLE INTEGRATION ---
1010 if let Some(physics_world) =
1011 self.world
1012 .get_resource::<gizmo_physics_rigid::world::PhysicsWorld>()
1013 {
1014 if !physics_world.fracture_events.is_empty() {
1015 let renderer = self.world.get_resource::<Renderer>().unwrap();
1016 if let Some(gpu_particles) = &renderer.gpu_particles {
1017 for event in &physics_world.fracture_events {
1018 let center = [
1019 event.impact_point.x,
1020 event.impact_point.y,
1021 event.impact_point.z,
1022 ];
1023 let dust_color = [0.6, 0.55, 0.5, 0.8]; // Dust color
1024 let force =
1025 (event.impact_force * 0.01).clamp(2.0, 15.0);
1026 let particle_count = (event.impact_force * 0.1)
1027 .clamp(50.0, 500.0)
1028 as u32;
1029 gpu_particles.spawn_explosion(
1030 &renderer.queue,
1031 center,
1032 particle_count,
1033 dust_color,
1034 force,
1035 );
1036 }
1037 }
1038 }
1039 }
1040
1041 // Olayları Güncelle (Çift-buffer temizliği)
1042 for updater in &mut self.event_updaters {
1043 updater(&mut self.world);
1044 }
1045
1046 // --- DRAW KISMI ---
1047 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1048 profiler.begin_scope("render");
1049 }
1050 let mut renderer = self.world.remove_resource::<Renderer>().unwrap();
1051
1052 let output = match renderer.surface.get_current_texture() {
1053 Ok(texture) => texture,
1054 Err(wgpu::SurfaceError::Outdated) => {
1055 self.world.insert_resource(renderer);
1056 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1057 profiler.end_scope("render");
1058 }
1059 return;
1060 }
1061 Err(e) => {
1062 tracing::error!("Surface hatasi: {:?}", e);
1063 self.world.insert_resource(renderer);
1064 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1065 profiler.end_scope("render");
1066 }
1067 return;
1068 }
1069 };
1070
1071 let view = output
1072 .texture
1073 .create_view(&wgpu::TextureViewDescriptor::default());
1074
1075 let mut encoder = renderer.device.create_command_encoder(
1076 &wgpu::CommandEncoderDescriptor {
1077 label: Some("Render Encoder"),
1078 },
1079 );
1080
1081 // Kullaniciya CommandEncoder verip cizdiriyoruz!
1082 if let Some(render_hk) = self.render_fn.as_mut() {
1083 render_hk(
1084 &mut self.world,
1085 &state,
1086 &mut encoder,
1087 &view,
1088 &mut renderer,
1089 light_time,
1090 );
1091 } else if let Some(s_render) = self.simple_render_fn.as_mut() {
1092 let mut ctx = RenderContext::new(
1093 &mut encoder,
1094 &view,
1095 &mut renderer,
1096 light_time,
1097 );
1098 s_render(&mut self.world, &state, &mut ctx);
1099 }
1100
1101 editor.render(
1102 &window,
1103 &renderer.device,
1104 &renderer.queue,
1105 &mut encoder,
1106 &view,
1107 full_output,
1108 );
1109
1110 renderer.queue.submit(std::iter::once(encoder.finish()));
1111 output.present();
1112
1113 self.world.insert_resource(renderer);
1114
1115 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1116 profiler.end_scope("render");
1117 }
1118
1119 // İşlemlerin bitiminde frame-özel input girdilerini (fare delta vs.) temizle
1120 self.input.begin_frame();
1121
1122 if let Some(mut profiler) = self.world.get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
1123 profiler.end_frame();
1124 }
1125 }
1126 }
1127 Event::AboutToWait => {
1128 window.request_redraw();
1129 }
1130 Event::DeviceEvent {
1131 event: winit::event::DeviceEvent::MouseMotion { delta },
1132 ..
1133 } => {
1134 self.input.on_mouse_delta(delta.0 as f32, delta.1 as f32);
1135 }
1136 _ => {}
1137 }
1138 })
1139 .unwrap();
1140 }
1141}