Skip to main content

oxihuman_export/
a_frame_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A-Frame VR/AR scene format export.
6
7/// An A-Frame entity in the scene.
8#[derive(Clone, Debug)]
9pub struct AFrameEntity {
10    pub tag: String,
11    pub attributes: Vec<(String, String)>,
12}
13
14/// An A-Frame scene document.
15#[derive(Clone, Debug, Default)]
16pub struct AFrameScene {
17    pub entities: Vec<AFrameEntity>,
18    pub title: String,
19}
20
21/// Create a new empty A-Frame scene.
22pub fn new_aframe_scene(title: &str) -> AFrameScene {
23    AFrameScene {
24        entities: Vec::new(),
25        title: title.to_string(),
26    }
27}
28
29/// Add an entity to the scene.
30pub fn aframe_push_entity(scene: &mut AFrameScene, tag: &str, attrs: Vec<(&str, &str)>) {
31    scene.entities.push(AFrameEntity {
32        tag: tag.to_string(),
33        attributes: attrs
34            .iter()
35            .map(|(k, v)| (k.to_string(), v.to_string()))
36            .collect(),
37    });
38}
39
40/// Return the number of entities in the scene.
41pub fn aframe_entity_count(scene: &AFrameScene) -> usize {
42    scene.entities.len()
43}
44
45/// Render the scene to an HTML string.
46pub fn render_aframe_html(scene: &AFrameScene) -> String {
47    let mut out = format!(
48        "<!DOCTYPE html>\n<html>\n<head><title>{}</title>\n\
49         <script src=\"https://aframe.io/releases/1.4.0/aframe.min.js\"></script>\n\
50         </head>\n<body>\n<a-scene>\n",
51        html_escape_af(&scene.title)
52    );
53    for entity in &scene.entities {
54        out.push_str(&format!("  <{}", entity.tag));
55        for (k, v) in &entity.attributes {
56            out.push_str(&format!(" {}=\"{}\"", k, html_escape_af(v)));
57        }
58        out.push_str(&format!("></{}>", entity.tag));
59        out.push('\n');
60    }
61    out.push_str("</a-scene>\n</body>\n</html>\n");
62    out
63}
64
65/// Add a mesh (as a box primitive) to the scene.
66pub fn aframe_add_box(scene: &mut AFrameScene, pos: [f32; 3], color: &str) {
67    let pos_str = format!("{} {} {}", pos[0], pos[1], pos[2]);
68    aframe_push_entity(
69        scene,
70        "a-box",
71        vec![("position", &pos_str), ("color", color)],
72    );
73}
74
75/// Add a sphere primitive.
76pub fn aframe_add_sphere(scene: &mut AFrameScene, radius: f32, pos: [f32; 3], color: &str) {
77    let pos_str = format!("{} {} {}", pos[0], pos[1], pos[2]);
78    let r_str = format!("{}", radius);
79    aframe_push_entity(
80        scene,
81        "a-sphere",
82        vec![("position", &pos_str), ("radius", &r_str), ("color", color)],
83    );
84}
85
86/// Export mesh positions as a-frame boxes.
87pub fn export_mesh_as_aframe(positions: &[[f32; 3]], title: &str) -> AFrameScene {
88    let mut scene = new_aframe_scene(title);
89    for &p in positions {
90        aframe_add_box(&mut scene, p, "#4CC3D9");
91    }
92    scene
93}
94
95/// Validate scene (non-empty title, entities have non-empty tags).
96pub fn validate_aframe(scene: &AFrameScene) -> bool {
97    !scene.title.is_empty() && scene.entities.iter().all(|e| !e.tag.is_empty())
98}
99
100fn html_escape_af(s: &str) -> String {
101    s.replace('&', "&amp;")
102        .replace('<', "&lt;")
103        .replace('>', "&gt;")
104        .replace('"', "&quot;")
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn new_scene_empty() {
113        let s = new_aframe_scene("test");
114        assert_eq!(aframe_entity_count(&s), 0);
115        assert_eq!(s.title, "test");
116    }
117
118    #[test]
119    fn push_entity_increments_count() {
120        let mut s = new_aframe_scene("test");
121        aframe_push_entity(&mut s, "a-box", vec![("color", "red")]);
122        assert_eq!(aframe_entity_count(&s), 1);
123    }
124
125    #[test]
126    fn render_html_contains_aframe_tag() {
127        let s = new_aframe_scene("demo");
128        let html = render_aframe_html(&s);
129        assert!(html.contains("<a-scene>"));
130    }
131
132    #[test]
133    fn add_box_adds_entity() {
134        let mut s = new_aframe_scene("test");
135        aframe_add_box(&mut s, [0.0, 1.0, 0.0], "#fff");
136        assert_eq!(aframe_entity_count(&s), 1);
137        assert_eq!(s.entities[0].tag, "a-box");
138    }
139
140    #[test]
141    fn add_sphere_adds_entity() {
142        let mut s = new_aframe_scene("test");
143        aframe_add_sphere(&mut s, 1.5, [0.0, 0.0, 0.0], "blue");
144        assert_eq!(s.entities[0].tag, "a-sphere");
145    }
146
147    #[test]
148    fn export_mesh_creates_one_entity_per_vertex() {
149        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
150        let s = export_mesh_as_aframe(&pos, "mesh");
151        assert_eq!(aframe_entity_count(&s), 3);
152    }
153
154    #[test]
155    fn validate_valid_scene() {
156        let mut s = new_aframe_scene("test");
157        aframe_push_entity(&mut s, "a-box", vec![]);
158        assert!(validate_aframe(&s));
159    }
160
161    #[test]
162    fn html_escape_works() {
163        let s = html_escape_af("<script>");
164        assert!(s.contains("&lt;") && s.contains("&gt;"));
165    }
166}