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