1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// src/editor/hook/cinematic_drive.rs
//
// EditorHook: the start screen's attract camera. While the sidebar is
// previewing a world, a cycle of slow shots (`editor/worlds/cinematic.rs`)
// takes the view: it frames the previewed world's renderable bounds, writes a
// pose onto the live `Camera3D` each frame, and draws the black fade its shots
// hand over through. Everything here is wiring -- the frame dt, the bounds, the
// camera, and when the cycle runs at all.
//
// It is a start-screen presentation and nothing more. The world's own pose is
// held from the frame the cycle takes over and put straight back when the
// screen hands the session a world, so the editing session opens on the camera
// the world declared. Nothing here touches the authored entries, so no shot can
// reach the world file or the session store.
use super::*;
use crate::components::Camera3D;
use framing::CameraPose;
use worlds::cinematic::{Cinematic, Framing};
impl EditorHook {
// Advance the cycle and write its pose. Runs after the frame's routing, so
// a click that picked another world has already restarted the cycle.
pub(super) fn drive_cinematic(&mut self, world: &mut World) {
if !self.start_mode {
return self.reset_cinematic();
}
// A preview still owed a rebuild is showing the world on its way out,
// so the cycle waits at its opening black rather than framing it. The
// pose it held goes back onto that world first: a compile that fails
// leaves it standing, and it must stand on its own camera.
if self.rebuild_preview {
self.stop_cinematic(world);
self.cinematic = Some(Cinematic::new());
return;
}
// The world's own pose, held before anything is written over it: the
// spin shot turns on the spot from it, and it is what opening hands
// back.
if self.cinematic_restore.is_none() {
self.cinematic_restore = camera_pose::read(world);
}
let Some(home) = self.cinematic_restore else {
// No camera to take, so nothing was taken.
return self.reset_cinematic();
};
let Some(framing) = self.cinematic_framing(world, home) else {
// Nothing to frame (an empty world, or one whose camera is driven
// by what it follows). A cycle that had already taken the camera
// gives it back rather than leaving a shot standing on it.
return self.stop_cinematic(world);
};
let dt = self.cinematic_dt();
let pose = {
let cine = self.cinematic.get_or_insert_with(Cinematic::new);
cine.advance(dt);
cine.pose(&framing)
};
camera_pose::write(world, &pose);
}
// Draw the fade at this moment's alpha, or hide it. Left out of the panel
// layer bands, so it covers the previewed world without ever dimming the
// sidebar listing over it.
pub(super) fn drive_cinematic_draw(&self, world: &mut World, vp: [f32; 2], shown: bool) {
// The loading cover owns the same black while it is up, and it leaves
// the sidebar out of it, so the fade stands down rather than dimming
// the listing through a compile.
match self.cinematic {
Some(cine) if shown && !self.loading_preview() => {
worlds::cinematic::apply(world, vp, cine.fade_alpha())
}
_ => worlds::cinematic::hide(world),
}
}
// End the cycle, handing the world back the pose it had when the cycle
// took over. For the commit path: the session adopts the running world, so
// its camera has to be the world's own again before it does.
pub(super) fn stop_cinematic(&mut self, world: &mut World) {
if let Some(pose) = self.cinematic_restore.take() {
camera_pose::write(world, &pose);
}
self.reset_cinematic();
}
// Drop the cycle without restoring: the world it was framing is gone, so
// the pose held for it no longer describes anything.
pub(super) fn reset_cinematic(&mut self) {
self.cinematic = None;
self.cinematic_clock = None;
self.cinematic_restore = None;
}
// Start the cycle over, keeping the pose it holds: the world it was
// framing is being replaced, and the drive hands that pose back to it
// before the rebuild runs, in case the rebuild fails and it stays.
pub(super) fn restart_cinematic(&mut self) {
self.cinematic = None;
self.cinematic_clock = None;
}
// What the shots frame this frame, or `None` when the preview gets no
// cinematic at all. The bounds are the engine's own: every renderable prop
// GraphicsSystem indexed this frame, folded the way the backends fold a
// scene's bounds for a probe bake.
fn cinematic_framing(&self, world: &World, home: CameraPose) -> Option<Framing> {
let vp = self.viewport;
if vp[0] <= 0.0 || vp[1] <= 0.0 {
return None;
}
let cam = world.query::<Camera3D>().next()?;
// A third-person camera is placed by the character it follows, every
// step, so a pose written here would be gone before the frame drew.
if cam.controller.as_ref().is_some_and(|c| c.follow.is_some()) {
return None;
}
let fov = cam.fov_y_degrees.to_radians();
let index = world.resource::<crate::ecs::PickIndex>()?;
let (mn, mx) = concinnity_core::render::reflection_probe::fold_world_bounds(
index.entries.iter().map(|e| (e.bb_min, e.bb_max)),
)?;
Framing::new(mn, mx, fov, vp[0] / vp[1], home)
}
// This frame's dt for the shot clock. The first frame of a cycle takes no
// time, so the cycle always opens on its own first moment.
fn cinematic_dt(&mut self) -> f32 {
let now = std::time::Instant::now();
let dt = self
.cinematic_clock
.map(|last| now.duration_since(last).as_secs_f32())
.unwrap_or(0.0);
self.cinematic_clock = Some(now);
dt
}
}