Skip to main content

proof_engine/editor/
viewport.rs

1// viewport.rs — Real-time editor viewport: wires EditorCamera, scene entities,
2// gizmos, grid/axes overlays, and the GPU Pipeline into a single render loop.
3
4use 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// ─── Viewport dimensions ────────────────────────────────────────────────────
12
13#[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// ─── Scene entity representation for viewport ───────────────────────────────
44
45#[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,   // Euler angles (degrees)
96    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// ─── Light representation ────────────────────────────────────────────────────
146
147#[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// ─── Grid / axes ─────────────────────────────────────────────────────────────
205
206#[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// ─── Viewport rendering settings ────────────────────────────────────────────
242
243#[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// ─── Viewport split / layout ─────────────────────────────────────────────────
449
450#[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// ─── Selection outline ───────────────────────────────────────────────────────
502
503#[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// ─── Hover state ─────────────────────────────────────────────────────────────
525
526#[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// ─── Viewport panel ──────────────────────────────────────────────────────────
548
549#[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        // Set default snap views based on panel index
569        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        // EditorCamera stores projection internally; aspect is set via the projection field
596    }
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    /// Update hover position given raw cursor input
626    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        // Plane intersect at y=0 as default
634        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// ─── Main Viewport struct ─────────────────────────────────────────────────────
661
662#[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        // Add panels if needed
735        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        // Resize existing
743        for (i, rect) in rects.iter().enumerate() {
744            if let Some(panel) = self.panels.get_mut(i) {
745                panel.resize(*rect);
746            }
747        }
748        // Trim
749        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            // Unlink children
783            for child_id in &e.children {
784                if let Some(child) = self.entities.get_mut(child_id) {
785                    child.parent = None;
786                }
787            }
788            // Unlink from parent
789            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        // Determine active panel from cursor
896        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    /// Build GPU frame data for the current frame
905    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// ─── Frame data ──────────────────────────────────────────────────────────────
923
924#[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// ─── Input ───────────────────────────────────────────────────────────────────
937
938#[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// ─── Box selection ───────────────────────────────────────────────────────────
955
956#[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// ─── Drag-drop target ────────────────────────────────────────────────────────
991
992#[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// ─── Stats overlay ───────────────────────────────────────────────────────────
1001
1002#[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// ─── Viewport controller (high-level) ────────────────────────────────────────
1050
1051#[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                    // Try entity pick first
1100                    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                    // Only count if the box is big enough (not a click)
1124                    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            // Orbit / pan / free-fly handled by EditorCamera
1140            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        // Update cursor shape
1189        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// ─── Tests ───────────────────────────────────────────────────────────────────
1265
1266#[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        // Origin should be somewhere in the viewport (not off screen)
1299        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}