Objects

Objects are the primary means to store persistent, version-controlled state. Whether you are building a measurement tool or a volumetric editor, data that represents a part of a project (from a single point to a large mesh) should be defined as an Object.

To learn more about versioned state concepts, see Versioned State.

Defining an Object

To create an object type, define a standard Rust struct and apply the necessary derives. This example shows how to combine coordinates with user-defined metadata:

use fdk::libs::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, FseObject, PersistentObject)]
pub struct SurveyPoint {
    pub name: String,
    pub location: DVec3,
    pub classification: u8,
}

Once defined, your custom objects automatically gain support for local undo/redo and global versioning.

Hierarchy and Representations

Objects are organized into a tree structure using the ParentOf relationship. This hierarchy allows for logical grouping and inherited properties (like visibility and coordinate transforms).

In addition to hierarchical links, objects use the HasA relationship to attach core components:

  • Node: Every object in the scene tree starts with a Node, which provides its identity and name.
  • Transform: Attached via HasA, defining the object’s position, rotation, and scale relative to its parent.
  • Representation: Defines the visual geometry (e.g., a Mesh, Point Cloud, or Polyline) associated with the node.

To build a hierarchy, you use the FseFacade within a statement to create relationships between objects:

// Link a child node to a parent
fse.add_relationship(parent_id, child_id, RelationType::ParentOf)?;

// Attach a representation to a node
fse.add_relationship(node_id, mesh_id, RelationType::HasA)?;

This relationship-based architecture ensures that the object model remains flexible. You can query the hierarchy, traverse parents, or swap out representations without destroying the underlying object identity.

Schema Migrations

The SDK provides tooling to implement legacy support, allowing you to migrate older versions of your custom objects to the latest schema as the project evolves. This ensures that data remains accessible across different versions of a plugin and the SDK.

  • The ObjectLegacy Trait: By implementing this trait, you can define how older serialized versions of your object should be mapped to the current Rust struct. This is particularly useful for handling semver-breaking changes or renaming fields.
  • Built-in Samples: You can define legacy samples within your code to act as regression tests. These samples ensure that your migration logic correctly handles historical data formats.
  • Self-Testing: We recommend adding an implementation_self_test to your plugin. This test uses the SDK’s registry tools to verify that all your object types are correctly registered and that their schemas are consistent, catching common serialization errors during development.

Mutating Object State

To change the state of an object, you must execute a Statement. Specifically, implementing a Mutation is the only way to modify authoritative project data. This constraint ensures that every change is captured by the versioning system, enabling automatic undo/redo, multi-user synchronization, and a reliable audit trail. To mutate an object, you define a Mutation statement that interacts with the FseFacade. This facade provides the interface for retrieving, modifying, and committing objects.

By wrapping mutations in statements, you guarantee that your plugin’s business logic remains decoupled from the low-level storage and networking concerns, while gaining all the collaborative features of the SDK for free.

Bevy Autosync: Deriving the ECS from Versioned State

While fse_state is the authoritative source of truth, Bevy Autosync synchronizes that state into the engine’s Entity Component System (ECS). When adding a new object, you can implement BevyAutoSync to define how that object’s state should be rendered out to the ECS.

When an object is created, modified, or restored during an undo/redo operation, the SDK triggers an autosync. This centralized logic provides a predictable mechanism for deriving the rendered environment from the application state.

Best Practices

To make the most of the versioned object system, we recommend a few design patterns for your plugins:

  • Composition over Inheritance: Instead of creating one large object, break data into many smaller, independent objects (e.g., individual buildings, survey points, or terrain patches). This reduces conflicts because users are less likely to edit the same object simultaneously.
  • Encourage Frequent Check-ins: For collaborative cloud projects, large, multi-day checkouts increase the likelihood of merge friction. Design your tools to encourage small, incremental updates.