Skip to main content

konjure_sdk/
geometry.rs

1use crate::{Diagnostic, Geometry, MAX_COORDINATE_M, Vec3};
2use alloc::{format, vec, vec::Vec};
3use serde::{Deserialize, Serialize};
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
6#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8/// Owned triangle or line mesh generated from a built-in [`Geometry`].
9///
10/// Each vector has independently allocated storage. Triangle primitives use
11/// counter-clockwise index triples when viewed from outside; [`Geometry::Line`] has
12/// exactly two indices and zero normals.
13pub struct Mesh {
14    /// Vertex positions in local metres.
15    pub positions: Vec<Vec3>,
16    /// Per-vertex unit normals; line meshes use zero vectors.
17    pub normals: Vec<Vec3>,
18    /// Zero-based indices into [`Self::positions`].
19    pub indices: Vec<u32>,
20}
21/// Tessellates a built-in geometry using fixed deterministic resolution.
22///
23/// The returned mesh owns newly allocated vertex and index buffers. It neither
24/// reads assets nor calls a renderer.
25///
26/// # Errors
27///
28/// Returns an error for invalid dimensions, a group, or an adapter-owned mesh
29/// asset that this core intentionally cannot resolve.
30///
31/// ```
32/// use konjure_sdk::{tessellate, Geometry};
33/// let mesh = tessellate(&Geometry::Sphere { radius_m: 0.5 })?;
34/// assert_eq!(mesh.positions.len(), mesh.normals.len());
35/// # Ok::<(), konjure_sdk::Diagnostic>(())
36/// ```
37pub fn tessellate(geometry: &Geometry) -> Result<Mesh, Diagnostic> {
38    match geometry {
39        Geometry::Group => Err(Diagnostic::plain("a group has no tessellated mesh")),
40        Geometry::Sphere { radius_m } => sphere(*radius_m),
41        Geometry::Torus {
42            major_radius_m,
43            minor_radius_m,
44        } => torus(*major_radius_m, *minor_radius_m),
45        Geometry::Box { size_m } => box_mesh(*size_m),
46        Geometry::Cylinder { radius_m, height_m } => cylinder(*radius_m, *height_m),
47        Geometry::Plane { width_m, depth_m } => plane(*width_m, *depth_m),
48        Geometry::Line { from_m, to_m } => {
49            valid_point(*from_m)?;
50            valid_point(*to_m)?;
51            Ok(Mesh {
52                positions: vec![*from_m, *to_m],
53                normals: vec![Vec3::ZERO; 2],
54                indices: vec![0, 1],
55            })
56        }
57        Geometry::Mesh { asset } => Err(Diagnostic::plain(format!(
58            "mesh asset `{asset}` requires an adapter-provided mesh"
59        ))),
60    }
61}
62fn valid_point(point: Vec3) -> Result<(), Diagnostic> {
63    if [point.x, point.y, point.z]
64        .into_iter()
65        .all(|value| value.is_finite() && value.abs() <= MAX_COORDINATE_M)
66    {
67        Ok(())
68    } else {
69        Err(Diagnostic::plain(
70            "line endpoints must be finite and within coordinate bounds",
71        ))
72    }
73}
74pub(crate) fn finite_positive(v: f64, what: &str) -> Result<(), Diagnostic> {
75    if v.is_finite() && (1e-6..=MAX_COORDINATE_M).contains(&v) {
76        Ok(())
77    } else {
78        Err(Diagnostic::plain(format!(
79            "{what} must be finite and between 0.000001 and {MAX_COORDINATE_M}"
80        )))
81    }
82}
83fn plane(w: f64, d: f64) -> Result<Mesh, Diagnostic> {
84    finite_positive(w, "plane width")?;
85    finite_positive(d, "plane depth")?;
86    let x = w / 2.;
87    let z = d / 2.;
88    Ok(Mesh {
89        positions: vec![
90            Vec3 {
91                x: -x,
92                y: 0.,
93                z: -z,
94            },
95            Vec3 { x, y: 0., z: -z },
96            Vec3 { x, y: 0., z },
97            Vec3 { x: -x, y: 0., z },
98        ],
99        normals: vec![
100            Vec3 {
101                x: 0.,
102                y: 1.,
103                z: 0.
104            };
105            4
106        ],
107        indices: vec![0, 2, 1, 0, 3, 2],
108    })
109}
110fn box_mesh(s: Vec3) -> Result<Mesh, Diagnostic> {
111    finite_positive(s.x, "box width")?;
112    finite_positive(s.y, "box height")?;
113    finite_positive(s.z, "box depth")?;
114    let x = s.x / 2.;
115    let y = s.y / 2.;
116    let z = s.z / 2.;
117    let faces = [
118        (
119            [
120                Vec3 {
121                    x: -x,
122                    y: -y,
123                    z: -z,
124                },
125                Vec3 { x: -x, y, z: -z },
126                Vec3 { x, y, z: -z },
127                Vec3 { x, y: -y, z: -z },
128            ],
129            Vec3 {
130                x: 0.,
131                y: 0.,
132                z: -1.,
133            },
134        ),
135        (
136            [
137                Vec3 { x: -x, y: -y, z },
138                Vec3 { x, y: -y, z },
139                Vec3 { x, y, z },
140                Vec3 { x: -x, y, z },
141            ],
142            Vec3 {
143                x: 0.,
144                y: 0.,
145                z: 1.,
146            },
147        ),
148        (
149            [
150                Vec3 {
151                    x: -x,
152                    y: -y,
153                    z: -z,
154                },
155                Vec3 { x: -x, y: -y, z },
156                Vec3 { x: -x, y, z },
157                Vec3 { x: -x, y, z: -z },
158            ],
159            Vec3 {
160                x: -1.,
161                y: 0.,
162                z: 0.,
163            },
164        ),
165        (
166            [
167                Vec3 { x, y: -y, z: -z },
168                Vec3 { x, y, z: -z },
169                Vec3 { x, y, z },
170                Vec3 { x, y: -y, z },
171            ],
172            Vec3 {
173                x: 1.,
174                y: 0.,
175                z: 0.,
176            },
177        ),
178        (
179            [
180                Vec3 { x: -x, y, z: -z },
181                Vec3 { x: -x, y, z },
182                Vec3 { x, y, z },
183                Vec3 { x, y, z: -z },
184            ],
185            Vec3 {
186                x: 0.,
187                y: 1.,
188                z: 0.,
189            },
190        ),
191        (
192            [
193                Vec3 {
194                    x: -x,
195                    y: -y,
196                    z: -z,
197                },
198                Vec3 { x, y: -y, z: -z },
199                Vec3 { x, y: -y, z },
200                Vec3 { x: -x, y: -y, z },
201            ],
202            Vec3 {
203                x: 0.,
204                y: -1.,
205                z: 0.,
206            },
207        ),
208    ];
209    let mut positions = Vec::with_capacity(24);
210    let mut normals = Vec::with_capacity(24);
211    let mut indices = Vec::with_capacity(36);
212    for (face, normal) in faces {
213        let start = positions.len() as u32;
214        positions.extend_from_slice(&face);
215        normals.extend_from_slice(&[normal; 4]);
216        indices.extend_from_slice(&[start, start + 1, start + 2, start, start + 2, start + 3]);
217    }
218    Ok(Mesh {
219        positions,
220        normals,
221        indices,
222    })
223}
224fn sphere(r: f64) -> Result<Mesh, Diagnostic> {
225    finite_positive(r, "sphere radius")?;
226    let (rings, segments) = (16usize, 32usize);
227    let mut p = Vec::with_capacity((rings + 1) * (segments + 1));
228    let mut n = Vec::with_capacity(p.capacity());
229    for y in 0..=rings {
230        let v = y as f64 / rings as f64;
231        let phi = v * core::f64::consts::PI;
232        for x in 0..=segments {
233            let u = x as f64 / segments as f64;
234            let t = u * 2. * core::f64::consts::PI;
235            let q = Vec3 {
236                x: libm::sin(phi) * libm::cos(t),
237                y: libm::cos(phi),
238                z: libm::sin(phi) * libm::sin(t),
239            };
240            p.push(Vec3 {
241                x: q.x * r,
242                y: q.y * r,
243                z: q.z * r,
244            });
245            n.push(q)
246        }
247    }
248    let mut i = Vec::new();
249    for y in 0..rings {
250        for x in 0..segments {
251            let a = (y * (segments + 1) + x) as u32;
252            let b = a + segments as u32 + 1;
253            i.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b])
254        }
255    }
256    Ok(Mesh {
257        positions: p,
258        normals: n,
259        indices: i,
260    })
261}
262fn torus(major: f64, minor: f64) -> Result<Mesh, Diagnostic> {
263    finite_positive(major, "torus major radius")?;
264    finite_positive(minor, "torus minor radius")?;
265    if minor >= major {
266        return Err(Diagnostic::plain(
267            "torus minor radius must be less than major radius",
268        ));
269    }
270    let (rings, segments) = (48usize, 12usize);
271    let mut positions = Vec::with_capacity((rings + 1) * (segments + 1));
272    let mut normals = Vec::with_capacity(positions.capacity());
273    for ring in 0..=rings {
274        let u = ring as f64 / rings as f64 * 2. * core::f64::consts::PI;
275        let (cu, su) = (libm::cos(u), libm::sin(u));
276        for segment in 0..=segments {
277            let v = segment as f64 / segments as f64 * 2. * core::f64::consts::PI;
278            let (cv, sv) = (libm::cos(v), libm::sin(v));
279            let normal = Vec3 {
280                x: cu * cv,
281                y: sv,
282                z: su * cv,
283            };
284            positions.push(Vec3 {
285                x: (major + minor * cv) * cu,
286                y: minor * sv,
287                z: (major + minor * cv) * su,
288            });
289            normals.push(normal);
290        }
291    }
292    let mut indices = Vec::with_capacity(rings * segments * 6);
293    for ring in 0..rings {
294        for segment in 0..segments {
295            let a = (ring * (segments + 1) + segment) as u32;
296            let b = a + segments as u32 + 1;
297            indices.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
298        }
299    }
300    Ok(Mesh {
301        positions,
302        normals,
303        indices,
304    })
305}
306fn cylinder(r: f64, h: f64) -> Result<Mesh, Diagnostic> {
307    finite_positive(r, "cylinder radius")?;
308    finite_positive(h, "cylinder height")?;
309    let segments = 16usize;
310    let mut p = Vec::with_capacity(2 * segments + 2);
311    let mut n = Vec::with_capacity(p.capacity());
312    for y in [-h / 2., h / 2.] {
313        for x in 0..segments {
314            let t = x as f64 / segments as f64 * 2. * core::f64::consts::PI;
315            let q = Vec3 {
316                x: libm::cos(t),
317                y: 0.,
318                z: libm::sin(t),
319            };
320            p.push(Vec3 {
321                x: q.x * r,
322                y,
323                z: q.z * r,
324            });
325            n.push(q)
326        }
327    }
328    let bottom = p.len() as u32;
329    p.push(Vec3 {
330        x: 0.,
331        y: -h / 2.,
332        z: 0.,
333    });
334    n.push(Vec3 {
335        x: 0.,
336        y: -1.,
337        z: 0.,
338    });
339    let top = p.len() as u32;
340    p.push(Vec3 {
341        x: 0.,
342        y: h / 2.,
343        z: 0.,
344    });
345    n.push(Vec3 {
346        x: 0.,
347        y: 1.,
348        z: 0.,
349    });
350    let mut i = Vec::new();
351    for x in 0..segments {
352        let next = (x + 1) % segments;
353        i.extend_from_slice(&[
354            x as u32,
355            (x + segments) as u32,
356            next as u32,
357            next as u32,
358            (x + segments) as u32,
359            (next + segments) as u32,
360            bottom,
361            x as u32,
362            next as u32,
363            top,
364            (next + segments) as u32,
365            (x + segments) as u32,
366        ]);
367    }
368    Ok(Mesh {
369        positions: p,
370        normals: n,
371        indices: i,
372    })
373}