Skip to main content

runmat_runtime/builtins/geometry/
mod.rs

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