Skip to main content

runmat_runtime/builtins/geometry/
mod.rs

1use runmat_builtins::{
2    Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
3    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
4    ClassDef, MethodDef, ObjectInstance, StructValue, Tensor, Value,
5};
6use runmat_geometry_core::GeometryAsset;
7use runmat_macros::runtime_builtin;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10use std::collections::HashMap;
11use std::sync::OnceLock;
12
13use crate::builtins::io::json::jsondecode::value_from_json;
14use crate::operations::{OperationContext, OperationEnvelope, OperationErrorEnvelope};
15use crate::{build_runtime_error, BuiltinResult, RuntimeError};
16
17pub mod triangulation;
18
19pub const GEOMETRY_ASSET_CLASS: &str = "geometry.Asset";
20const GEOMETRY_INSPECT_RESULT_CLASS: &str = "geometry.InspectResult";
21pub const GEOMETRY_ASSET_JSON_PROPERTY: &str = "__runmat_geometry_asset_json";
22const GEOMETRY_LOAD_NAME: &str = "geometry.load";
23const GEOMETRY_INSPECT_NAME: &str = "geometry.inspect";
24const GEOMETRY_LIST_REGIONS_NAME: &str = "geometry.listRegions";
25const GEOMETRY_MESHES_NAME: &str = "geometry.meshes";
26
27const STRUCT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
28    name: "result",
29    ty: BuiltinParamType::Any,
30    arity: BuiltinParamArity::Required,
31    default: None,
32    description: "Operation result as a struct.",
33}];
34const PATH_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
35    name: "path",
36    ty: BuiltinParamType::StringScalar,
37    arity: BuiltinParamArity::Required,
38    default: None,
39    description: "Path to the geometry file.",
40}];
41
42const GEOMETRY_LOAD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
43    label: "asset = geometry.load(path)",
44    inputs: &PATH_INPUT,
45    outputs: &STRUCT_OUTPUT,
46}];
47const GEOMETRY_INSPECT_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
48    label: "info = geometry.inspect(path)",
49    inputs: &PATH_INPUT,
50    outputs: &STRUCT_OUTPUT,
51}];
52const GEOMETRY_LIST_REGIONS_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
53    [BuiltinSignatureDescriptor {
54        label: "regions = geometry.listRegions(asset)",
55        inputs: &STRUCT_OUTPUT,
56        outputs: &STRUCT_OUTPUT,
57    }];
58const GEOMETRY_MESHES_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
59    label: "meshes = geometry.meshes(asset)",
60    inputs: &STRUCT_OUTPUT,
61    outputs: &STRUCT_OUTPUT,
62}];
63
64const GEOMETRY_LOAD_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
65    code: "RM.GEOMETRY.LOAD.IO",
66    identifier: Some("RunMat:geometry:load:IoFailure"),
67    when: "The geometry file cannot be read.",
68    message: "geometry.load: failed to read geometry file",
69};
70const GEOMETRY_LOAD_ERROR_OPERATION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
71    code: "RM.GEOMETRY.LOAD.OPERATION_FAILED",
72    identifier: Some("RunMat:geometry:load:OperationFailed"),
73    when: "The geometry load operation rejects or cannot import the file.",
74    message: "geometry.load: operation failed",
75};
76const GEOMETRY_LOAD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
77    code: "RM.GEOMETRY.LOAD.INTERNAL",
78    identifier: Some("RunMat:geometry:load:Internal"),
79    when: "The loaded geometry asset cannot be converted to a RunMat value.",
80    message: "geometry.load: internal error",
81};
82const GEOMETRY_LOAD_ERRORS: [BuiltinErrorDescriptor; 3] = [
83    GEOMETRY_LOAD_ERROR_IO,
84    GEOMETRY_LOAD_ERROR_OPERATION,
85    GEOMETRY_LOAD_ERROR_INTERNAL,
86];
87pub const GEOMETRY_LOAD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
88    signatures: &GEOMETRY_LOAD_SIGNATURES,
89    output_mode: BuiltinOutputMode::Fixed,
90    completion_policy: BuiltinCompletionPolicy::Public,
91    errors: &GEOMETRY_LOAD_ERRORS,
92};
93
94const GEOMETRY_INSPECT_ERROR_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
95    code: "RM.GEOMETRY.INSPECT.IO",
96    identifier: Some("RunMat:geometry:inspect:IoFailure"),
97    when: "The geometry file cannot be read.",
98    message: "geometry.inspect: failed to read geometry file",
99};
100const GEOMETRY_INSPECT_ERROR_OPERATION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
101    code: "RM.GEOMETRY.INSPECT.OPERATION_FAILED",
102    identifier: Some("RunMat:geometry:inspect:OperationFailed"),
103    when: "The geometry inspection operation fails.",
104    message: "geometry.inspect: operation failed",
105};
106const GEOMETRY_INSPECT_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
107    code: "RM.GEOMETRY.INSPECT.INTERNAL",
108    identifier: Some("RunMat:geometry:inspect:Internal"),
109    when: "The inspection result cannot be converted to a RunMat value.",
110    message: "geometry.inspect: internal error",
111};
112const GEOMETRY_INSPECT_ERRORS: [BuiltinErrorDescriptor; 3] = [
113    GEOMETRY_INSPECT_ERROR_IO,
114    GEOMETRY_INSPECT_ERROR_OPERATION,
115    GEOMETRY_INSPECT_ERROR_INTERNAL,
116];
117pub const GEOMETRY_INSPECT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
118    signatures: &GEOMETRY_INSPECT_SIGNATURES,
119    output_mode: BuiltinOutputMode::Fixed,
120    completion_policy: BuiltinCompletionPolicy::Public,
121    errors: &GEOMETRY_INSPECT_ERRORS,
122};
123pub const GEOMETRY_LIST_REGIONS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
124    signatures: &GEOMETRY_LIST_REGIONS_SIGNATURES,
125    output_mode: BuiltinOutputMode::Fixed,
126    completion_policy: BuiltinCompletionPolicy::Public,
127    errors: &GEOMETRY_INSPECT_ERRORS,
128};
129pub const GEOMETRY_MESHES_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
130    signatures: &GEOMETRY_MESHES_SIGNATURES,
131    output_mode: BuiltinOutputMode::Fixed,
132    completion_policy: BuiltinCompletionPolicy::Public,
133    errors: &GEOMETRY_INSPECT_ERRORS,
134};
135
136#[runtime_builtin(
137    name = "geometry.load",
138    category = "geometry",
139    summary = "Load a geometry file into a structured geometry asset.",
140    keywords = "geometry,load,cad,mesh,stl,step,obj",
141    descriptor(crate::builtins::geometry::GEOMETRY_LOAD_DESCRIPTOR),
142    builtin_path = "crate::builtins::geometry"
143)]
144pub async fn geometry_load_builtin(path: String) -> BuiltinResult<Value> {
145    let bytes = read_file(GEOMETRY_LOAD_NAME, &GEOMETRY_LOAD_ERROR_IO, &path).await?;
146    operation_result_to_value(
147        GEOMETRY_LOAD_NAME,
148        &GEOMETRY_LOAD_ERROR_OPERATION,
149        &GEOMETRY_LOAD_ERROR_INTERNAL,
150        crate::geometry::geometry_load_op(&path, &bytes, OperationContext::new(None, None)),
151        Some(GEOMETRY_ASSET_CLASS),
152        Some(GEOMETRY_ASSET_JSON_PROPERTY),
153    )
154}
155
156#[runtime_builtin(
157    name = "geometry.inspect",
158    category = "geometry",
159    summary = "Inspect a geometry file without importing the full asset.",
160    keywords = "geometry,inspect,cad,mesh,stl,step,obj",
161    descriptor(crate::builtins::geometry::GEOMETRY_INSPECT_DESCRIPTOR),
162    builtin_path = "crate::builtins::geometry"
163)]
164pub async fn geometry_inspect_builtin(path: String) -> BuiltinResult<Value> {
165    let bytes = read_file(GEOMETRY_INSPECT_NAME, &GEOMETRY_INSPECT_ERROR_IO, &path).await?;
166    operation_result_to_value(
167        GEOMETRY_INSPECT_NAME,
168        &GEOMETRY_INSPECT_ERROR_OPERATION,
169        &GEOMETRY_INSPECT_ERROR_INTERNAL,
170        crate::geometry::geometry_inspect_op(&path, &bytes, OperationContext::new(None, None)),
171        Some(GEOMETRY_INSPECT_RESULT_CLASS),
172        None,
173    )
174}
175
176#[runtime_builtin(
177    name = "geometry.listRegions",
178    category = "geometry",
179    summary = "List regions imported into a geometry asset.",
180    keywords = "geometry,regions,cad,selectors,fea",
181    descriptor(crate::builtins::geometry::GEOMETRY_LIST_REGIONS_DESCRIPTOR),
182    builtin_path = "crate::builtins::geometry"
183)]
184pub async fn geometry_list_regions_builtin(asset: Value) -> BuiltinResult<Value> {
185    let asset = geometry_asset_from_value(&asset)?;
186    operation_result_to_value(
187        GEOMETRY_LIST_REGIONS_NAME,
188        &GEOMETRY_INSPECT_ERROR_OPERATION,
189        &GEOMETRY_INSPECT_ERROR_INTERNAL,
190        crate::geometry::geometry_list_regions_op(&asset, OperationContext::new(None, None)),
191        None,
192        None,
193    )
194}
195
196#[runtime_builtin(
197    name = "geometry.meshes",
198    category = "geometry",
199    summary = "Return renderable surface mesh topology for a geometry asset.",
200    keywords = "geometry,mesh,vertices,triangles,faces,patch,fea",
201    descriptor(crate::builtins::geometry::GEOMETRY_MESHES_DESCRIPTOR),
202    builtin_path = "crate::builtins::geometry"
203)]
204pub async fn geometry_meshes_builtin(asset: Value) -> BuiltinResult<Value> {
205    let asset = geometry_asset_from_value_with_builtin(&asset, GEOMETRY_MESHES_NAME)?;
206    geometry_meshes_value(&asset)
207}
208
209async fn read_file(
210    builtin: &'static str,
211    error: &'static BuiltinErrorDescriptor,
212    path: &str,
213) -> BuiltinResult<Vec<u8>> {
214    runmat_filesystem::read_async(path)
215        .await
216        .map_err(|err| builtin_error_with_source(builtin, error, err.to_string(), err))
217}
218
219fn geometry_asset_from_value(value: &Value) -> BuiltinResult<GeometryAsset> {
220    geometry_asset_from_value_with_builtin(value, GEOMETRY_LIST_REGIONS_NAME)
221}
222
223fn geometry_asset_from_value_with_builtin(
224    value: &Value,
225    builtin: &'static str,
226) -> BuiltinResult<GeometryAsset> {
227    let Value::Object(object) = value else {
228        return Err(builtin_error(
229            builtin,
230            &GEOMETRY_INSPECT_ERROR_INTERNAL,
231            format!("{builtin}: expected geometry.Asset"),
232        ));
233    };
234    if object.class_name != GEOMETRY_ASSET_CLASS {
235        return Err(builtin_error(
236            builtin,
237            &GEOMETRY_INSPECT_ERROR_INTERNAL,
238            format!(
239                "{builtin}: expected {GEOMETRY_ASSET_CLASS}, got {}",
240                object.class_name
241            ),
242        ));
243    }
244    object_json_property(
245        builtin,
246        object,
247        GEOMETRY_ASSET_JSON_PROPERTY,
248        &GEOMETRY_INSPECT_ERROR_INTERNAL,
249    )
250}
251
252fn geometry_meshes_value(asset: &GeometryAsset) -> BuiltinResult<Value> {
253    let values = asset
254        .surface_meshes
255        .iter()
256        .map(|surface| {
257            let mut mesh = StructValue::new();
258            mesh.insert("mesh_id", Value::String(surface.mesh_id.clone()));
259            mesh.insert("vertices", vertices_tensor(&surface.vertices)?);
260            mesh.insert("triangles", triangles_tensor(&surface.triangles)?);
261            mesh.insert("faces", triangles_tensor(&surface.triangles)?);
262            mesh.insert(
263                "region_mappings",
264                region_mappings_value(asset, &surface.mesh_id)?,
265            );
266            Ok(Value::Struct(mesh))
267        })
268        .collect::<BuiltinResult<Vec<_>>>()?;
269    crate::make_cell_with_shape(values, vec![1, asset.surface_meshes.len()])
270        .map_err(|err| builtin_error(GEOMETRY_MESHES_NAME, &GEOMETRY_INSPECT_ERROR_INTERNAL, err))
271}
272
273fn vertices_tensor(vertices: &[[f64; 3]]) -> BuiltinResult<Value> {
274    let mut data = Vec::with_capacity(vertices.len() * 3);
275    for col in 0..3 {
276        for vertex in vertices {
277            data.push(vertex[col]);
278        }
279    }
280    Tensor::new_2d(data, vertices.len(), 3)
281        .map(Value::Tensor)
282        .map_err(|err| {
283            builtin_error(
284                GEOMETRY_MESHES_NAME,
285                &GEOMETRY_INSPECT_ERROR_INTERNAL,
286                format!("geometry.meshes: failed to build vertices tensor: {err}"),
287            )
288        })
289}
290
291fn triangles_tensor(triangles: &[[u32; 3]]) -> BuiltinResult<Value> {
292    let mut data = Vec::with_capacity(triangles.len() * 3);
293    for col in 0..3 {
294        for triangle in triangles {
295            data.push(f64::from(triangle[col]) + 1.0);
296        }
297    }
298    Tensor::new_2d(data, triangles.len(), 3)
299        .map(Value::Tensor)
300        .map_err(|err| {
301            builtin_error(
302                GEOMETRY_MESHES_NAME,
303                &GEOMETRY_INSPECT_ERROR_INTERNAL,
304                format!("geometry.meshes: failed to build triangle tensor: {err}"),
305            )
306        })
307}
308
309fn region_mappings_value(asset: &GeometryAsset, mesh_id: &str) -> BuiltinResult<Value> {
310    let values = asset
311        .region_entity_mappings
312        .iter()
313        .filter(|mapping| mapping.mesh_id == mesh_id)
314        .map(|mapping| {
315            let mut value = StructValue::new();
316            value.insert("region_id", Value::String(mapping.region_id.clone()));
317            value.insert(
318                "entity_kind",
319                Value::String(format!("{:?}", mapping.entity_kind).to_ascii_lowercase()),
320            );
321            let mut ranges = Vec::with_capacity(mapping.ranges.len() * 2);
322            for col in 0..2 {
323                for range in &mapping.ranges {
324                    ranges.push(if col == 0 {
325                        range.start as f64 + 1.0
326                    } else {
327                        range.count as f64
328                    });
329                }
330            }
331            let range_tensor = Tensor::new_2d(ranges, mapping.ranges.len(), 2)
332                .map(Value::Tensor)
333                .map_err(|err| {
334                    builtin_error(
335                        GEOMETRY_MESHES_NAME,
336                        &GEOMETRY_INSPECT_ERROR_INTERNAL,
337                        format!("geometry.meshes: failed to build range tensor: {err}"),
338                    )
339                })?;
340            value.insert("ranges", range_tensor);
341            Ok(Value::Struct(value))
342        })
343        .collect::<BuiltinResult<Vec<_>>>()?;
344    let cols = values.len();
345    crate::make_cell_with_shape(values, vec![1, cols])
346        .map_err(|err| builtin_error(GEOMETRY_MESHES_NAME, &GEOMETRY_INSPECT_ERROR_INTERNAL, err))
347}
348
349fn object_json_property<T: DeserializeOwned>(
350    builtin: &'static str,
351    object: &ObjectInstance,
352    property: &'static str,
353    error: &'static BuiltinErrorDescriptor,
354) -> BuiltinResult<T> {
355    let Some(Value::String(json)) = object.properties.get(property) else {
356        return Err(build_runtime_error(format!(
357            "{} is missing required runtime payload property `{property}`",
358            object.class_name
359        ))
360        .with_builtin(builtin)
361        .with_identifier(error.identifier.unwrap_or("RunMat:geometry:Internal"))
362        .build());
363    };
364    serde_json::from_str(json)
365        .map_err(|err| builtin_error_with_source(builtin, error, err.to_string(), err))
366}
367
368fn operation_result_to_value<T: Serialize>(
369    builtin: &'static str,
370    operation_error_descriptor: &'static BuiltinErrorDescriptor,
371    internal_error_descriptor: &'static BuiltinErrorDescriptor,
372    result: Result<OperationEnvelope<T>, OperationErrorEnvelope>,
373    class_name: Option<&'static str>,
374    hidden_json_property: Option<&'static str>,
375) -> BuiltinResult<Value> {
376    let envelope =
377        result.map_err(|err| operation_error(builtin, operation_error_descriptor, err))?;
378    match class_name {
379        Some(class_name) => serializable_to_object(
380            builtin,
381            internal_error_descriptor,
382            class_name,
383            &envelope.data,
384            hidden_json_property,
385        ),
386        None => serializable_to_value(builtin, internal_error_descriptor, &envelope.data),
387    }
388}
389
390fn serializable_to_value<T: Serialize>(
391    builtin: &'static str,
392    error: &'static BuiltinErrorDescriptor,
393    value: &T,
394) -> BuiltinResult<Value> {
395    let json = serde_json::to_value(value)
396        .map_err(|err| builtin_error_with_source(builtin, error, err.to_string(), err))?;
397    value_from_json(&json)
398}
399
400fn serializable_to_object<T: Serialize>(
401    builtin: &'static str,
402    error: &'static BuiltinErrorDescriptor,
403    class_name: &'static str,
404    value: &T,
405    hidden_json_property: Option<&'static str>,
406) -> BuiltinResult<Value> {
407    ensure_geometry_classes_registered();
408    let json = serde_json::to_value(value)
409        .map_err(|err| builtin_error_with_source(builtin, error, err.to_string(), err))?;
410    let converted = value_from_json(&json)
411        .map_err(|err| builtin_error_with_source(builtin, error, err.message().to_string(), err))?;
412    let mut object = ObjectInstance::new(class_name.to_string());
413    if let Value::Struct(fields) = converted {
414        object.properties = fields.fields.into_iter().collect();
415    } else {
416        object.properties.insert("value".to_string(), converted);
417    }
418    if let Some(property) = hidden_json_property {
419        object
420            .properties
421            .insert(property.to_string(), Value::String(json.to_string()));
422    }
423    Ok(Value::Object(object))
424}
425
426fn ensure_geometry_classes_registered() {
427    static REGISTER: OnceLock<()> = OnceLock::new();
428    REGISTER.get_or_init(|| {
429        runmat_builtins::register_class(ClassDef {
430            name: GEOMETRY_ASSET_CLASS.to_string(),
431            parent: None,
432            properties: HashMap::new(),
433            methods: geometry_asset_methods(),
434        });
435        runmat_builtins::register_class(ClassDef {
436            name: GEOMETRY_INSPECT_RESULT_CLASS.to_string(),
437            parent: None,
438            properties: HashMap::new(),
439            methods: HashMap::<String, MethodDef>::new(),
440        });
441        triangulation::register_delaunaytri_class();
442    });
443}
444
445fn geometry_asset_methods() -> HashMap<String, MethodDef> {
446    [
447        ("listRegions", GEOMETRY_LIST_REGIONS_NAME),
448        ("meshes", GEOMETRY_MESHES_NAME),
449    ]
450    .into_iter()
451    .map(|(name, function_name)| {
452        (
453            name.to_string(),
454            MethodDef {
455                name: name.to_string(),
456                is_static: false,
457                is_abstract: false,
458                is_sealed: false,
459                access: Access::Public,
460                function_name: function_name.to_string(),
461                implicit_class_argument: None,
462            },
463        )
464    })
465    .collect()
466}
467
468fn operation_error(
469    builtin: &'static str,
470    error: &'static BuiltinErrorDescriptor,
471    source: OperationErrorEnvelope,
472) -> RuntimeError {
473    let message = format!(
474        "{}: {}: {}",
475        error.message, source.error_code, source.message
476    );
477    build_runtime_error(message)
478        .with_builtin(builtin)
479        .with_identifier(
480            error
481                .identifier
482                .unwrap_or("RunMat:geometry:OperationFailed"),
483        )
484        .build()
485}
486
487fn builtin_error(
488    builtin: &'static str,
489    error: &'static BuiltinErrorDescriptor,
490    message: impl Into<String>,
491) -> RuntimeError {
492    build_runtime_error(format!("{}: {}", error.message, message.into()))
493        .with_builtin(builtin)
494        .with_identifier(error.identifier.unwrap_or("RunMat:geometry:Internal"))
495        .build()
496}
497
498fn builtin_error_with_source<E>(
499    builtin: &'static str,
500    error: &'static BuiltinErrorDescriptor,
501    message: impl Into<String>,
502    source: E,
503) -> RuntimeError
504where
505    E: std::error::Error + Send + Sync + 'static,
506{
507    build_runtime_error(format!("{}: {}", error.message, message.into()))
508        .with_builtin(builtin)
509        .with_identifier(error.identifier.unwrap_or("RunMat:geometry:Internal"))
510        .with_source(source)
511        .build()
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use futures::executor::block_on;
518    use runmat_builtins::Value;
519
520    #[test]
521    fn geometry_inspect_builtin_returns_object_value() {
522        let tmp = tempfile::TempDir::new().unwrap();
523        let path = tmp.path().join("part.stl");
524        std::fs::write(
525            &path,
526            "solid demo\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid demo\n",
527        )
528        .unwrap();
529
530        let value = block_on(geometry_inspect_builtin(path.to_string_lossy().to_string()))
531            .expect("inspect builtin should return an object");
532
533        let Value::Object(result) = value else {
534            panic!("expected object value");
535        };
536        assert_eq!(result.class_name, GEOMETRY_INSPECT_RESULT_CLASS);
537        assert!(result.properties.contains_key("format"));
538        assert!(result.properties.contains_key("byte_count"));
539    }
540
541    #[test]
542    fn geometry_list_regions_builtin_returns_imported_regions() {
543        let tmp = tempfile::TempDir::new().unwrap();
544        let path = tmp.path().join("part.step");
545        std::fs::write(
546            &path,
547            "ISO-10303-21;\nHEADER;\nFILE_NAME('Assembly_A');\nENDSEC;\nDATA;\n#10=PRODUCT('Bracket_A','',(#1));\nENDSEC;\nEND-ISO-10303-21;\n",
548        )
549        .unwrap();
550
551        let asset = block_on(geometry_load_builtin(path.to_string_lossy().to_string()))
552            .expect("geometry should load");
553        let regions =
554            block_on(geometry_list_regions_builtin(asset)).expect("regions should be listed");
555
556        let Value::Struct(result) = regions else {
557            panic!("expected struct value");
558        };
559        assert!(result.fields.contains_key("regions"));
560    }
561
562    #[test]
563    fn geometry_meshes_builtin_returns_patch_ready_surface_topology() {
564        let tmp = tempfile::TempDir::new().unwrap();
565        let path = tmp.path().join("part.stl");
566        std::fs::write(
567            &path,
568            "solid demo\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid demo\n",
569        )
570        .unwrap();
571
572        let asset = block_on(geometry_load_builtin(path.to_string_lossy().to_string()))
573            .expect("geometry should load");
574        let meshes = block_on(geometry_meshes_builtin(asset)).expect("meshes should project");
575
576        let Value::Cell(cell) = meshes else {
577            panic!("expected cell array of mesh structs");
578        };
579        assert_eq!(cell.data.len(), 1);
580        let Value::Struct(mesh) = &cell.data[0] else {
581            panic!("expected mesh struct");
582        };
583        let Some(Value::Tensor(vertices)) = mesh.fields.get("vertices") else {
584            panic!("expected vertices tensor");
585        };
586        assert_eq!(vertices.shape, vec![3, 3]);
587        let Some(Value::Tensor(faces)) = mesh.fields.get("faces") else {
588            panic!("expected faces tensor");
589        };
590        assert_eq!(faces.shape, vec![1, 3]);
591        assert_eq!(faces.data, vec![1.0, 2.0, 3.0]);
592    }
593}