IO and File Formats

PlantGeom works with three complementary representations:

  • MTG on disk and in memory: the graph structure written in .mtg files that you can import and manipulate in Julia using MultiScaleTreeGraph.jl.
  • OPF on disk: one plant object with topology + geometry stored in the OPF format (.opf). This is basically an .mtg file with geometry data.
  • GWA on disk: a 3D object with geometry but not topology stored in the GWA format (.gwa).
  • OPS on disk: a scene file that places multiple plant objects (i.e. OPF/GWA objects) in space. It is stored in the OPS format (.ops).

Mental Model

  • Use read_opf when you want one plant object with its geometry.
  • Use read_gwa when you want one standalone 3D object without topology (e.g. a solar panel, or a single leaf).
  • Use read_ops when you want a whole scene (many OPF/GWA objects + scene transforms).
  • Use read_mtg when you have topology/attributes only (no explicit mesh geometry).

In all cases, PlantGeom builds a MultiScaleTreeGraph in memory.

What Is Inside an OPF?

An OPF (.opf) is an XML file describing one object (typically one plant):

  • mesh definitions (meshBDD)
  • materials (materialBDD)
  • mesh/material mapping (shapeBDD)
  • attribute dictionary (attributeBDD)
  • MTG topology and per-node values (topology), which is the graph structure with node attributes (i.e. the mtg), including geometry (transformation matrix and reference-mesh shapeIndex).

Minimal structure (illustrative):

<opf version="2.0" editable="true">
  <meshBDD>...</meshBDD>
  <materialBDD>...</materialBDD>
  <shapeBDD>...</shapeBDD>
  <attributeBDD>...</attributeBDD>
  <topology>...</topology>
</opf>

What Is Inside an OPS?

An OPS (.ops) is a scene file, not a mesh file. It stores:

  • scene dimensions (T ... flat line)
  • a list of object rows (path to .opf/.gwa + transform values)
  • optional functional-group sections (#[Archimed] ...)

Each object row references a plant file and gives scene placement:

  • position (x y z)
  • scale
  • inclination azimuth + angle
  • rotation

So: OPS = where objects are in the scene, OPF = what each object is.

What Is an MTG File?

An MTG file (.mtg) stores graph topology and attributes in text form. It is very convenient for reconstruction workflows because it is lightweight and human-readable, but it does not carry explicit mesh geometry.

These files typically come from field measurements. You can still perform semi-automatic reconstruction if it has standard attributes with set_geometry_from_attributes! or reconstruct_geometry_from_attributes!.

Reading Reference Meshes from .ply / .obj

PlantGeom does not provide dedicated read_ply/read_obj functions, because they are defined in external packages like FileIO and MeshIO.

The usual approach is:

  1. load external mesh files with FileIO + MeshIO
  2. convert to a GeometryBasics mesh (if needed)
  3. wrap into RefMesh
using PlantGeom
using GeometryBasics
using Colors
using FileIO
using MeshIO

# Load a polygon mesh from disk (.ply, .obj, ...)
raw_mesh = load("leaf.ply")

# Convert to GeometryBasics mesh when required by the loader output:
mesh_gb = GeometryBasics.mesh(raw_mesh)

# Wrap it as a PlantGeom reference mesh:
leaf_ref = RefMesh("LeafFromPLY", mesh_gb, RGB(0.18, 0.58, 0.28))

Same idea for OBJ:

stem_ref = RefMesh("StemFromOBJ", GeometryBasics.mesh(load("stem.obj")), RGB(0.55, 0.42, 0.30))

You can then use these RefMesh objects directly in Geometry(...), set_geometry_from_attributes!, or reconstruction dictionaries.

For the procedural counterpart (direct node geometries with ExtrudedTubeGeometry and extrusion helpers), see: Procedural / Extrusion Geometry.

Reading Examples

using PlantGeom
using MultiScaleTreeGraph

files_dir = joinpath(dirname(dirname(pathof(PlantGeom))), "test", "files")
opf_file = joinpath(files_dir, "simple_plant.opf")
ops_file = joinpath(files_dir, "scene.ops")
mtg_file = joinpath(files_dir, "reconstruction_standard.mtg")

opf = read_opf(opf_file)
scene_dimensions, object_table = read_ops_file(ops_file)
scene = read_ops(ops_file)
mtg_topology = read_mtg(mtg_file)

(
    opf_nodes_with_geometry=length(descendants(opf, :geometry; ignore_nothing=true, self=true)),
    scene_objects=length(object_table),
    scene_children=length(children(scene)),
    mtg_nodes=length(descendants(mtg_topology; self=true)),
)
(opf_nodes_with_geometry = 4, scene_objects = 6, scene_children = 6, mtg_nodes = 9)

Writing Examples

tmp_opf = tempname() * ".opf"
tmp_ops = tempname() * ".ops"
tmp_ops_rows = tempname() * ".ops"

write_opf(tmp_opf, opf)
write_ops(tmp_ops, scene) # default: writes OPS + emitted OPF/GWA object files
write_ops_file(tmp_ops_rows, scene_dimensions, object_table) # rows only

opf_roundtrip = read_opf(tmp_opf)
ops_roundtrip = read_ops_file(tmp_ops)
ops_rows_roundtrip = read_ops_file(tmp_ops_rows)

summary = (
    opf_written=isfile(tmp_opf),
    ops_written=isfile(tmp_ops),
    ops_rows_written=isfile(tmp_ops_rows),
    opf_roundtrip_nodes=length(descendants(opf_roundtrip, :geometry; ignore_nothing=true, self=true)),
    ops_roundtrip_rows=length(ops_roundtrip.object_table),
    ops_rows_roundtrip_rows=length(ops_rows_roundtrip.object_table),
)

rm(tmp_opf; force=true)
rm(tmp_ops; force=true)
rm(tmp_ops_rows; force=true)

summary
(opf_written = true, ops_written = true, ops_rows_written = true, opf_roundtrip_nodes = 4, ops_roundtrip_rows = 6, ops_rows_roundtrip_rows = 6)

OPF Reference Mesh IDs

read_opf stores reference meshes on the MTG root as opf[:ref_meshes], a Dict{Int,RefMesh} keyed by OPF shape IDs (the same IDs used by shapeIndex, typically 0-based).

ref_meshes_by_id = opf[:ref_meshes]
shape_ids = sort(collect(keys(ref_meshes_by_id)))
(
    n_ref_meshes=length(ref_meshes_by_id),
    first_shape_id=first(shape_ids),
    id_type=eltype(shape_ids),
)
(n_ref_meshes = 2, first_shape_id = 0, id_type = Int64)

If you only need a list for plotting, use get_ref_meshes(opf) (or collect(values(opf[:ref_meshes]))).

Which Reader Should I Use?

GoalRecommended function
Load one plant object with explicit geometryread_opf(file)
Parse an OPS scene table without loading all geometryread_ops_file(file)
Load the full OPS scene as MTG children + transformsread_ops(file)
Load topology/attributes text for reconstructionread_mtg(file)

Practical Notes

  • read_ops resolves object file paths relative to the OPS file directory.
  • write_ops(file, scene) writes the OPS scene table and, by default, emits one OPF/GWA object file per scene child.
  • write_ops_file(file, scene_dimensions, object_table) writes only the OPS scene table rows (no object files emitted).
  • You can override OPF attribute typing explicitly with read_opf(file; attribute_types=Dict("Length" => Float64)) (same keyword is available in read_ops and forwarded to embedded OPFs).
  • For reconstruction workflows from .mtg, see: AMAP-Style Quickstart.