1use crate::editor::camera_controller::{EditorCamera, SnapView};
5use crate::editor::gizmos::GizmoMode;
6use crate::editor::perf_overlay::PerfOverlay;
7
8use glam::{Mat4, Vec2, Vec3, Vec4};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy)]
14pub struct ViewportRect {
15 pub x: f32,
16 pub y: f32,
17 pub width: f32,
18 pub height: f32,
19}
20
21impl ViewportRect {
22 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
23 Self { x, y, width, height }
24 }
25
26 pub fn aspect(&self) -> f32 {
27 if self.height > 0.0 { self.width / self.height } else { 1.0 }
28 }
29
30 pub fn contains(&self, px: f32, py: f32) -> bool {
31 px >= self.x && px < self.x + self.width
32 && py >= self.y && py < self.y + self.height
33 }
34
35 pub fn to_ndc(&self, px: f32, py: f32) -> Vec2 {
36 Vec2::new(
37 (px - self.x) / self.width * 2.0 - 1.0,
38 1.0 - (py - self.y) / self.height * 2.0,
39 )
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct EntityId(pub u32);
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum EntityKind {
50 SdfBody,
51 ParticleEmitter,
52 DirectionalLight,
53 PointLight,
54 SpotLight,
55 AreaLight,
56 Camera,
57 ForceField,
58 BoneRoot,
59 StaticMesh,
60 Trigger,
61 AudioEmitter,
62 Decal,
63 Probe,
64 Marker,
65}
66
67impl EntityKind {
68 pub fn icon(&self) -> &'static str {
69 match self {
70 Self::SdfBody => "[SDF]",
71 Self::ParticleEmitter => "[FX]",
72 Self::DirectionalLight => "[SUN]",
73 Self::PointLight => "[PT]",
74 Self::SpotLight => "[SPT]",
75 Self::AreaLight => "[AREA]",
76 Self::Camera => "[CAM]",
77 Self::ForceField => "[FIELD]",
78 Self::BoneRoot => "[BONE]",
79 Self::StaticMesh => "[MESH]",
80 Self::Trigger => "[TRIG]",
81 Self::AudioEmitter => "[SND]",
82 Self::Decal => "[DECAL]",
83 Self::Probe => "[PROBE]",
84 Self::Marker => "[MARK]",
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
90pub struct ViewportEntity {
91 pub id: EntityId,
92 pub name: String,
93 pub kind: EntityKind,
94 pub position: Vec3,
95 pub rotation: Vec3, pub scale: Vec3,
97 pub visible: bool,
98 pub locked: bool,
99 pub parent: Option<EntityId>,
100 pub children: Vec<EntityId>,
101 pub sdf_graph_id: Option<u32>,
102 pub material_tag: Option<String>,
103 pub cast_shadows: bool,
104 pub receive_shadows: bool,
105 pub static_batching: bool,
106 pub layer_mask: u32,
107}
108
109impl ViewportEntity {
110 pub fn new(id: EntityId, name: String, kind: EntityKind) -> Self {
111 Self {
112 id,
113 name,
114 kind,
115 position: Vec3::ZERO,
116 rotation: Vec3::ZERO,
117 scale: Vec3::ONE,
118 visible: true,
119 locked: false,
120 parent: None,
121 children: Vec::new(),
122 sdf_graph_id: None,
123 material_tag: None,
124 cast_shadows: true,
125 receive_shadows: true,
126 static_batching: false,
127 layer_mask: 0xFFFF_FFFF,
128 }
129 }
130
131 pub fn model_matrix(&self) -> Mat4 {
132 let t = Mat4::from_translation(self.position);
133 let rx = Mat4::from_rotation_x(self.rotation.x.to_radians());
134 let ry = Mat4::from_rotation_y(self.rotation.y.to_radians());
135 let rz = Mat4::from_rotation_z(self.rotation.z.to_radians());
136 let s = Mat4::from_scale(self.scale);
137 t * ry * rx * rz * s
138 }
139
140 pub fn world_bounds_radius(&self) -> f32 {
141 self.scale.length() * 0.5
142 }
143}
144
145#[derive(Debug, Clone)]
148pub enum LightData {
149 Directional {
150 direction: Vec3,
151 color: Vec3,
152 intensity: f32,
153 shadow_distance: f32,
154 shadow_resolution: u32,
155 cascade_count: u8,
156 cascade_splits: [f32; 4],
157 },
158 Point {
159 color: Vec3,
160 intensity: f32,
161 radius: f32,
162 falloff_exp: f32,
163 cast_shadow: bool,
164 shadow_near: f32,
165 },
166 Spot {
167 direction: Vec3,
168 color: Vec3,
169 intensity: f32,
170 radius: f32,
171 inner_cone_deg: f32,
172 outer_cone_deg: f32,
173 cast_shadow: bool,
174 },
175 Area {
176 color: Vec3,
177 intensity: f32,
178 width: f32,
179 height: f32,
180 two_sided: bool,
181 },
182}
183
184impl LightData {
185 pub fn color(&self) -> Vec3 {
186 match self {
187 Self::Directional { color, .. } => *color,
188 Self::Point { color, .. } => *color,
189 Self::Spot { color, .. } => *color,
190 Self::Area { color, .. } => *color,
191 }
192 }
193
194 pub fn intensity(&self) -> f32 {
195 match self {
196 Self::Directional { intensity, .. } => *intensity,
197 Self::Point { intensity, .. } => *intensity,
198 Self::Spot { intensity, .. } => *intensity,
199 Self::Area { intensity, .. } => *intensity,
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
207pub struct GridSettings {
208 pub visible: bool,
209 pub size: f32,
210 pub subdivisions: u32,
211 pub color_major: Vec4,
212 pub color_minor: Vec4,
213 pub fade_distance: f32,
214 pub show_axes: bool,
215 pub show_origin: bool,
216 pub snap_enabled: bool,
217 pub snap_translate: f32,
218 pub snap_rotate: f32,
219 pub snap_scale: f32,
220}
221
222impl Default for GridSettings {
223 fn default() -> Self {
224 Self {
225 visible: true,
226 size: 100.0,
227 subdivisions: 10,
228 color_major: Vec4::new(0.5, 0.5, 0.5, 0.8),
229 color_minor: Vec4::new(0.3, 0.3, 0.3, 0.4),
230 fade_distance: 80.0,
231 show_axes: true,
232 show_origin: true,
233 snap_enabled: false,
234 snap_translate: 0.25,
235 snap_rotate: 15.0,
236 snap_scale: 0.1,
237 }
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub enum ShadingMode {
245 Lit,
246 Unlit,
247 Wireframe,
248 Normals,
249 Albedo,
250 Roughness,
251 Metallic,
252 AO,
253 Emission,
254 Depth,
255 UV,
256 VertexColor,
257 SdfDistance,
258 SdfNormals,
259 Overdraw,
260 LightingOnly,
261 SpecularOnly,
262 DiffuseOnly,
263}
264
265impl ShadingMode {
266 pub fn label(&self) -> &'static str {
267 match self {
268 Self::Lit => "Lit",
269 Self::Unlit => "Unlit",
270 Self::Wireframe => "Wireframe",
271 Self::Normals => "Normals",
272 Self::Albedo => "Albedo",
273 Self::Roughness => "Roughness",
274 Self::Metallic => "Metallic",
275 Self::AO => "AO",
276 Self::Emission => "Emission",
277 Self::Depth => "Depth",
278 Self::UV => "UV",
279 Self::VertexColor => "Vertex Color",
280 Self::SdfDistance => "SDF Distance",
281 Self::SdfNormals => "SDF Normals",
282 Self::Overdraw => "Overdraw",
283 Self::LightingOnly => "Lighting Only",
284 Self::SpecularOnly => "Specular Only",
285 Self::DiffuseOnly => "Diffuse Only",
286 }
287 }
288
289 pub fn all() -> &'static [Self] {
290 &[
291 Self::Lit, Self::Unlit, Self::Wireframe, Self::Normals,
292 Self::Albedo, Self::Roughness, Self::Metallic, Self::AO,
293 Self::Emission, Self::Depth, Self::UV, Self::VertexColor,
294 Self::SdfDistance, Self::SdfNormals, Self::Overdraw,
295 Self::LightingOnly, Self::SpecularOnly, Self::DiffuseOnly,
296 ]
297 }
298}
299
300#[derive(Debug, Clone)]
301pub struct RenderSettings {
302 pub shading_mode: ShadingMode,
303 pub msaa_samples: u8,
304 pub bloom_enabled: bool,
305 pub bloom_threshold: f32,
306 pub bloom_intensity: f32,
307 pub ao_enabled: bool,
308 pub ao_radius: f32,
309 pub ao_samples: u32,
310 pub shadow_enabled: bool,
311 pub shadow_bias: f32,
312 pub ssr_enabled: bool,
313 pub ssr_steps: u32,
314 pub dof_enabled: bool,
315 pub dof_focus_dist: f32,
316 pub dof_aperture: f32,
317 pub motion_blur_enabled: bool,
318 pub motion_blur_strength: f32,
319 pub exposure: f32,
320 pub tonemapping: ToneMappingMode,
321 pub gamma: f32,
322 pub show_bounding_boxes: bool,
323 pub show_light_cones: bool,
324 pub show_bone_envelopes: bool,
325 pub show_force_field_radii: bool,
326 pub show_particle_counts: bool,
327 pub render_scale: f32,
328 pub background_color: Vec4,
329 pub use_skybox: bool,
330 pub skybox_intensity: f32,
331 pub fog_enabled: bool,
332 pub fog_start: f32,
333 pub fog_end: f32,
334 pub fog_density: f32,
335 pub fog_color: Vec3,
336}
337
338impl Default for RenderSettings {
339 fn default() -> Self {
340 Self {
341 shading_mode: ShadingMode::Lit,
342 msaa_samples: 4,
343 bloom_enabled: true,
344 bloom_threshold: 1.0,
345 bloom_intensity: 0.3,
346 ao_enabled: true,
347 ao_radius: 0.5,
348 ao_samples: 16,
349 shadow_enabled: true,
350 shadow_bias: 0.002,
351 ssr_enabled: false,
352 ssr_steps: 32,
353 dof_enabled: false,
354 dof_focus_dist: 10.0,
355 dof_aperture: 0.05,
356 motion_blur_enabled: false,
357 motion_blur_strength: 0.5,
358 exposure: 1.0,
359 tonemapping: ToneMappingMode::Aces,
360 gamma: 2.2,
361 show_bounding_boxes: false,
362 show_light_cones: true,
363 show_bone_envelopes: false,
364 show_force_field_radii: true,
365 show_particle_counts: false,
366 render_scale: 1.0,
367 background_color: Vec4::new(0.12, 0.12, 0.14, 1.0),
368 use_skybox: false,
369 skybox_intensity: 1.0,
370 fog_enabled: false,
371 fog_start: 20.0,
372 fog_end: 100.0,
373 fog_density: 0.02,
374 fog_color: Vec3::new(0.6, 0.7, 0.8),
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub enum ToneMappingMode {
381 None,
382 Reinhard,
383 ReinhardExtended,
384 Aces,
385 AcesApprox,
386 Uncharted2,
387 Filmic,
388 AgX,
389 Lottes,
390}
391
392impl ToneMappingMode {
393 pub fn label(&self) -> &'static str {
394 match self {
395 Self::None => "None (Linear)",
396 Self::Reinhard => "Reinhard",
397 Self::ReinhardExtended => "Reinhard Extended",
398 Self::Aces => "ACES",
399 Self::AcesApprox => "ACES (Fast)",
400 Self::Uncharted2 => "Uncharted 2",
401 Self::Filmic => "Filmic",
402 Self::AgX => "AgX",
403 Self::Lottes => "Lottes",
404 }
405 }
406
407 pub fn all() -> &'static [Self] {
408 &[
409 Self::None, Self::Reinhard, Self::ReinhardExtended,
410 Self::Aces, Self::AcesApprox, Self::Uncharted2,
411 Self::Filmic, Self::AgX, Self::Lottes,
412 ]
413 }
414
415 pub fn glsl_fn(&self) -> &'static str {
416 match self {
417 Self::None => "// linear passthrough",
418 Self::Reinhard => "color = color / (color + vec3(1.0));",
419 Self::ReinhardExtended => "color = color * (1.0 + color/vec3(9.0)) / (1.0 + color);",
420 Self::Aces => {
421 "const mat3 m1=mat3(0.59719,0.07600,0.02840,0.35458,0.90834,0.13383,0.04823,0.01566,0.83777);\
422 const mat3 m2=mat3(1.60475,-0.10208,-0.00327,-0.53108,1.10813,-0.07276,-0.07367,-0.00605,1.07602);\
423 vec3 v=m1*color;vec3 a=v*(v+0.0245786)-0.000090537;\
424 vec3 b=v*(0.983729*v+0.4329510)+0.238081;color=m2*(a/b);"
425 }
426 Self::AcesApprox => {
427 "color=color*(2.51*color+0.03)/(color*(2.43*color+0.59)+0.14);"
428 }
429 Self::Uncharted2 => {
430 "vec3 _uc2(vec3 x){return((x*(0.15*x+0.10*0.50)+0.20*0.02)/(x*(0.15*x+0.50)+0.20*0.30))-0.02/0.30;}\
431 color=_uc2(color*2.0)/_uc2(vec3(11.2));"
432 }
433 Self::Filmic => {
434 "vec3 x=max(vec3(0.0),color-0.004);\
435 color=(x*(6.2*x+0.5))/(x*(6.2*x+1.7)+0.06);"
436 }
437 Self::AgX => "// AgX tonemapping (placeholder)",
438 Self::Lottes => {
439 "const float a=1.6,d=0.977,hdrMax=8.0,midIn=0.18,midOut=0.267;\
440 const float b=((-pow(midIn,a)+pow(hdrMax,a)*midOut)/(pow(pow(hdrMax,a),d)-pow(midIn,a)))/(midOut);\
441 const float c=(pow(hdrMax,a*d)*pow(midIn,a)-pow(hdrMax,a)*pow(midIn,a*d)*midOut)/(pow(pow(hdrMax,a),d)-pow(midIn,a))/midOut;\
442 color=pow(color,vec3(a))/(pow(color,vec3(a*d))*b+c);"
443 }
444 }
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451pub enum ViewportLayout {
452 Single,
453 HorizontalSplit,
454 VerticalSplit,
455 QuadSplit,
456 ThreeLeft,
457 ThreeRight,
458}
459
460impl ViewportLayout {
461 pub fn panel_count(&self) -> usize {
462 match self {
463 Self::Single => 1,
464 Self::HorizontalSplit | Self::VerticalSplit => 2,
465 Self::ThreeLeft | Self::ThreeRight => 3,
466 Self::QuadSplit => 4,
467 }
468 }
469
470 pub fn rects(&self, w: f32, h: f32) -> Vec<ViewportRect> {
471 match self {
472 Self::Single => vec![ViewportRect::new(0.0, 0.0, w, h)],
473 Self::HorizontalSplit => vec![
474 ViewportRect::new(0.0, 0.0, w, h * 0.5),
475 ViewportRect::new(0.0, h * 0.5, w, h * 0.5),
476 ],
477 Self::VerticalSplit => vec![
478 ViewportRect::new(0.0, 0.0, w * 0.5, h),
479 ViewportRect::new(w * 0.5, 0.0, w * 0.5, h),
480 ],
481 Self::QuadSplit => vec![
482 ViewportRect::new(0.0, 0.0, w*0.5, h*0.5),
483 ViewportRect::new(w*0.5, 0.0, w*0.5, h*0.5),
484 ViewportRect::new(0.0, h*0.5, w*0.5, h*0.5),
485 ViewportRect::new(w*0.5, h*0.5, w*0.5, h*0.5),
486 ],
487 Self::ThreeLeft => vec![
488 ViewportRect::new(0.0, 0.0, w*0.5, h),
489 ViewportRect::new(w*0.5, 0.0, w*0.5, h*0.5),
490 ViewportRect::new(w*0.5, h*0.5, w*0.5, h*0.5),
491 ],
492 Self::ThreeRight => vec![
493 ViewportRect::new(0.0, 0.0, w*0.5, h*0.5),
494 ViewportRect::new(0.0, h*0.5, w*0.5, h*0.5),
495 ViewportRect::new(w*0.5, 0.0, w*0.5, h),
496 ],
497 }
498 }
499}
500
501#[derive(Debug, Clone)]
504pub struct SelectionOutline {
505 pub enabled: bool,
506 pub color: Vec4,
507 pub width: f32,
508 pub pulse: bool,
509 pub pulse_speed: f32,
510}
511
512impl Default for SelectionOutline {
513 fn default() -> Self {
514 Self {
515 enabled: true,
516 color: Vec4::new(1.0, 0.6, 0.1, 1.0),
517 width: 2.0,
518 pulse: true,
519 pulse_speed: 2.0,
520 }
521 }
522}
523
524#[derive(Debug, Clone)]
527pub struct HoverState {
528 pub entity: Option<EntityId>,
529 pub screen_pos: Vec2,
530 pub world_pos: Vec3,
531 pub world_normal: Vec3,
532 pub distance: f32,
533}
534
535impl Default for HoverState {
536 fn default() -> Self {
537 Self {
538 entity: None,
539 screen_pos: Vec2::ZERO,
540 world_pos: Vec3::ZERO,
541 world_normal: Vec3::Y,
542 distance: f32::MAX,
543 }
544 }
545}
546
547#[derive(Debug)]
550pub struct ViewportPanel {
551 pub index: usize,
552 pub rect: ViewportRect,
553 pub camera: EditorCamera,
554 pub snap_view: SnapView,
555 pub render_settings: RenderSettings,
556 pub is_active: bool,
557 pub is_playing: bool,
558 pub show_gizmos: bool,
559 pub hover: HoverState,
560 pub frame_count: u64,
561 pub render_time_ms: f32,
562 pub hovered_port: Option<String>,
563}
564
565impl ViewportPanel {
566 pub fn new(index: usize, rect: ViewportRect) -> Self {
567 let mut camera = EditorCamera::new();
568 let snap = match index {
570 0 => SnapView::IsoFrontRight,
571 1 => SnapView::Top,
572 2 => SnapView::Front,
573 3 => SnapView::Right,
574 _ => SnapView::IsoFrontRight,
575 };
576 camera.snap_to(snap);
577 Self {
578 index,
579 rect,
580 camera,
581 snap_view: snap,
582 render_settings: RenderSettings::default(),
583 is_active: index == 0,
584 is_playing: false,
585 show_gizmos: true,
586 hover: HoverState::default(),
587 frame_count: 0,
588 render_time_ms: 0.0,
589 hovered_port: None,
590 }
591 }
592
593 pub fn resize(&mut self, rect: ViewportRect) {
594 self.rect = rect;
595 }
597
598 pub fn view_matrix(&self) -> Mat4 {
599 self.camera.view_matrix()
600 }
601
602 pub fn proj_matrix(&self) -> Mat4 {
603 self.camera.projection.matrix(self.rect.aspect())
604 }
605
606 pub fn view_proj(&self) -> Mat4 {
607 self.proj_matrix() * self.view_matrix()
608 }
609
610 pub fn world_to_screen(&self, world: Vec3) -> Vec2 {
611 let clip = self.view_proj() * Vec4::new(world.x, world.y, world.z, 1.0);
612 if clip.w.abs() < 1e-6 { return Vec2::new(-99999.0, -99999.0); }
613 let ndc = Vec2::new(clip.x / clip.w, clip.y / clip.w);
614 Vec2::new(
615 self.rect.x + (ndc.x * 0.5 + 0.5) * self.rect.width,
616 self.rect.y + (1.0 - (ndc.y * 0.5 + 0.5)) * self.rect.height,
617 )
618 }
619
620 pub fn screen_to_ray(&self, screen_x: f32, screen_y: f32) -> (Vec3, Vec3) {
621 let ndc = self.rect.to_ndc(screen_x, screen_y);
622 self.camera.screen_to_ray(ndc)
623 }
624
625 pub fn update_hover(&mut self, screen_x: f32, screen_y: f32) {
627 if !self.rect.contains(screen_x, screen_y) {
628 self.hover.entity = None;
629 return;
630 }
631 self.hover.screen_pos = Vec2::new(screen_x, screen_y);
632 let (origin, dir) = self.screen_to_ray(screen_x, screen_y);
633 if dir.y.abs() > 1e-5 {
635 let t = -origin.y / dir.y;
636 if t > 0.0 {
637 self.hover.world_pos = origin + dir * t;
638 self.hover.world_normal = Vec3::Y;
639 self.hover.distance = t;
640 }
641 }
642 }
643
644 pub fn cycle_shading_mode(&mut self) {
645 let all = ShadingMode::all();
646 let cur = self.render_settings.shading_mode;
647 let idx = all.iter().position(|&m| m == cur).unwrap_or(0);
648 self.render_settings.shading_mode = all[(idx + 1) % all.len()];
649 }
650
651 pub fn toggle_wireframe(&mut self) {
652 if self.render_settings.shading_mode == ShadingMode::Wireframe {
653 self.render_settings.shading_mode = ShadingMode::Lit;
654 } else {
655 self.render_settings.shading_mode = ShadingMode::Wireframe;
656 }
657 }
658}
659
660#[derive(Debug)]
663pub struct Viewport {
664 pub layout: ViewportLayout,
665 pub panels: Vec<ViewportPanel>,
666 pub active_panel: usize,
667 pub total_width: f32,
668 pub total_height: f32,
669 pub grid: GridSettings,
670 pub selection_outline: SelectionOutline,
671 pub selected_entities: Vec<EntityId>,
672 pub gizmo_mode: GizmoMode,
673 pub gizmo_space: GizmoSpace,
674 pub entities: HashMap<EntityId, ViewportEntity>,
675 pub lights: Vec<LightData>,
676 next_entity_id: u32,
677 pub show_stats: bool,
678 pub show_fps: bool,
679 pub show_camera_info: bool,
680 pub playback_time: f32,
681 pub is_playing: bool,
682}
683
684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
685pub enum GizmoSpace {
686 Local,
687 World,
688}
689
690impl Viewport {
691 pub fn new(width: f32, height: f32) -> Self {
692 let layout = ViewportLayout::Single;
693 let rects = layout.rects(width, height);
694 let panels = rects.into_iter().enumerate()
695 .map(|(i, r)| ViewportPanel::new(i, r))
696 .collect();
697
698 Self {
699 layout,
700 panels,
701 active_panel: 0,
702 total_width: width,
703 total_height: height,
704 grid: GridSettings::default(),
705 selection_outline: SelectionOutline::default(),
706 selected_entities: Vec::new(),
707 gizmo_mode: GizmoMode::Translate,
708 gizmo_space: GizmoSpace::World,
709 entities: HashMap::new(),
710 lights: Vec::new(),
711 next_entity_id: 1,
712 show_stats: true,
713 show_fps: true,
714 show_camera_info: false,
715 playback_time: 0.0,
716 is_playing: false,
717 }
718 }
719
720 pub fn resize(&mut self, width: f32, height: f32) {
721 self.total_width = width;
722 self.total_height = height;
723 let rects = self.layout.rects(width, height);
724 for (panel, rect) in self.panels.iter_mut().zip(rects.into_iter()) {
725 panel.resize(rect);
726 }
727 }
728
729 pub fn set_layout(&mut self, layout: ViewportLayout) {
730 let rects = layout.rects(self.total_width, self.total_height);
731 let old_count = self.panels.len();
732 let new_count = layout.panel_count();
733
734 while self.panels.len() < new_count {
736 let idx = self.panels.len();
737 let rect = rects.get(idx).copied().unwrap_or(
738 ViewportRect::new(0.0, 0.0, self.total_width, self.total_height)
739 );
740 self.panels.push(ViewportPanel::new(idx, rect));
741 }
742 for (i, rect) in rects.iter().enumerate() {
744 if let Some(panel) = self.panels.get_mut(i) {
745 panel.resize(*rect);
746 }
747 }
748 self.panels.truncate(new_count);
750 let _ = old_count;
751 self.layout = layout;
752 self.active_panel = self.active_panel.min(new_count.saturating_sub(1));
753 }
754
755 pub fn active_panel(&self) -> &ViewportPanel {
756 &self.panels[self.active_panel.min(self.panels.len().saturating_sub(1))]
757 }
758
759 pub fn active_panel_mut(&mut self) -> &mut ViewportPanel {
760 let idx = self.active_panel.min(self.panels.len().saturating_sub(1));
761 &mut self.panels[idx]
762 }
763
764 pub fn spawn_entity(&mut self, name: String, kind: EntityKind) -> EntityId {
765 let id = EntityId(self.next_entity_id);
766 self.next_entity_id += 1;
767 let entity = ViewportEntity::new(id, name, kind);
768 self.entities.insert(id, entity);
769 id
770 }
771
772 pub fn spawn_at(&mut self, name: String, kind: EntityKind, pos: Vec3) -> EntityId {
773 let id = self.spawn_entity(name, kind);
774 if let Some(e) = self.entities.get_mut(&id) {
775 e.position = pos;
776 }
777 id
778 }
779
780 pub fn despawn(&mut self, id: EntityId) {
781 if let Some(e) = self.entities.remove(&id) {
782 for child_id in &e.children {
784 if let Some(child) = self.entities.get_mut(child_id) {
785 child.parent = None;
786 }
787 }
788 if let Some(pid) = e.parent {
790 if let Some(parent) = self.entities.get_mut(&pid) {
791 parent.children.retain(|&c| c != id);
792 }
793 }
794 }
795 self.selected_entities.retain(|&e| e != id);
796 }
797
798 pub fn select(&mut self, id: EntityId, additive: bool) {
799 if !additive {
800 self.selected_entities.clear();
801 }
802 if !self.selected_entities.contains(&id) {
803 self.selected_entities.push(id);
804 }
805 }
806
807 pub fn deselect_all(&mut self) {
808 self.selected_entities.clear();
809 }
810
811 pub fn select_box(
812 &mut self,
813 min_screen: Vec2,
814 max_screen: Vec2,
815 additive: bool,
816 ) {
817 if !additive { self.selected_entities.clear(); }
818 let panel = &self.panels[self.active_panel];
819 for (&id, entity) in &self.entities {
820 if !entity.visible { continue; }
821 let sp = panel.world_to_screen(entity.position);
822 if sp.x >= min_screen.x && sp.x <= max_screen.x
823 && sp.y >= min_screen.y && sp.y <= max_screen.y
824 {
825 if !self.selected_entities.contains(&id) {
826 self.selected_entities.push(id);
827 }
828 }
829 }
830 }
831
832 pub fn frame_selection(&mut self) {
833 if self.selected_entities.is_empty() { return; }
834 let mut center = Vec3::ZERO;
835 let mut count = 0;
836 let mut radius = 1.0f32;
837 for &id in &self.selected_entities {
838 if let Some(e) = self.entities.get(&id) {
839 center += e.position;
840 count += 1;
841 radius = radius.max(e.world_bounds_radius());
842 }
843 }
844 if count > 0 {
845 center /= count as f32;
846 let panel = &mut self.panels[self.active_panel];
847 panel.camera.frame_selection(center, radius);
848 }
849 }
850
851 pub fn frame_all(&mut self) {
852 if self.entities.is_empty() { return; }
853 let mut center = Vec3::ZERO;
854 for e in self.entities.values() { center += e.position; }
855 center /= self.entities.len() as f32;
856 let mut radius = 5.0f32;
857 for e in self.entities.values() {
858 radius = radius.max((e.position - center).length() + e.world_bounds_radius());
859 }
860 let panel = &mut self.panels[self.active_panel];
861 panel.camera.frame_selection(center, radius);
862 }
863
864 pub fn set_snap_view(&mut self, view: SnapView) {
865 let panel = &mut self.panels[self.active_panel];
866 panel.camera.snap_to(view);
867 panel.snap_view = view;
868 }
869
870 pub fn toggle_gizmo_space(&mut self) {
871 self.gizmo_space = match self.gizmo_space {
872 GizmoSpace::Local => GizmoSpace::World,
873 GizmoSpace::World => GizmoSpace::Local,
874 };
875 }
876
877 pub fn cycle_gizmo_mode(&mut self) {
878 self.gizmo_mode = match self.gizmo_mode {
879 GizmoMode::Translate => GizmoMode::Rotate,
880 GizmoMode::Rotate => GizmoMode::Scale,
881 _ => GizmoMode::Translate,
882 };
883 }
884
885 pub fn update(&mut self, dt: f32, input: &ViewportInput) {
886 if self.is_playing {
887 self.playback_time += dt;
888 }
889 for panel in &mut self.panels {
890 panel.camera.update(dt, None);
891 if input.cursor_moved {
892 panel.update_hover(input.cursor_x, input.cursor_y);
893 }
894 }
895 for (i, panel) in self.panels.iter().enumerate() {
897 if panel.rect.contains(input.cursor_x, input.cursor_y) {
898 self.active_panel = i;
899 break;
900 }
901 }
902 }
903
904 pub fn build_frame_data(&self) -> ViewportFrameData {
906 let panel = self.active_panel();
907 ViewportFrameData {
908 view: panel.view_matrix(),
909 proj: panel.proj_matrix(),
910 view_proj: panel.view_proj(),
911 camera_pos: panel.camera.orbit_position(),
912 viewport_size: Vec2::new(panel.rect.width, panel.rect.height),
913 time: self.playback_time,
914 shading_mode: panel.render_settings.shading_mode,
915 entity_transforms: self.entities.iter()
916 .map(|(&id, e)| (id, e.model_matrix()))
917 .collect(),
918 }
919 }
920}
921
922#[derive(Debug)]
925pub struct ViewportFrameData {
926 pub view: Mat4,
927 pub proj: Mat4,
928 pub view_proj: Mat4,
929 pub camera_pos: Vec3,
930 pub viewport_size: Vec2,
931 pub time: f32,
932 pub shading_mode: ShadingMode,
933 pub entity_transforms: HashMap<EntityId, Mat4>,
934}
935
936#[derive(Debug, Default)]
939pub struct ViewportInput {
940 pub cursor_x: f32,
941 pub cursor_y: f32,
942 pub cursor_moved: bool,
943 pub left_pressed: bool,
944 pub right_pressed: bool,
945 pub middle_pressed: bool,
946 pub left_released: bool,
947 pub right_released: bool,
948 pub scroll_delta: f32,
949 pub shift: bool,
950 pub ctrl: bool,
951 pub alt: bool,
952}
953
954#[derive(Debug, Clone)]
957pub struct BoxSelection {
958 pub active: bool,
959 pub start: Vec2,
960 pub end: Vec2,
961}
962
963impl BoxSelection {
964 pub fn new() -> Self {
965 Self { active: false, start: Vec2::ZERO, end: Vec2::ZERO }
966 }
967
968 pub fn begin(&mut self, x: f32, y: f32) {
969 self.active = true;
970 self.start = Vec2::new(x, y);
971 self.end = Vec2::new(x, y);
972 }
973
974 pub fn update(&mut self, x: f32, y: f32) {
975 self.end = Vec2::new(x, y);
976 }
977
978 pub fn finish(&mut self) -> (Vec2, Vec2) {
979 self.active = false;
980 let min = Vec2::new(self.start.x.min(self.end.x), self.start.y.min(self.end.y));
981 let max = Vec2::new(self.start.x.max(self.end.x), self.start.y.max(self.end.y));
982 (min, max)
983 }
984}
985
986impl Default for BoxSelection {
987 fn default() -> Self { Self::new() }
988}
989
990#[derive(Debug, Clone, PartialEq, Eq)]
993pub enum DropTarget {
994 None,
995 Viewport { panel: usize },
996 HierarchyNode { entity: EntityId },
997 AssetSlot { slot_name: String },
998}
999
1000#[derive(Debug, Default, Clone)]
1003pub struct ViewportStats {
1004 pub fps: f32,
1005 pub frame_ms: f32,
1006 pub entity_count: usize,
1007 pub light_count: usize,
1008 pub draw_calls: u32,
1009 pub triangle_count: u32,
1010 pub texture_memory_mb: f32,
1011 pub vertex_memory_mb: f32,
1012 pub visible_entities: usize,
1013 pub culled_entities: usize,
1014 pub shadow_draw_calls: u32,
1015 pub particle_count: u32,
1016}
1017
1018impl ViewportStats {
1019 pub fn format_compact(&self) -> String {
1020 format!(
1021 "FPS:{:.0} | {:.1}ms | {}ent | {}dc | {}K tri",
1022 self.fps, self.frame_ms, self.entity_count,
1023 self.draw_calls, self.triangle_count / 1000
1024 )
1025 }
1026
1027 pub fn format_verbose(&self) -> String {
1028 format!(
1029 "FPS: {:.1} Frame: {:.2}ms\n\
1030 Entities: {} ({} visible, {} culled)\n\
1031 Draw calls: {} (+{} shadow)\n\
1032 Triangles: {}K\n\
1033 Lights: {}\n\
1034 Particles: {}\n\
1035 Texture mem: {:.1}MB\n\
1036 Vertex mem: {:.1}MB",
1037 self.fps, self.frame_ms,
1038 self.entity_count, self.visible_entities, self.culled_entities,
1039 self.draw_calls, self.shadow_draw_calls,
1040 self.triangle_count / 1000,
1041 self.light_count,
1042 self.particle_count,
1043 self.texture_memory_mb,
1044 self.vertex_memory_mb,
1045 )
1046 }
1047}
1048
1049#[derive(Debug)]
1052pub struct ViewportController {
1053 pub viewport: Viewport,
1054 pub box_select: BoxSelection,
1055 pub stats: ViewportStats,
1056 pub perf: PerfOverlay,
1057 drag_entity_start: Option<(EntityId, Vec3)>,
1058 pub drop_target: DropTarget,
1059 pub cursor_shape: CursorShape,
1060}
1061
1062#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1063pub enum CursorShape {
1064 Default,
1065 Crosshair,
1066 Move,
1067 ResizeH,
1068 ResizeV,
1069 ResizeDiag,
1070 Hand,
1071 NotAllowed,
1072 EyeDropper,
1073}
1074
1075impl ViewportController {
1076 pub fn new(width: f32, height: f32) -> Self {
1077 Self {
1078 viewport: Viewport::new(width, height),
1079 box_select: BoxSelection::new(),
1080 stats: ViewportStats::default(),
1081 perf: PerfOverlay::new(),
1082 drag_entity_start: None,
1083 drop_target: DropTarget::None,
1084 cursor_shape: CursorShape::Default,
1085 }
1086 }
1087
1088 pub fn resize(&mut self, w: f32, h: f32) {
1089 self.viewport.resize(w, h);
1090 }
1091
1092 pub fn handle_mouse_down(&mut self, x: f32, y: f32, button: MouseButton, modifiers: KeyMods) {
1093 if let Some(panel_idx) = self.panel_at(x, y) {
1094 self.viewport.active_panel = panel_idx;
1095 let panel = &self.viewport.panels[panel_idx];
1096
1097 match button {
1098 MouseButton::Left => {
1099 let (origin, dir) = panel.screen_to_ray(x, y);
1101 if let Some(picked) = self.pick_entity(origin, dir) {
1102 self.viewport.select(picked, modifiers.shift);
1103 self.drag_entity_start = Some((picked,
1104 self.viewport.entities.get(&picked)
1105 .map(|e| e.position).unwrap_or(Vec3::ZERO)
1106 ));
1107 } else if !modifiers.shift {
1108 self.box_select.begin(x, y);
1109 }
1110 }
1111 MouseButton::Right => {}
1112 MouseButton::Middle => {}
1113 }
1114 }
1115 }
1116
1117 pub fn handle_mouse_up(&mut self, x: f32, y: f32, button: MouseButton, modifiers: KeyMods) {
1118 match button {
1119 MouseButton::Left => {
1120 self.drag_entity_start = None;
1121 if self.box_select.active {
1122 let (min, max) = self.box_select.finish();
1123 if (max - min).length() > 4.0 {
1125 self.viewport.select_box(min, max, modifiers.shift);
1126 }
1127 }
1128 }
1129 _ => {}
1130 }
1131 let _ = (x, y);
1132 }
1133
1134 pub fn handle_mouse_move(&mut self, x: f32, y: f32, dx: f32, dy: f32, buttons: MouseButtons) {
1135 if self.box_select.active {
1136 self.box_select.update(x, y);
1137 }
1138 if buttons.right {
1139 let panel_idx = self.viewport.active_panel;
1141 let panel = &mut self.viewport.panels[panel_idx];
1142 panel.camera.orbit_drag(Vec2::new(dx * 0.3, dy * 0.3));
1143 }
1144 if buttons.middle {
1145 let panel_idx = self.viewport.active_panel;
1146 let panel = &mut self.viewport.panels[panel_idx];
1147 panel.camera.pan_drag(Vec2::new(dx, dy));
1148 }
1149 }
1150
1151 pub fn handle_scroll(&mut self, delta: f32) {
1152 let panel_idx = self.viewport.active_panel;
1153 let panel = &mut self.viewport.panels[panel_idx];
1154 panel.camera.scroll_zoom(delta);
1155 }
1156
1157 fn panel_at(&self, x: f32, y: f32) -> Option<usize> {
1158 self.viewport.panels.iter()
1159 .position(|p| p.rect.contains(x, y))
1160 }
1161
1162 fn pick_entity(&self, origin: Vec3, dir: Vec3) -> Option<EntityId> {
1163 let mut best: Option<(EntityId, f32)> = None;
1164 for (&id, entity) in &self.viewport.entities {
1165 if !entity.visible { continue; }
1166 let radius = entity.world_bounds_radius().max(0.3);
1167 let oc = origin - entity.position;
1168 let b = oc.dot(dir);
1169 let c = oc.dot(oc) - radius * radius;
1170 let disc = b * b - c;
1171 if disc >= 0.0 {
1172 let t = -b - disc.sqrt();
1173 if t > 0.01 {
1174 if best.map(|(_, bt)| t < bt).unwrap_or(true) {
1175 best = Some((id, t));
1176 }
1177 }
1178 }
1179 }
1180 best.map(|(id, _)| id)
1181 }
1182
1183 pub fn update(&mut self, dt: f32, input: &ViewportInput) {
1184 self.perf.begin_frame();
1185 self.viewport.update(dt, input);
1186 self.perf.end_frame(dt * 1000.0, 0, 0, 0.0, 0.0);
1187
1188 self.cursor_shape = if self.box_select.active {
1190 CursorShape::Crosshair
1191 } else if self.drag_entity_start.is_some() {
1192 CursorShape::Move
1193 } else {
1194 CursorShape::Default
1195 };
1196 }
1197
1198 pub fn frame_selection(&mut self) { self.viewport.frame_selection(); }
1199 pub fn frame_all(&mut self) { self.viewport.frame_all(); }
1200
1201 pub fn spawn_entity_at_cursor(&mut self, kind: EntityKind) -> EntityId {
1202 let panel = self.viewport.active_panel();
1203 let pos = panel.hover.world_pos;
1204 let name = format!("{:?}_{}", kind, self.viewport.entities.len());
1205 self.viewport.spawn_at(name, kind, pos)
1206 }
1207
1208 pub fn entity_count(&self) -> usize { self.viewport.entities.len() }
1209
1210 pub fn selected_count(&self) -> usize { self.viewport.selected_entities.len() }
1211
1212 pub fn render_grid_lines(&self) -> Vec<GridLine> {
1213 let g = &self.viewport.grid;
1214 if !g.visible { return Vec::new(); }
1215 let mut lines = Vec::new();
1216 let half = g.size * 0.5;
1217 let step = g.size / g.subdivisions as f32;
1218 let mut i = 0;
1219 let mut x = -half;
1220 while x <= half + 1e-4 {
1221 let major = (i % g.subdivisions as i32) == 0;
1222 let color = if major { g.color_major } else { g.color_minor };
1223 lines.push(GridLine {
1224 start: Vec3::new(x, 0.0, -half),
1225 end: Vec3::new(x, 0.0, half),
1226 color,
1227 });
1228 lines.push(GridLine {
1229 start: Vec3::new(-half, 0.0, x),
1230 end: Vec3::new( half, 0.0, x),
1231 color,
1232 });
1233 x += step;
1234 i += 1;
1235 }
1236 lines
1237 }
1238}
1239
1240#[derive(Debug, Clone)]
1241pub struct GridLine {
1242 pub start: Vec3,
1243 pub end: Vec3,
1244 pub color: Vec4,
1245}
1246
1247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1248pub enum MouseButton { Left, Right, Middle }
1249
1250#[derive(Debug, Clone, Copy, Default)]
1251pub struct MouseButtons {
1252 pub left: bool,
1253 pub right: bool,
1254 pub middle: bool,
1255}
1256
1257#[derive(Debug, Clone, Copy, Default)]
1258pub struct KeyMods {
1259 pub shift: bool,
1260 pub ctrl: bool,
1261 pub alt: bool,
1262}
1263
1264#[cfg(test)]
1267mod tests {
1268 use super::*;
1269
1270 #[test]
1271 fn viewport_resize() {
1272 let mut v = Viewport::new(1920.0, 1080.0);
1273 v.resize(2560.0, 1440.0);
1274 assert_eq!(v.total_width, 2560.0);
1275 }
1276
1277 #[test]
1278 fn layout_quad_has_four_panels() {
1279 let mut v = Viewport::new(1000.0, 1000.0);
1280 v.set_layout(ViewportLayout::QuadSplit);
1281 assert_eq!(v.panels.len(), 4);
1282 }
1283
1284 #[test]
1285 fn spawn_and_despawn() {
1286 let mut v = Viewport::new(800.0, 600.0);
1287 let id = v.spawn_entity("Test".into(), EntityKind::Marker);
1288 assert!(v.entities.contains_key(&id));
1289 v.despawn(id);
1290 assert!(!v.entities.contains_key(&id));
1291 }
1292
1293 #[test]
1294 fn world_to_screen_roundtrip_origin() {
1295 let v = Viewport::new(800.0, 600.0);
1296 let panel = v.active_panel();
1297 let sp = panel.world_to_screen(Vec3::ZERO);
1298 assert!(sp.x > -1000.0 && sp.x < 2000.0);
1300 }
1301
1302 #[test]
1303 fn gizmo_mode_cycle() {
1304 let mut v = Viewport::new(800.0, 600.0);
1305 v.cycle_gizmo_mode();
1306 assert_eq!(v.gizmo_mode, GizmoMode::Rotate);
1307 v.cycle_gizmo_mode();
1308 assert_eq!(v.gizmo_mode, GizmoMode::Scale);
1309 v.cycle_gizmo_mode();
1310 assert_eq!(v.gizmo_mode, GizmoMode::Translate);
1311 }
1312}