1use anyhow::Result;
20use midly::{Format, Header, MetaMessage, MidiMessage, Smf, Timing, TrackEvent, TrackEventKind};
21
22#[derive(Debug, Clone)]
23pub struct ObjBuilding {
24 pub name: String,
25 pub x: f32,
26 pub y: f32,
27 pub w: f32,
28 pub d: f32,
29 pub h: f32,
30}
31
32pub fn render_obj(buildings: &[ObjBuilding]) -> String {
33 let mut out = String::new();
34 out.push_str("# tokmd code city\n");
35 let mut vertex_index = 1usize;
36
37 for b in buildings {
38 out.push_str(&format!("o {}\n", sanitize_name(&b.name)));
39 let (x, y, z) = (b.x, b.y, 0.0f32);
40 let (w, d, h) = (b.w, b.d, b.h);
41
42 let v = [
43 (x, y, z),
44 (x + w, y, z),
45 (x + w, y + d, z),
46 (x, y + d, z),
47 (x, y, z + h),
48 (x + w, y, z + h),
49 (x + w, y + d, z + h),
50 (x, y + d, z + h),
51 ];
52 for (vx, vy, vz) in v {
53 out.push_str(&format!("v {} {} {}\n", vx, vy, vz));
54 }
55
56 let faces = [
57 [1, 2, 3, 4],
58 [5, 6, 7, 8],
59 [1, 2, 6, 5],
60 [2, 3, 7, 6],
61 [3, 4, 8, 7],
62 [4, 1, 5, 8],
63 ];
64 for face in faces {
65 out.push_str(&format!(
66 "f {} {} {} {}\n",
67 vertex_index + face[0] - 1,
68 vertex_index + face[1] - 1,
69 vertex_index + face[2] - 1,
70 vertex_index + face[3] - 1,
71 ));
72 }
73
74 vertex_index += 8;
75 }
76
77 out
78}
79
80fn sanitize_name(name: &str) -> String {
81 name.chars()
82 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
83 .collect()
84}
85
86#[derive(Debug, Clone)]
87pub struct MidiNote {
88 pub key: u8,
89 pub velocity: u8,
90 pub start: u32,
91 pub duration: u32,
92 pub channel: u8,
93}
94
95pub fn render_midi(notes: &[MidiNote], tempo_bpm: u16) -> Result<Vec<u8>> {
96 let ticks_per_quarter = 480u16;
97 let mut events: Vec<(u32, TrackEventKind<'static>)> = Vec::new();
98
99 let tempo = 60_000_000u32 / tempo_bpm.max(1) as u32;
100 events.push((0, TrackEventKind::Meta(MetaMessage::Tempo(tempo.into()))));
101
102 for note in notes {
103 let ch = note.channel.min(15).into();
104 events.push((
105 note.start,
106 TrackEventKind::Midi {
107 channel: ch,
108 message: MidiMessage::NoteOn {
109 key: note.key.into(),
110 vel: note.velocity.into(),
111 },
112 },
113 ));
114 events.push((
115 note.start + note.duration,
116 TrackEventKind::Midi {
117 channel: ch,
118 message: MidiMessage::NoteOff {
119 key: note.key.into(),
120 vel: 0.into(),
121 },
122 },
123 ));
124 }
125
126 events.sort_by(|a, b| {
127 a.0.cmp(&b.0).then_with(|| {
128 let rank = |k: &TrackEventKind| -> (u8, u8, u8) {
129 match k {
130 TrackEventKind::Meta(_) => (0, 0, 0),
131 TrackEventKind::Midi {
132 channel,
133 message: MidiMessage::NoteOff { key, .. },
134 } => (1, (*channel).into(), (*key).into()),
135 TrackEventKind::Midi {
136 channel,
137 message: MidiMessage::NoteOn { key, .. },
138 } => (2, (*channel).into(), (*key).into()),
139 _ => (3, 0, 0),
140 }
141 };
142 rank(&a.1).cmp(&rank(&b.1))
143 })
144 });
145
146 let mut track: Vec<TrackEvent> = Vec::new();
147 let mut last_time = 0u32;
148 for (time, kind) in events {
149 let delta = time.saturating_sub(last_time);
150 last_time = time;
151 track.push(TrackEvent {
152 delta: delta.into(),
153 kind,
154 });
155 }
156
157 track.push(TrackEvent {
158 delta: 0.into(),
159 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
160 });
161
162 let smf = Smf {
163 header: Header::new(
164 Format::SingleTrack,
165 Timing::Metrical(ticks_per_quarter.into()),
166 ),
167 tracks: vec![track],
168 };
169
170 let mut out = Vec::new();
171 smf.write_std(&mut out)?;
172 Ok(out)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
181 fn sanitize_name_replaces_non_alphanumeric() {
182 assert_eq!(sanitize_name("hello world"), "hello_world");
183 assert_eq!(sanitize_name("src/main.rs"), "src_main_rs");
184 assert_eq!(sanitize_name("foo-bar_baz"), "foo_bar_baz");
185 }
186
187 #[test]
188 fn sanitize_name_preserves_alphanumeric() {
189 assert_eq!(sanitize_name("abc123"), "abc123");
190 }
191
192 #[test]
193 fn sanitize_name_empty() {
194 assert_eq!(sanitize_name(""), "");
195 }
196
197 #[test]
199 fn render_obj_empty_input() {
200 let result = render_obj(&[]);
201 assert_eq!(result, "# tokmd code city\n");
202 }
203
204 #[test]
205 fn render_obj_single_building() {
206 let buildings = vec![ObjBuilding {
207 name: "main".into(),
208 x: 0.0,
209 y: 0.0,
210 w: 1.0,
211 d: 1.0,
212 h: 2.0,
213 }];
214 let result = render_obj(&buildings);
215 assert!(result.starts_with("# tokmd code city\n"));
216 assert!(result.contains("o main\n"));
217 assert_eq!(result.matches("\nv ").count(), 8);
219 assert_eq!(result.matches("\nf ").count(), 6);
221 }
222
223 #[test]
224 fn render_obj_multiple_buildings() {
225 let buildings = vec![
226 ObjBuilding {
227 name: "a".into(),
228 x: 0.0,
229 y: 0.0,
230 w: 1.0,
231 d: 1.0,
232 h: 1.0,
233 },
234 ObjBuilding {
235 name: "b".into(),
236 x: 2.0,
237 y: 0.0,
238 w: 1.0,
239 d: 1.0,
240 h: 3.0,
241 },
242 ];
243 let result = render_obj(&buildings);
244 assert!(result.contains("o a\n"));
245 assert!(result.contains("o b\n"));
246 assert_eq!(result.matches("\nv ").count(), 16);
248 assert_eq!(result.matches("\nf ").count(), 12);
250 }
251
252 #[test]
253 fn render_obj_sanitizes_names() {
254 let buildings = vec![ObjBuilding {
255 name: "src/main.rs".into(),
256 x: 0.0,
257 y: 0.0,
258 w: 1.0,
259 d: 1.0,
260 h: 1.0,
261 }];
262 let result = render_obj(&buildings);
263 assert!(result.contains("o src_main_rs\n"));
264 assert!(!result.contains("o src/main.rs\n"));
265 }
266
267 #[test]
269 fn render_midi_deterministic_overlap() {
270 let notes1 = vec![
272 MidiNote {
273 key: 60,
274 velocity: 100,
275 start: 0,
276 duration: 480,
277 channel: 0,
278 },
279 MidiNote {
280 key: 64,
281 velocity: 100,
282 start: 0,
283 duration: 480,
284 channel: 1,
285 },
286 ];
287 let notes2 = vec![
288 MidiNote {
289 key: 64,
290 velocity: 100,
291 start: 0,
292 duration: 480,
293 channel: 1,
294 },
295 MidiNote {
296 key: 60,
297 velocity: 100,
298 start: 0,
299 duration: 480,
300 channel: 0,
301 },
302 ];
303
304 let result1 = render_midi(¬es1, 120).unwrap();
305 let result2 = render_midi(¬es2, 120).unwrap();
306
307 assert_eq!(
308 result1, result2,
309 "Output must be deterministic regardless of input note order"
310 );
311 }
312
313 #[test]
314 fn render_midi_empty_notes() {
315 let result = render_midi(&[], 120).unwrap();
316 assert!(!result.is_empty());
318 assert_eq!(&result[..4], b"MThd");
320 }
321
322 #[test]
323 fn render_midi_single_note() {
324 let notes = vec![MidiNote {
325 key: 60,
326 velocity: 100,
327 start: 0,
328 duration: 480,
329 channel: 0,
330 }];
331 let result = render_midi(¬es, 120).unwrap();
332 assert_eq!(&result[..4], b"MThd");
333 assert!(result.len() > 14); }
336
337 #[test]
338 fn render_midi_multiple_notes() {
339 let notes = vec![
340 MidiNote {
341 key: 60,
342 velocity: 100,
343 start: 0,
344 duration: 480,
345 channel: 0,
346 },
347 MidiNote {
348 key: 64,
349 velocity: 80,
350 start: 480,
351 duration: 480,
352 channel: 0,
353 },
354 MidiNote {
355 key: 67,
356 velocity: 60,
357 start: 960,
358 duration: 480,
359 channel: 1,
360 },
361 ];
362 let result = render_midi(¬es, 120).unwrap();
363 assert_eq!(&result[..4], b"MThd");
364 }
365
366 #[test]
367 fn render_midi_channel_clamped_to_15() {
368 let notes = vec![MidiNote {
369 key: 60,
370 velocity: 100,
371 start: 0,
372 duration: 480,
373 channel: 255, }];
375 let result = render_midi(¬es, 120).unwrap();
377 assert!(!result.is_empty());
378 }
379
380 #[test]
381 fn render_midi_tempo_min_clamped() {
382 let notes = vec![MidiNote {
383 key: 60,
384 velocity: 100,
385 start: 0,
386 duration: 480,
387 channel: 0,
388 }];
389 let result = render_midi(¬es, 0).unwrap();
391 assert!(!result.is_empty());
392 }
393}