1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use pfx_core::anim::{Atlas, ClipEvent, FrameEvent, Playback, Rig, SpriteClip, SpriteSheet};
5use pfx_scene::Entry;
6use pfx_scene::types as file;
7
8use super::build::{Places, Reader, Step, content_from, inner};
9use super::{Content, Matrix, Object, SceneError, SceneMesh};
10
11pub const SPRITE_CLIP: &str = "";
12
13#[derive(Clone, Debug, Default, PartialEq)]
14pub struct Posed {
15 pub time: f32,
16 pub models: Vec<Matrix>,
17 pub palettes: BTreeMap<usize, Vec<Matrix>>,
18 pub frames: BTreeMap<usize, [f32; 4]>,
19}
20
21impl Posed {
22 pub fn at(time: f32) -> Self {
23 Self {
24 time,
25 ..Self::default()
26 }
27 }
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub struct Animation {
32 pub clip: Option<String>,
33 pub blend: Vec<(String, f32)>,
34 pub speed: f32,
35 pub playback: Playback,
36 pub start: f32,
37 pub rig: Option<Arc<Rig>>,
38 pub sprite: Option<Sprite>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct Sprite {
43 pub sheet: Arc<SpriteSheet>,
44 pub content: String,
45}
46
47impl Animation {
48 pub fn plays(&self) -> bool {
49 self.clip.is_some() || !self.blend.is_empty()
50 }
51
52 pub fn still_frame(&self) -> Option<[f32; 4]> {
53 let sprite = self.sprite.as_ref()?;
54 let sheet = &sprite.sheet;
55 let frame = self
56 .clip
57 .as_deref()
58 .and_then(|clip| sheet.clip(clip))
59 .map(|clip| {
60 let clip = &sheet.clips()[clip];
61 let step = (self.start as f64 * clip.fps() as f64).floor() as u64;
62 clip.frames()[clip.position(step)]
63 })
64 .unwrap_or(0);
65 sheet.atlas().uv(frame)
66 }
67}
68
69pub fn surface(frame: [f32; 4]) -> ([f32; 2], [f32; 2], [f32; 4]) {
70 (
71 [frame[0], frame[1]],
72 [frame[2] - frame[0], frame[3] - frame[1]],
73 frame,
74 )
75}
76
77fn playback(repeat: Option<file::Repeat>) -> Option<Playback> {
78 repeat.map(|repeat| match repeat {
79 file::Repeat::Loop => Playback::Loop,
80 file::Repeat::Once => Playback::Once,
81 file::Repeat::PingPong => Playback::PingPong,
82 })
83}
84
85pub fn content_key(object: &str) -> String {
86 format!("sprite:{object}")
87}
88
89pub(super) fn build_animations(
90 places: &Places,
91 entries: &[Entry<file::Object>],
92 objects: &mut [Object],
93 meshes: &BTreeMap<String, SceneMesh>,
94 contents: &mut BTreeMap<String, Content>,
95 reader: &mut Reader,
96) -> Result<BTreeMap<String, Animation>, SceneError> {
97 let mut out = BTreeMap::new();
98 for (entry, object) in entries.iter().zip(objects.iter_mut()) {
99 let Some(found) = &entry.value.animation else {
100 continue;
101 };
102 let name = entry.key.as_str();
103 let refuse = |key: Option<&str>, message: String| {
104 let mut at = vec![Step::Named("object", inner(name)), Step::Key("animation")];
105 if let Some(key) = key {
106 at.push(Step::Key(key));
107 }
108 places.refusal(&entry.file, &at, format!("object {name}: {message}"))
109 };
110 let looped = playback(found.looping).unwrap_or_default();
111 let mut animation = Animation {
112 clip: found.clip.clone(),
113 blend: found
114 .blend
115 .iter()
116 .flatten()
117 .map(|blend| {
118 (
119 blend.clip.clone(),
120 blend.weight.unwrap_or(file::BlendClip::WEIGHT),
121 )
122 })
123 .collect(),
124 speed: found.speed.unwrap_or(file::Animation::SPEED),
125 playback: looped,
126 start: found.start.unwrap_or(file::Animation::START),
127 rig: None,
128 sprite: None,
129 };
130 if found.is_sprite() {
131 let atlas_path = found.atlas.as_deref().unwrap_or_default();
132 let path = places.root.join(atlas_path);
133 let bytes = reader
134 .read(&path)
135 .map_err(|message| refuse(Some("atlas"), message))?;
136 let content =
137 content_from(path, &bytes).map_err(|message| refuse(Some("atlas"), message))?;
138 if let Some(other) = &object.content {
139 return Err(refuse(
140 None,
141 format!("it shows content {other} and a sprite; give it one"),
142 ));
143 }
144 let size = [content.width, content.height];
145 let atlas = match (&found.grid, &found.frames) {
146 (Some([columns, rows]), _) => Atlas::grid(
147 size,
148 [size[0] / columns.max(&1), size[1] / rows.max(&1)],
149 Some((columns * rows) as usize),
150 ),
151 (None, Some(frames)) => Atlas::rects(size, frames.clone()),
152 (None, None) => Atlas::grid(size, size, None),
153 }
154 .map_err(|error| {
155 refuse(
156 Some(if found.grid.is_some() {
157 "grid"
158 } else {
159 "frames"
160 }),
161 error.to_string(),
162 )
163 })?;
164 let fps = found.fps.unwrap_or(file::Animation::FPS);
165 let mut clips: Vec<SpriteClip> = Vec::new();
166 if found.clips.is_empty() {
167 clips.push(
168 SpriteClip::new(SPRITE_CLIP, (0..atlas.len()).collect(), fps, looped)
169 .map_err(|error| refuse(None, error.to_string()))?,
170 );
171 if animation.clip.is_none() {
172 animation.clip = Some(SPRITE_CLIP.to_string());
173 }
174 }
175 for (clip, range) in &found.clips {
176 if range.to as usize >= atlas.len() {
177 return Err(refuse(
178 Some("clips"),
179 format!(
180 "sprite clip {clip} runs to frame {}, past the atlas's last frame {}",
181 range.to,
182 atlas.len().saturating_sub(1)
183 ),
184 ));
185 }
186 clips.push(
187 SpriteClip::new(
188 clip.clone(),
189 (range.from as usize..=range.to as usize).collect(),
190 range.fps.unwrap_or(fps),
191 playback(range.looping).unwrap_or(looped),
192 )
193 .map_err(|error| refuse(Some("clips"), error.to_string()))?,
194 );
195 }
196 let mut marks: BTreeMap<String, Vec<FrameEvent>> = BTreeMap::new();
197 for event in &found.events {
198 let clip = event
199 .clip
200 .clone()
201 .or_else(|| animation.clip.clone())
202 .unwrap_or_default();
203 let Some(index) = clips.iter().position(|candidate| candidate.name() == clip)
204 else {
205 return Err(refuse(
206 Some("events"),
207 format!(
208 "event {} marks clip {clip}, which the sprite has not",
209 event.name
210 ),
211 ));
212 };
213 let target = &clips[index];
214 let frame = match (event.frame, event.time) {
215 (Some(frame), _) => frame as usize,
216 (None, Some(time)) => (time as f64 * target.fps() as f64).floor() as usize,
217 (None, None) => continue,
218 };
219 if frame < target.frames().len() {
220 marks
221 .entry(clip)
222 .or_default()
223 .push(FrameEvent::new(frame, event.name.clone()));
224 }
225 }
226 let clips = clips
227 .into_iter()
228 .map(|clip| match marks.remove(clip.name()) {
229 Some(events) => clip.with_events(events),
230 None => Ok(clip),
231 })
232 .collect::<Result<Vec<_>, _>>()
233 .map_err(|error| refuse(Some("events"), error.to_string()))?;
234 let sheet =
235 SpriteSheet::new(atlas, clips).map_err(|error| refuse(None, error.to_string()))?;
236 if let Some(clip) = &animation.clip
237 && sheet.clip(clip).is_none()
238 {
239 return Err(refuse(
240 Some("clip"),
241 format!("the sprite has no clip {clip}"),
242 ));
243 }
244 let key = content_key(name);
245 contents.insert(key.clone(), content);
246 object.content = Some(key.clone());
247 animation.sprite = Some(Sprite {
248 sheet: Arc::new(sheet),
249 content: key,
250 });
251 } else {
252 let Some(rig) = meshes.get(&object.mesh).and_then(|mesh| mesh.rig.clone()) else {
253 return Err(refuse(
254 None,
255 format!("mesh {} holds no animation clips", object.mesh),
256 ));
257 };
258 let known = |clip: &str| rig.clip(clip).is_some();
259 if let Some(clip) = &animation.clip
260 && !known(clip)
261 {
262 return Err(refuse(
263 Some("clip"),
264 format!("mesh {} has no clip {clip}", object.mesh),
265 ));
266 }
267 if let Some((clip, _)) = animation.blend.iter().find(|(clip, _)| !known(clip)) {
268 return Err(refuse(
269 Some("blend"),
270 format!("mesh {} has no clip {clip}", object.mesh),
271 ));
272 }
273 let mut marks: BTreeMap<String, Vec<ClipEvent>> = BTreeMap::new();
274 for event in &found.events {
275 let Some(clip) = event.clip.clone().or_else(|| animation.clip.clone()) else {
276 return Err(refuse(
277 Some("events"),
278 format!("event {} names no clip", event.name),
279 ));
280 };
281 let Some(index) = rig.clip(&clip) else {
282 return Err(refuse(
283 Some("events"),
284 format!(
285 "event {} marks clip {clip}, which mesh {} has not",
286 event.name, object.mesh
287 ),
288 ));
289 };
290 let Some(time) = event.time else {
291 return Err(refuse(
292 Some("events"),
293 format!("event {} of a glTF clip takes a time", event.name),
294 ));
295 };
296 if time <= rig.clips()[index].duration() {
297 marks
298 .entry(clip)
299 .or_default()
300 .push(ClipEvent::new(time, event.name.clone()));
301 }
302 }
303 let mut rig = Rig::clone(&rig);
304 for (clip, events) in marks {
305 rig = rig
306 .with_events(&clip, events)
307 .map_err(|error| refuse(Some("events"), error.to_string()))?;
308 }
309 animation.rig = Some(Arc::new(rig));
310 }
311 out.insert(name.to_string(), animation);
312 }
313 Ok(out)
314}