802 lines
28 KiB
Rust
802 lines
28 KiB
Rust
//! Schema-aware scene document seam over the current DynamicScene RON format.
|
|
//!
|
|
//! This module intentionally stays Bevy-free. The editor can keep loading and
|
|
//! saving DynamicScene RON while tools start depending on stable actor/component
|
|
//! document concepts instead of runtime ECS entity IDs.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use ron::ser::PrettyConfig;
|
|
use serde::{Deserialize, Serialize};
|
|
use shared::{ActorId, PrefabInstance, PrefabRef, SceneComposition};
|
|
|
|
use crate::{
|
|
migrate_scene_text, read_schema_version, stamp_schema_version, strip_schema_version,
|
|
validate_scene_authoring_only,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SceneDocument {
|
|
pub schema_version: u32,
|
|
pub entities: Vec<SceneEntity>,
|
|
pub composition: Option<SceneComposition>,
|
|
pub resource_types: Vec<String>,
|
|
pub prefab_instances: Vec<PrefabInstance>,
|
|
pub prefab_refs: Vec<PrefabRef>,
|
|
normalized_ron: String,
|
|
resources: BTreeMap<String, String>,
|
|
source_keys: BTreeMap<SceneEntityId, u64>,
|
|
}
|
|
|
|
impl SceneDocument {
|
|
pub fn from_ron_text(text: &str) -> Result<Self, String> {
|
|
let normalized = migrate_scene_text(text)?;
|
|
validate_scene_authoring_only(&normalized)?;
|
|
validate_hierarchy_references(&normalized)?;
|
|
let body = strip_schema_version(&normalized)?;
|
|
let parsed: DynamicSceneBody = ron::from_str(&body)
|
|
.map_err(|err| format!("could not parse scene document body: {err}"))?;
|
|
let (resources, mut raw_entities) = extract_raw_scene_values(&normalized)?;
|
|
|
|
let composition = extract_scene_composition(&parsed.resources)?;
|
|
let resource_types = parsed.resources.keys().cloned().collect();
|
|
let prefab_instances = extract_prefab_instances(&parsed.entities)?;
|
|
let prefab_refs = extract_prefab_refs(&parsed.entities)?;
|
|
let mut entities = Vec::with_capacity(parsed.entities.len());
|
|
let mut source_keys = BTreeMap::new();
|
|
for (index, (source_key, _entity)) in parsed.entities.into_iter().enumerate() {
|
|
let components = raw_entities
|
|
.remove(&source_key)
|
|
.ok_or_else(|| format!("scene entity `{source_key}` has no lossless syntax node"))?
|
|
.into_iter()
|
|
.map(|(type_name, ron)| SceneComponentBlob::from_ron(type_name, ron))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let actor_id = extract_actor_id(&components);
|
|
let name = extract_name(&components);
|
|
let document_id = actor_id
|
|
.as_ref()
|
|
.map(|id| SceneEntityId::ActorId(id.clone()))
|
|
.unwrap_or_else(|| SceneEntityId::Anonymous(format!("anonymous:{index}")));
|
|
|
|
entities.push(SceneEntity {
|
|
document_id: document_id.clone(),
|
|
actor_id,
|
|
name,
|
|
parent_actor_id: None,
|
|
components,
|
|
});
|
|
source_keys.insert(document_id, source_key);
|
|
}
|
|
|
|
let key_to_actor: BTreeMap<u64, String> = entities
|
|
.iter()
|
|
.filter_map(|entity| {
|
|
let source_key = source_keys.get(&entity.document_id).copied()?;
|
|
Some((source_key, entity.actor_id.clone()?))
|
|
})
|
|
.collect();
|
|
for entity in &mut entities {
|
|
entity.parent_actor_id = entity
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == CHILD_OF_COMPONENT)
|
|
.and_then(|component| parse_child_of_source_key(&component.ron))
|
|
.and_then(|source_key| key_to_actor.get(&source_key).cloned());
|
|
}
|
|
|
|
Ok(Self {
|
|
schema_version: read_schema_version(&normalized),
|
|
entities,
|
|
composition,
|
|
resource_types,
|
|
prefab_instances,
|
|
prefab_refs,
|
|
normalized_ron: normalized,
|
|
resources,
|
|
source_keys,
|
|
})
|
|
}
|
|
|
|
pub fn to_ron_text(&self) -> Result<String, String> {
|
|
let mut resources = self.resources.clone();
|
|
match &self.composition {
|
|
Some(composition) => {
|
|
let composition_ron = ron::to_string(composition)
|
|
.map_err(|error| format!("could not encode scene composition: {error}"))?;
|
|
resources.insert(SCENE_COMPOSITION_RESOURCE.to_string(), composition_ron);
|
|
}
|
|
None => {
|
|
resources.remove(SCENE_COMPOSITION_RESOURCE);
|
|
}
|
|
}
|
|
|
|
let mut used_keys: std::collections::HashSet<u64> =
|
|
self.source_keys.values().copied().collect();
|
|
let mut next_key = used_keys
|
|
.iter()
|
|
.copied()
|
|
.max()
|
|
.unwrap_or(0)
|
|
.saturating_add(1);
|
|
let mut entities = BTreeMap::new();
|
|
for entity in &self.entities {
|
|
let source_key = self
|
|
.source_keys
|
|
.get(&entity.document_id)
|
|
.copied()
|
|
.unwrap_or_else(|| {
|
|
while used_keys.contains(&next_key) {
|
|
next_key = next_key.saturating_add(1);
|
|
}
|
|
let assigned = next_key;
|
|
used_keys.insert(assigned);
|
|
next_key = next_key.saturating_add(1);
|
|
assigned
|
|
});
|
|
let components = entity
|
|
.components
|
|
.iter()
|
|
.map(|component| {
|
|
ron::from_str::<ron::Value>(&component.ron).map_err(|error| {
|
|
format!("invalid component RON `{}`: {error}", component.type_name)
|
|
})?;
|
|
Ok((component.type_name.clone(), component.ron.clone()))
|
|
})
|
|
.collect::<Result<BTreeMap<_, _>, String>>()?;
|
|
entities.insert(source_key, components);
|
|
}
|
|
encode_lossless_scene_body(&resources, &entities)
|
|
}
|
|
|
|
pub fn normalized_ron(&self) -> &str {
|
|
&self.normalized_ron
|
|
}
|
|
|
|
pub fn find_entity(&self, id: &SceneEntityId) -> Option<&SceneEntity> {
|
|
self.entities
|
|
.iter()
|
|
.find(|entity| &entity.document_id == id)
|
|
}
|
|
|
|
pub fn apply_patch(&mut self, patch: ScenePatch) -> Result<(), String> {
|
|
match patch {
|
|
ScenePatch::SetComponent { target, component } => {
|
|
let entity = self
|
|
.find_entity_mut(&target)
|
|
.ok_or_else(|| format!("scene entity `{target}` not found"))?;
|
|
if let Some(existing) = entity
|
|
.components
|
|
.iter_mut()
|
|
.find(|existing| existing.type_name == component.type_name)
|
|
{
|
|
*existing = component;
|
|
} else {
|
|
entity.components.push(component);
|
|
}
|
|
}
|
|
ScenePatch::RemoveComponent {
|
|
target,
|
|
component_type,
|
|
} => {
|
|
let entity = self
|
|
.find_entity_mut(&target)
|
|
.ok_or_else(|| format!("scene entity `{target}` not found"))?;
|
|
entity
|
|
.components
|
|
.retain(|component| component.type_name != component_type);
|
|
}
|
|
ScenePatch::AddEntity { entity } => {
|
|
if self.find_entity(&entity.document_id).is_some() {
|
|
return Err(format!(
|
|
"scene entity `{}` already exists",
|
|
entity.document_id
|
|
));
|
|
}
|
|
let next_key = self
|
|
.source_keys
|
|
.values()
|
|
.copied()
|
|
.max()
|
|
.unwrap_or(0)
|
|
.saturating_add(1);
|
|
self.source_keys
|
|
.insert(entity.document_id.clone(), next_key);
|
|
self.entities.push(entity);
|
|
}
|
|
ScenePatch::DeleteEntity { target } => {
|
|
let Some(target_actor_id) = self
|
|
.find_entity(&target)
|
|
.and_then(|entity| entity.actor_id.clone())
|
|
else {
|
|
return Err(format!("scene entity `{target}` not found"));
|
|
};
|
|
let mut removed_actor_ids = std::collections::HashSet::from([target_actor_id]);
|
|
let mut changed = true;
|
|
while changed {
|
|
changed = false;
|
|
for entity in &self.entities {
|
|
if entity
|
|
.parent_actor_id
|
|
.as_ref()
|
|
.is_some_and(|parent| removed_actor_ids.contains(parent))
|
|
{
|
|
if let Some(actor_id) = entity.actor_id.clone() {
|
|
changed |= removed_actor_ids.insert(actor_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let removed_ids: Vec<_> = self
|
|
.entities
|
|
.iter()
|
|
.filter(|entity| {
|
|
entity
|
|
.actor_id
|
|
.as_ref()
|
|
.is_some_and(|actor_id| removed_actor_ids.contains(actor_id))
|
|
})
|
|
.map(|entity| entity.document_id.clone())
|
|
.collect();
|
|
self.entities
|
|
.retain(|entity| !removed_ids.contains(&entity.document_id));
|
|
for removed in removed_ids {
|
|
self.source_keys.remove(&removed);
|
|
}
|
|
}
|
|
ScenePatch::Reparent {
|
|
target,
|
|
parent_actor_id,
|
|
} => {
|
|
let parent_source_key = parent_actor_id
|
|
.as_ref()
|
|
.map(|parent| {
|
|
self.source_keys
|
|
.get(&SceneEntityId::ActorId(parent.clone()))
|
|
.copied()
|
|
.ok_or_else(|| format!("parent actor `{parent}` not found"))
|
|
})
|
|
.transpose()?;
|
|
let entity = self
|
|
.find_entity_mut(&target)
|
|
.ok_or_else(|| format!("scene entity `{target}` not found"))?;
|
|
match parent_source_key {
|
|
Some(parent_source_key) => {
|
|
let component = SceneComponentBlob::from_ron(
|
|
CHILD_OF_COMPONENT,
|
|
format!("({parent_source_key})"),
|
|
)?;
|
|
if let Some(existing) = entity
|
|
.components
|
|
.iter_mut()
|
|
.find(|component| component.type_name == CHILD_OF_COMPONENT)
|
|
{
|
|
*existing = component;
|
|
} else {
|
|
entity.components.push(component);
|
|
}
|
|
}
|
|
None => entity
|
|
.components
|
|
.retain(|component| component.type_name != CHILD_OF_COMPONENT),
|
|
}
|
|
entity.parent_actor_id = parent_actor_id;
|
|
}
|
|
ScenePatch::Rename { target, name } => {
|
|
let entity = self
|
|
.find_entity_mut(&target)
|
|
.ok_or_else(|| format!("scene entity `{target}` not found"))?;
|
|
let component = SceneComponentBlob::from_serializable(NAME_COMPONENT, &name)?;
|
|
if let Some(existing) = entity
|
|
.components
|
|
.iter_mut()
|
|
.find(|existing| existing.type_name == NAME_COMPONENT)
|
|
{
|
|
*existing = component;
|
|
} else {
|
|
entity.components.push(component);
|
|
}
|
|
entity.name = Some(name);
|
|
}
|
|
ScenePatch::SetTransform { target, transform } => {
|
|
let entity = self
|
|
.find_entity_mut(&target)
|
|
.ok_or_else(|| format!("scene entity `{target}` not found"))?;
|
|
let component =
|
|
SceneComponentBlob::from_serializable(TRANSFORM_COMPONENT, &transform)?;
|
|
if let Some(existing) = entity
|
|
.components
|
|
.iter_mut()
|
|
.find(|existing| existing.type_name == TRANSFORM_COMPONENT)
|
|
{
|
|
*existing = component;
|
|
} else {
|
|
entity.components.push(component);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn find_entity_mut(&mut self, id: &SceneEntityId) -> Option<&mut SceneEntity> {
|
|
self.entities
|
|
.iter_mut()
|
|
.find(|entity| &entity.document_id == id)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub enum SceneEntityId {
|
|
ActorId(String),
|
|
Anonymous(String),
|
|
}
|
|
|
|
impl std::fmt::Display for SceneEntityId {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::ActorId(id) => write!(f, "actor:{id}"),
|
|
Self::Anonymous(id) => write!(f, "{id}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SceneEntity {
|
|
pub document_id: SceneEntityId,
|
|
pub actor_id: Option<String>,
|
|
pub name: Option<String>,
|
|
pub parent_actor_id: Option<String>,
|
|
pub components: Vec<SceneComponentBlob>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SceneComponentBlob {
|
|
pub type_name: String,
|
|
pub ron: String,
|
|
}
|
|
|
|
impl SceneComponentBlob {
|
|
pub fn from_serializable(
|
|
type_name: impl Into<String>,
|
|
value: &impl Serialize,
|
|
) -> Result<Self, String> {
|
|
let pretty = PrettyConfig::new();
|
|
let ron = ron::ser::to_string_pretty(value, pretty)
|
|
.map_err(|err| format!("could not serialize component blob: {err}"))?;
|
|
Ok(Self {
|
|
type_name: type_name.into(),
|
|
ron,
|
|
})
|
|
}
|
|
|
|
pub fn from_ron(type_name: impl Into<String>, ron: impl Into<String>) -> Result<Self, String> {
|
|
let type_name = type_name.into();
|
|
let ron = ron.into();
|
|
ron::from_str::<ron::Value>(&ron)
|
|
.map_err(|error| format!("invalid component RON `{type_name}`: {error}"))?;
|
|
Ok(Self { type_name, ron })
|
|
}
|
|
|
|
pub fn to_value(&self) -> Result<ron::Value, String> {
|
|
ron::from_str(&self.ron)
|
|
.map_err(|error| format!("invalid component RON `{}`: {error}", self.type_name))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub enum ScenePatch {
|
|
SetComponent {
|
|
target: SceneEntityId,
|
|
component: SceneComponentBlob,
|
|
},
|
|
RemoveComponent {
|
|
target: SceneEntityId,
|
|
component_type: String,
|
|
},
|
|
AddEntity {
|
|
entity: SceneEntity,
|
|
},
|
|
DeleteEntity {
|
|
target: SceneEntityId,
|
|
},
|
|
Reparent {
|
|
target: SceneEntityId,
|
|
parent_actor_id: Option<String>,
|
|
},
|
|
Rename {
|
|
target: SceneEntityId,
|
|
name: String,
|
|
},
|
|
SetTransform {
|
|
target: SceneEntityId,
|
|
transform: SceneTransform,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct SceneTransform {
|
|
pub translation: [f32; 3],
|
|
pub rotation: [f32; 4],
|
|
pub scale: [f32; 3],
|
|
}
|
|
|
|
impl Default for SceneTransform {
|
|
fn default() -> Self {
|
|
Self {
|
|
translation: [0.0, 0.0, 0.0],
|
|
rotation: [0.0, 0.0, 0.0, 1.0],
|
|
scale: [1.0, 1.0, 1.0],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct DynamicSceneBody {
|
|
#[serde(default)]
|
|
resources: BTreeMap<String, ron::Value>,
|
|
#[serde(default)]
|
|
entities: BTreeMap<u64, DynamicSceneEntity>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct DynamicSceneEntity {
|
|
#[serde(default)]
|
|
components: BTreeMap<String, ron::Value>,
|
|
}
|
|
|
|
const ACTOR_ID_COMPONENT: &str = "shared::components::ActorId";
|
|
const CHILD_OF_COMPONENT: &str = "bevy_ecs::hierarchy::ChildOf";
|
|
const NAME_COMPONENT: &str = "bevy_ecs::name::Name";
|
|
const SCENE_COMPOSITION_RESOURCE: &str = "shared::components::SceneComposition";
|
|
const PREFAB_INSTANCE_COMPONENT: &str = "shared::components::PrefabInstance";
|
|
const PREFAB_REF_COMPONENT: &str = "shared::components::PrefabRef";
|
|
pub const TRANSFORM_COMPONENT: &str = "bevy_transform::components::transform::Transform";
|
|
|
|
type RawSceneEntities = BTreeMap<u64, BTreeMap<String, String>>;
|
|
|
|
fn extract_raw_scene_values(
|
|
text: &str,
|
|
) -> Result<(BTreeMap<String, String>, RawSceneEntities), String> {
|
|
let file = ron_edit::File::try_from(text)
|
|
.map_err(|error| format!("could not parse lossless scene syntax: {error}"))?;
|
|
let root = as_struct(&file.value.content, "scene root")?;
|
|
let resources = as_map(struct_field(root, "resources")?, "scene resources")?;
|
|
let entities = as_map(struct_field(root, "entities")?, "scene entities")?;
|
|
|
|
let resources = resources
|
|
.0
|
|
.values
|
|
.iter()
|
|
.map(|entry| {
|
|
Ok((
|
|
parse_string_key(&entry.content.key)?,
|
|
entry.content.value.content.to_string(),
|
|
))
|
|
})
|
|
.collect::<Result<BTreeMap<_, _>, String>>()?;
|
|
let entities = entities
|
|
.0
|
|
.values
|
|
.iter()
|
|
.map(|entry| {
|
|
let source_key = entry
|
|
.content
|
|
.key
|
|
.to_string()
|
|
.parse::<u64>()
|
|
.map_err(|error| format!("invalid scene entity key: {error}"))?;
|
|
let entity = as_struct(&entry.content.value.content, "scene entity")?;
|
|
let components = as_map(struct_field(entity, "components")?, "entity components")?;
|
|
let components = components
|
|
.0
|
|
.values
|
|
.iter()
|
|
.map(|component| {
|
|
Ok((
|
|
parse_string_key(&component.content.key)?,
|
|
component.content.value.content.to_string(),
|
|
))
|
|
})
|
|
.collect::<Result<BTreeMap<_, _>, String>>()?;
|
|
Ok((source_key, components))
|
|
})
|
|
.collect::<Result<BTreeMap<_, _>, String>>()?;
|
|
Ok((resources, entities))
|
|
}
|
|
|
|
fn as_struct<'a, 's>(
|
|
value: &'a ron_edit::Value<'s>,
|
|
label: &str,
|
|
) -> Result<&'a ron_edit::Struct<'s>, String> {
|
|
match value {
|
|
ron_edit::Value::Struct(value) => Ok(value),
|
|
_ => Err(format!("{label} must be a RON struct")),
|
|
}
|
|
}
|
|
|
|
fn as_map<'a, 's>(
|
|
value: &'a ron_edit::Value<'s>,
|
|
label: &str,
|
|
) -> Result<&'a ron_edit::Map<'s>, String> {
|
|
match value {
|
|
ron_edit::Value::Map(value) => Ok(value),
|
|
_ => Err(format!("{label} must be a RON map")),
|
|
}
|
|
}
|
|
|
|
fn struct_field<'a, 's>(
|
|
value: &'a ron_edit::Struct<'s>,
|
|
name: &str,
|
|
) -> Result<&'a ron_edit::Value<'s>, String> {
|
|
value
|
|
.fields
|
|
.values
|
|
.iter()
|
|
.find(|field| field.content.key == name)
|
|
.map(|field| &field.content.value.content)
|
|
.ok_or_else(|| format!("RON struct is missing `{name}`"))
|
|
}
|
|
|
|
fn parse_string_key(value: &ron_edit::Value<'_>) -> Result<String, String> {
|
|
ron::from_str(&value.to_string()).map_err(|error| format!("invalid string map key: {error}"))
|
|
}
|
|
|
|
fn parse_child_of_source_key(text: &str) -> Option<u64> {
|
|
let value = ron::from_str::<ron::Value>(text).ok()?;
|
|
parse_child_of_source_key_value(&value)
|
|
}
|
|
|
|
fn parse_child_of_source_key_value(value: &ron::Value) -> Option<u64> {
|
|
let ron::Value::Seq(values) = value else {
|
|
return None;
|
|
};
|
|
let ron::Value::Number(ron::value::Number::Integer(value)) = values.first()? else {
|
|
return None;
|
|
};
|
|
u64::try_from(*value).ok()
|
|
}
|
|
|
|
pub(crate) fn validate_hierarchy_references(text: &str) -> Result<(), String> {
|
|
let body = strip_schema_version(text)?;
|
|
let parsed: DynamicSceneBody = ron::from_str(&body)
|
|
.map_err(|error| format!("could not parse scene hierarchy: {error}"))?;
|
|
let mut parents = BTreeMap::new();
|
|
|
|
for (entity_key, entity) in &parsed.entities {
|
|
let Some(value) = entity.components.get(CHILD_OF_COMPONENT) else {
|
|
continue;
|
|
};
|
|
let parent_key = parse_child_of_source_key_value(value).ok_or_else(|| {
|
|
format!("scene entity `{entity_key}` has a malformed `{CHILD_OF_COMPONENT}` value")
|
|
})?;
|
|
if !parsed.entities.contains_key(&parent_key) {
|
|
return Err(format!(
|
|
"scene entity `{entity_key}` references missing hierarchy parent `{parent_key}`"
|
|
));
|
|
}
|
|
parents.insert(*entity_key, parent_key);
|
|
}
|
|
|
|
for start in parents.keys().copied() {
|
|
let mut visited = std::collections::BTreeSet::new();
|
|
let mut current = start;
|
|
while let Some(parent) = parents.get(¤t).copied() {
|
|
if !visited.insert(current) {
|
|
return Err(format!(
|
|
"scene hierarchy contains a cycle involving entity `{current}`"
|
|
));
|
|
}
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_lossless_scene_body(
|
|
resources: &BTreeMap<String, String>,
|
|
entities: &BTreeMap<u64, BTreeMap<String, String>>,
|
|
) -> Result<String, String> {
|
|
let mut body = String::from("(resources: {\n");
|
|
for (type_name, value) in resources {
|
|
ron::from_str::<ron::Value>(value)
|
|
.map_err(|error| format!("invalid resource RON `{type_name}`: {error}"))?;
|
|
body.push_str(" ");
|
|
body.push_str(
|
|
&ron::to_string(type_name)
|
|
.map_err(|error| format!("could not encode resource type: {error}"))?,
|
|
);
|
|
body.push_str(": ");
|
|
body.push_str(value);
|
|
body.push_str(",\n");
|
|
}
|
|
body.push_str(" },\n entities: {\n");
|
|
for (source_key, components) in entities {
|
|
body.push_str(&format!(" {source_key}: (components: {{\n"));
|
|
for (type_name, value) in components {
|
|
body.push_str(" ");
|
|
body.push_str(
|
|
&ron::to_string(type_name)
|
|
.map_err(|error| format!("could not encode component type: {error}"))?,
|
|
);
|
|
body.push_str(": ");
|
|
body.push_str(value);
|
|
body.push_str(",\n");
|
|
}
|
|
body.push_str(" }),\n");
|
|
}
|
|
body.push_str(" },\n)");
|
|
stamp_schema_version(&body)
|
|
}
|
|
|
|
fn extract_scene_composition(
|
|
resources: &BTreeMap<String, ron::Value>,
|
|
) -> Result<Option<SceneComposition>, String> {
|
|
let mut matches = resources
|
|
.iter()
|
|
.filter(|(type_name, _)| type_name.as_str() == SCENE_COMPOSITION_RESOURCE);
|
|
let Some((_type_name, value)) = matches.next() else {
|
|
return Ok(None);
|
|
};
|
|
if matches.next().is_some() {
|
|
return Err("scene contains multiple composition resources".to_string());
|
|
}
|
|
value
|
|
.clone()
|
|
.into_rust::<SceneComposition>()
|
|
.map(Some)
|
|
.map_err(|error| format!("could not parse scene composition: {error}"))
|
|
}
|
|
|
|
fn extract_prefab_instances(
|
|
entities: &BTreeMap<u64, DynamicSceneEntity>,
|
|
) -> Result<Vec<PrefabInstance>, String> {
|
|
entities
|
|
.values()
|
|
.filter_map(|entity| entity.components.get(PREFAB_INSTANCE_COMPONENT))
|
|
.map(|value| {
|
|
value
|
|
.clone()
|
|
.into_rust::<PrefabInstance>()
|
|
.map_err(|error| format!("could not parse prefab instance: {error}"))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn extract_prefab_refs(
|
|
entities: &BTreeMap<u64, DynamicSceneEntity>,
|
|
) -> Result<Vec<PrefabRef>, String> {
|
|
entities
|
|
.values()
|
|
.filter_map(|entity| entity.components.get(PREFAB_REF_COMPONENT))
|
|
.map(|value| {
|
|
value
|
|
.clone()
|
|
.into_rust::<PrefabRef>()
|
|
.map_err(|error| format!("could not parse prefab reference: {error}"))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn extract_name(components: &[SceneComponentBlob]) -> Option<String> {
|
|
components
|
|
.iter()
|
|
.find(|component| component.type_name == NAME_COMPONENT)
|
|
.map(|component| component.ron.trim().trim_matches('"').to_string())
|
|
.filter(|name| !name.is_empty())
|
|
}
|
|
|
|
fn extract_actor_id(components: &[SceneComponentBlob]) -> Option<String> {
|
|
let component = components
|
|
.iter()
|
|
.find(|component| component.type_name == ACTOR_ID_COMPONENT)?;
|
|
ron::from_str::<ActorId>(&component.ron)
|
|
.ok()
|
|
.map(|id| id.0)
|
|
.or_else(|| ron::from_str::<String>(&component.ron).ok())
|
|
.or_else(|| {
|
|
ron::from_str::<Vec<String>>(&component.ron)
|
|
.ok()
|
|
.and_then(|values| values.into_iter().next())
|
|
})
|
|
.filter(|value| !value.trim().is_empty())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn lossless_parser_accepts_anonymous_structs_and_tuples() {
|
|
let value = "(translation: (1.0, 2.0, 3.0), scale: (1.0, 1.0, 1.0))";
|
|
assert!(ron_edit::File::try_from(value).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_scene_document_without_exposing_runtime_entity_ids() {
|
|
let text = r#"(schema_version: 2,
|
|
resources: {},
|
|
entities: {
|
|
4294967133: (
|
|
components: {
|
|
"bevy_ecs::name::Name": "Crate",
|
|
"shared::components::ActorId": ("crate-01"),
|
|
"shared::components::LevelObject": (),
|
|
},
|
|
),
|
|
},
|
|
)"#;
|
|
let document = SceneDocument::from_ron_text(text).unwrap();
|
|
assert_eq!(document.schema_version, crate::CURRENT_SCENE_SCHEMA_VERSION);
|
|
assert_eq!(document.entities.len(), 1);
|
|
assert_eq!(document.entities[0].actor_id.as_deref(), Some("crate-01"));
|
|
assert_eq!(document.entities[0].name.as_deref(), Some("Crate"));
|
|
assert_eq!(
|
|
document.entities[0].document_id,
|
|
SceneEntityId::ActorId("crate-01".into())
|
|
);
|
|
assert!(!format!("{:?}", document.entities[0].document_id).contains("4294967133"));
|
|
}
|
|
|
|
#[test]
|
|
fn round_trips_normalized_ron_text() {
|
|
let body = "(resources: {}, entities: {})";
|
|
let document = SceneDocument::from_ron_text(body).unwrap();
|
|
let text = document.to_ron_text().unwrap();
|
|
assert!(text.starts_with("(schema_version: 2,"));
|
|
assert!(SceneDocument::from_ron_text(&text)
|
|
.unwrap()
|
|
.entities
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn patch_rename_and_transform_are_representable() {
|
|
let text = r#"(schema_version: 2,
|
|
resources: {},
|
|
entities: {
|
|
1: (
|
|
components: {
|
|
"shared::components::ActorId": ("actor-a"),
|
|
"shared::components::LevelObject": (),
|
|
},
|
|
),
|
|
},
|
|
)"#;
|
|
let mut document = SceneDocument::from_ron_text(text).unwrap();
|
|
let target = SceneEntityId::ActorId("actor-a".into());
|
|
document
|
|
.apply_patch(ScenePatch::Rename {
|
|
target: target.clone(),
|
|
name: "Renamed".into(),
|
|
})
|
|
.unwrap();
|
|
document
|
|
.apply_patch(ScenePatch::SetTransform {
|
|
target: target.clone(),
|
|
transform: SceneTransform {
|
|
translation: [1.0, 2.0, 3.0],
|
|
..Default::default()
|
|
},
|
|
})
|
|
.unwrap();
|
|
|
|
let entity = document.find_entity(&target).unwrap();
|
|
assert_eq!(entity.name.as_deref(), Some("Renamed"));
|
|
assert!(entity
|
|
.components
|
|
.iter()
|
|
.any(|component| component.type_name == TRANSFORM_COMPONENT));
|
|
|
|
let reparsed = SceneDocument::from_ron_text(&document.to_ron_text().unwrap()).unwrap();
|
|
let entity = reparsed.find_entity(&target).unwrap();
|
|
assert_eq!(entity.name.as_deref(), Some("Renamed"));
|
|
let transform = entity
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == TRANSFORM_COMPONENT)
|
|
.unwrap();
|
|
assert!(transform.ron.contains("1.0"));
|
|
}
|
|
}
|