4335 lines
159 KiB
Rust
4335 lines
159 KiB
Rust
//! Prefab instance overrides — apply/revert per field group (v2).
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use bevy::prelude::*;
|
|
use bevy::world_serialization::{
|
|
DynamicWorld, DynamicWorldRoot, WorldInstance, WorldInstanceReady,
|
|
};
|
|
use bevy_egui::egui;
|
|
use egui_phosphor_icons::icons;
|
|
use scene::document::{SceneComponentBlob, SceneDocument, SceneEntityId, ScenePatch};
|
|
use shared::{
|
|
decode_prefab_overrides, encode_prefab_overrides, ActorId, EditorVisibility,
|
|
HydratedPrefabMember, HydratedPrefabReady, MaterialDesc, PrefabActorPath,
|
|
PrefabComponentOverride, PrefabInstance, PrefabOverrides, PrefabPropertyOverride, PrefabRef,
|
|
PrefabStructuralOverride,
|
|
};
|
|
|
|
use crate::asset_db::{ensure_asset_record, AssetRegistry};
|
|
use crate::history::{
|
|
capture_prefab_edit_state, record_prefab_edit_state, record_prefab_source_apply,
|
|
};
|
|
use crate::ui::inspector::{
|
|
apply_component_card_response, component_card, component_card_context, property_row,
|
|
ComponentCardOptions,
|
|
};
|
|
use crate::ui::theme::{ERROR, SUCCESS, TEXT_MUTED, WARNING};
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct PrefabOverrideClipboard {
|
|
pub property_path: String,
|
|
pub value_ron: String,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct PrefabHealthCache {
|
|
entries: HashMap<Entity, (Instant, PrefabHealth)>,
|
|
}
|
|
|
|
pub fn prefab_source_revision(project_root: &Path, source_path: &str) -> Result<String, String> {
|
|
scene::prefab_graph_revision(&project_root.join(source_path), project_root)
|
|
}
|
|
|
|
fn current_source_revision(world: &World, instance: &PrefabInstance) -> Option<String> {
|
|
let root = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))?;
|
|
prefab_source_revision(&root, &instance.source_path).ok()
|
|
}
|
|
|
|
fn track_current_source_revision(
|
|
world: &World,
|
|
instance: &PrefabInstance,
|
|
overrides: &mut PrefabOverrides,
|
|
) {
|
|
overrides.source_revision = current_source_revision(world, instance);
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum PrefabHealth {
|
|
Ready,
|
|
Loading,
|
|
Untracked,
|
|
ChangedBase,
|
|
Conflict(Vec<String>),
|
|
Broken(String),
|
|
InvalidOverrides(String),
|
|
}
|
|
|
|
fn prefab_health(
|
|
world: &mut World,
|
|
entity: Entity,
|
|
instance: &PrefabInstance,
|
|
overrides: &PrefabOverrides,
|
|
) -> PrefabHealth {
|
|
if let Some(health) = world
|
|
.get_resource::<PrefabHealthCache>()
|
|
.and_then(|cache| cache.entries.get(&entity))
|
|
.filter(|(checked, _)| checked.elapsed() < Duration::from_millis(500))
|
|
.map(|(_, health)| health.clone())
|
|
{
|
|
return health;
|
|
}
|
|
let health = compute_prefab_health(world, entity, instance, overrides);
|
|
if let Some(mut cache) = world.get_resource_mut::<PrefabHealthCache>() {
|
|
cache
|
|
.entries
|
|
.insert(entity, (Instant::now(), health.clone()));
|
|
}
|
|
health
|
|
}
|
|
|
|
fn compute_prefab_health(
|
|
world: &World,
|
|
entity: Entity,
|
|
instance: &PrefabInstance,
|
|
overrides: &PrefabOverrides,
|
|
) -> PrefabHealth {
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return PrefabHealth::Broken("Project workspace is unavailable".to_string());
|
|
};
|
|
if let Some(blocked) = world.get::<shared::PrefabHydrationBlocked>(entity) {
|
|
return PrefabHealth::Broken(blocked.reason.clone());
|
|
}
|
|
if let Err(error) =
|
|
scene::validate_prefab_graph(&project_root.join(&instance.source_path), &project_root)
|
|
{
|
|
return PrefabHealth::Broken(error);
|
|
}
|
|
if world.get::<HydratedPrefabReady>(entity).is_none() {
|
|
return PrefabHealth::Loading;
|
|
}
|
|
let current = prefab_source_revision(&project_root, &instance.source_path).ok();
|
|
let known_keys = prefab_legacy_target_keys(&project_root, instance);
|
|
let mut stale: Vec<String> = overrides
|
|
.child_visibility
|
|
.keys()
|
|
.filter(|key| !known_keys.contains(*key))
|
|
.cloned()
|
|
.collect();
|
|
stale.extend(prefab_semantic_conflicts(
|
|
&project_root,
|
|
instance,
|
|
overrides,
|
|
));
|
|
stale.sort();
|
|
stale.dedup();
|
|
if !stale.is_empty() {
|
|
return PrefabHealth::Conflict(stale);
|
|
}
|
|
match (&overrides.source_revision, current) {
|
|
(None, _) => PrefabHealth::Untracked,
|
|
(Some(recorded), Some(current)) if recorded != ¤t => PrefabHealth::ChangedBase,
|
|
_ => PrefabHealth::Ready,
|
|
}
|
|
}
|
|
|
|
fn prefab_legacy_target_keys(project_root: &Path, instance: &PrefabInstance) -> HashSet<String> {
|
|
let direct = PrefabActorPath::direct("");
|
|
source_document_for_target(project_root, instance, &direct)
|
|
.map(|document| {
|
|
document
|
|
.entities
|
|
.into_iter()
|
|
.flat_map(|entity| [entity.actor_id, entity.name])
|
|
.flatten()
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn prefab_semantic_conflicts(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
overrides: &PrefabOverrides,
|
|
) -> Vec<String> {
|
|
let mut conflicts = Vec::new();
|
|
for property in &overrides.properties {
|
|
let source = source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
&property.target,
|
|
&property.component_type,
|
|
)
|
|
.and_then(|source| {
|
|
apply_layer_prerequisites(
|
|
source,
|
|
overrides,
|
|
&property.target,
|
|
&property.component_type,
|
|
)
|
|
});
|
|
let conflict = match source {
|
|
Ok(Some(source)) if property.component_type == std::any::type_name::<Transform>() => {
|
|
apply_transform_property(&source.ron, property).is_err()
|
|
}
|
|
Ok(Some(source))
|
|
if property.component_type == std::any::type_name::<MaterialDesc>() =>
|
|
{
|
|
apply_material_property(&source.ron, property).is_err()
|
|
}
|
|
Ok(Some(_)) => false,
|
|
_ => true,
|
|
};
|
|
if conflict {
|
|
conflicts.push(property_conflict_key(property));
|
|
}
|
|
}
|
|
for component in &overrides.components {
|
|
let current = source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
&component.target,
|
|
&component.component_type,
|
|
)
|
|
.ok()
|
|
.flatten();
|
|
let conflict = match (&component.base_component_ron, current) {
|
|
(None, Some(_)) | (Some(_), None) => true,
|
|
(Some(base), Some(current)) => {
|
|
!component_values_equal(&component.component_type, ¤t.ron, base)
|
|
}
|
|
(None, None) => false,
|
|
};
|
|
if conflict {
|
|
conflicts.push(component_conflict_key(component));
|
|
}
|
|
}
|
|
for structural in &overrides.structural {
|
|
let target = structural_target(structural);
|
|
let document = source_document_for_target(project_root, instance, target);
|
|
let source_entity = document.as_ref().ok().and_then(|document| {
|
|
document.find_entity(&SceneEntityId::ActorId(target.actor_id.clone()))
|
|
});
|
|
let conflict = match structural {
|
|
PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision,
|
|
..
|
|
} => {
|
|
source_entity.is_none()
|
|
|| match prefab_source_revision(project_root, &instance.source_path) {
|
|
Ok(current) => current != *base_subtree_revision,
|
|
Err(_) => true,
|
|
}
|
|
}
|
|
PrefabStructuralOverride::ReparentChild {
|
|
base_parent,
|
|
new_parent,
|
|
base_transform_ron,
|
|
..
|
|
} => {
|
|
let parent_changed = match source_parent_for_target(project_root, instance, target)
|
|
{
|
|
Ok(current) => current != *base_parent,
|
|
Err(_) => true,
|
|
};
|
|
let transform_changed = base_transform_ron.as_ref().is_some_and(|base| {
|
|
source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
target,
|
|
std::any::type_name::<Transform>(),
|
|
)
|
|
.ok()
|
|
.flatten()
|
|
.is_none_or(|current| {
|
|
!component_values_equal(
|
|
std::any::type_name::<Transform>(),
|
|
¤t.ron,
|
|
base,
|
|
)
|
|
})
|
|
});
|
|
let destination_missing = new_parent.as_ref().is_some_and(|parent| {
|
|
parent.instance_chain != target.instance_chain
|
|
|| source_parent_for_target(project_root, instance, parent).is_err()
|
|
});
|
|
parent_changed || transform_changed || destination_missing
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { .. } => false,
|
|
};
|
|
if conflict {
|
|
conflicts.push(structural_conflict_key(structural));
|
|
}
|
|
}
|
|
conflicts
|
|
}
|
|
|
|
fn actor_path_key(target: &PrefabActorPath) -> String {
|
|
if target.instance_chain.is_empty() {
|
|
target.actor_id.clone()
|
|
} else {
|
|
format!("{}/{}", target.instance_chain.join("/"), target.actor_id)
|
|
}
|
|
}
|
|
|
|
fn property_conflict_key(property: &PrefabPropertyOverride) -> String {
|
|
format!(
|
|
"{}.{}.{}",
|
|
actor_path_key(&property.target),
|
|
property.component_type,
|
|
property.property_path
|
|
)
|
|
}
|
|
|
|
fn component_conflict_key(component: &PrefabComponentOverride) -> String {
|
|
format!(
|
|
"{}.{}",
|
|
actor_path_key(&component.target),
|
|
component.component_type
|
|
)
|
|
}
|
|
|
|
fn structural_conflict_key(structural: &PrefabStructuralOverride) -> String {
|
|
format!(
|
|
"{}.structure",
|
|
actor_path_key(structural_target(structural))
|
|
)
|
|
}
|
|
|
|
fn source_document_for_target(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
target: &PrefabActorPath,
|
|
) -> Result<SceneDocument, String> {
|
|
let mut source_path = instance.source_path.clone();
|
|
for anchor_actor_id in &target.instance_chain {
|
|
let document = SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(&source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)?;
|
|
let component = document
|
|
.find_entity(&SceneEntityId::ActorId(anchor_actor_id.clone()))
|
|
.and_then(|entity| {
|
|
entity.components.iter().find(|component| {
|
|
component.type_name == std::any::type_name::<PrefabInstance>()
|
|
})
|
|
})
|
|
.ok_or_else(|| format!("nested prefab anchor `{anchor_actor_id}` is missing"))?;
|
|
let nested: PrefabInstance =
|
|
ron::from_str(&component.ron).map_err(|error| error.to_string())?;
|
|
source_path = nested.source_path;
|
|
}
|
|
SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)
|
|
}
|
|
|
|
fn source_component_for_target(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
target: &PrefabActorPath,
|
|
component_type: &str,
|
|
) -> Result<Option<SceneComponentBlob>, String> {
|
|
let mut source_path = instance.source_path.clone();
|
|
let mut remaining_target = target.clone();
|
|
let mut layers = Vec::new();
|
|
for anchor_actor_id in &target.instance_chain {
|
|
let document = SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(&source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)?;
|
|
let nested_component = document
|
|
.find_entity(&SceneEntityId::ActorId(anchor_actor_id.clone()))
|
|
.and_then(|entity| {
|
|
entity.components.iter().find(|component| {
|
|
component.type_name == std::any::type_name::<PrefabInstance>()
|
|
})
|
|
})
|
|
.ok_or_else(|| format!("nested prefab anchor `{anchor_actor_id}` is missing"))?;
|
|
let nested: PrefabInstance =
|
|
ron::from_str(&nested_component.ron).map_err(|error| error.to_string())?;
|
|
remaining_target.instance_chain.remove(0);
|
|
layers.push((decode_prefab_overrides(&nested)?, remaining_target.clone()));
|
|
source_path = nested.source_path;
|
|
}
|
|
let document = SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)?;
|
|
let mut current = document
|
|
.find_entity(&SceneEntityId::ActorId(target.actor_id.clone()))
|
|
.and_then(|entity| {
|
|
entity
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == component_type)
|
|
})
|
|
.cloned();
|
|
for (overrides, layer_target) in layers.into_iter().rev() {
|
|
current = apply_layer_to_component(current, &overrides, &layer_target, component_type)?;
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
fn source_parent_for_target(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
target: &PrefabActorPath,
|
|
) -> Result<Option<PrefabActorPath>, String> {
|
|
let mut source_path = instance.source_path.clone();
|
|
let mut remaining_target = target.clone();
|
|
let mut layers = Vec::new();
|
|
for anchor_actor_id in &target.instance_chain {
|
|
let document = SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(&source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)?;
|
|
let component = document
|
|
.find_entity(&SceneEntityId::ActorId(anchor_actor_id.clone()))
|
|
.and_then(|entity| {
|
|
entity.components.iter().find(|component| {
|
|
component.type_name == std::any::type_name::<PrefabInstance>()
|
|
})
|
|
})
|
|
.ok_or_else(|| format!("nested prefab anchor `{anchor_actor_id}` is missing"))?;
|
|
let nested: PrefabInstance =
|
|
ron::from_str(&component.ron).map_err(|error| error.to_string())?;
|
|
remaining_target.instance_chain.remove(0);
|
|
layers.push((decode_prefab_overrides(&nested)?, remaining_target.clone()));
|
|
source_path = nested.source_path;
|
|
}
|
|
let document = SceneDocument::from_ron_text(
|
|
&std::fs::read_to_string(project_root.join(source_path))
|
|
.map_err(|error| error.to_string())?,
|
|
)?;
|
|
let mut parent_actor_id = document
|
|
.find_entity(&SceneEntityId::ActorId(target.actor_id.clone()))
|
|
.ok_or_else(|| format!("source actor `{}` is missing", actor_path_key(target)))?
|
|
.parent_actor_id
|
|
.clone();
|
|
for (overrides, layer_target) in layers.into_iter().rev() {
|
|
for structural in &overrides.structural {
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild { target, .. } if target == &layer_target => {
|
|
return Err(format!(
|
|
"source actor `{}` was removed by a nested layer",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
base_parent,
|
|
new_parent,
|
|
..
|
|
} if target == &layer_target => {
|
|
let expected = base_parent.as_ref().map(|parent| parent.actor_id.as_str());
|
|
if parent_actor_id.as_deref() != expected {
|
|
return Err(format!(
|
|
"nested parent override for `{}` conflicts with its source",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
parent_actor_id = new_parent.as_ref().map(|parent| parent.actor_id.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
Ok(parent_actor_id.map(|actor_id| PrefabActorPath {
|
|
instance_chain: target.instance_chain.clone(),
|
|
actor_id,
|
|
}))
|
|
}
|
|
|
|
fn apply_layer_to_component(
|
|
current: Option<SceneComponentBlob>,
|
|
overrides: &PrefabOverrides,
|
|
target: &PrefabActorPath,
|
|
component_type: &str,
|
|
) -> Result<Option<SceneComponentBlob>, String> {
|
|
let mut current = apply_layer_prerequisites(current, overrides, target, component_type)?;
|
|
for property in &overrides.properties {
|
|
if property.target != *target || property.component_type != component_type {
|
|
continue;
|
|
}
|
|
let source = current.as_ref().ok_or_else(|| {
|
|
format!(
|
|
"nested property `{}` target is missing component `{component_type}`",
|
|
property.property_path
|
|
)
|
|
})?;
|
|
let ron = if component_type == std::any::type_name::<Transform>() {
|
|
apply_transform_property(&source.ron, property)?
|
|
} else if component_type == std::any::type_name::<MaterialDesc>() {
|
|
apply_material_property(&source.ron, property)?
|
|
} else {
|
|
return Err(format!(
|
|
"nested property resolution does not support component `{component_type}`"
|
|
));
|
|
};
|
|
current = Some(SceneComponentBlob::from_ron(component_type, ron)?);
|
|
}
|
|
if overrides.structural.iter().any(|structural| {
|
|
matches!(structural, PrefabStructuralOverride::RemoveChild { target: candidate, .. } if candidate == target)
|
|
}) {
|
|
return Ok(None);
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
fn apply_layer_prerequisites(
|
|
mut current: Option<SceneComponentBlob>,
|
|
overrides: &PrefabOverrides,
|
|
target: &PrefabActorPath,
|
|
component_type: &str,
|
|
) -> Result<Option<SceneComponentBlob>, String> {
|
|
if component_type == std::any::type_name::<Transform>() {
|
|
for structural in &overrides.structural {
|
|
if let PrefabStructuralOverride::ReparentChild {
|
|
target: candidate,
|
|
base_transform_ron,
|
|
local_transform_ron,
|
|
..
|
|
} = structural
|
|
{
|
|
if candidate != target {
|
|
continue;
|
|
}
|
|
if let Some(base) = base_transform_ron {
|
|
let current_ron = current.as_ref().map(|component| component.ron.as_str());
|
|
if current_ron
|
|
.is_none_or(|value| !component_values_equal(component_type, value, base))
|
|
{
|
|
return Err(format!(
|
|
"nested reparent transform for `{}` conflicts with its source",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
}
|
|
if let Some(value) = local_transform_ron {
|
|
current = Some(SceneComponentBlob::from_ron(component_type, value.clone())?);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for component in &overrides.components {
|
|
if component.target != *target || component.component_type != component_type {
|
|
continue;
|
|
}
|
|
let current_ron = current.as_ref().map(|value| value.ron.as_str());
|
|
let base_matches = match (&component.base_component_ron, current_ron) {
|
|
(None, None) => true,
|
|
(Some(base), Some(current)) => component_values_equal(component_type, current, base),
|
|
_ => false,
|
|
};
|
|
if !base_matches {
|
|
return Err(format!(
|
|
"nested component `{component_type}` for `{}` conflicts with its source",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
current = component
|
|
.value_component_ron
|
|
.as_ref()
|
|
.map(|value| SceneComponentBlob::from_ron(component_type, value.clone()))
|
|
.transpose()?;
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
fn source_target_exists(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
target: &PrefabActorPath,
|
|
) -> bool {
|
|
source_document_for_target(project_root, instance, target)
|
|
.ok()
|
|
.and_then(|document| {
|
|
document
|
|
.find_entity(&SceneEntityId::ActorId(target.actor_id.clone()))
|
|
.map(|_| ())
|
|
})
|
|
.is_some()
|
|
}
|
|
|
|
fn accept_current_source(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
fn keep_prefab_overrides(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
let prerequisite_layer = overrides.clone();
|
|
for property in &mut overrides.properties {
|
|
if let Ok(Some(source)) = source_component_for_target(
|
|
&project_root,
|
|
&instance,
|
|
&property.target,
|
|
&property.component_type,
|
|
)
|
|
.and_then(|source| {
|
|
apply_layer_prerequisites(
|
|
source,
|
|
&prerequisite_layer,
|
|
&property.target,
|
|
&property.component_type,
|
|
)
|
|
}) {
|
|
property.base_component_ron = source.ron;
|
|
}
|
|
}
|
|
for component in &mut overrides.components {
|
|
component.base_component_ron = source_component_for_target(
|
|
&project_root,
|
|
&instance,
|
|
&component.target,
|
|
&component.component_type,
|
|
)
|
|
.ok()
|
|
.flatten()
|
|
.map(|source| source.ron);
|
|
}
|
|
for structural in &mut overrides.structural {
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision,
|
|
..
|
|
} => {
|
|
*base_subtree_revision =
|
|
prefab_source_revision(&project_root, &instance.source_path)
|
|
.unwrap_or_default();
|
|
}
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
base_parent,
|
|
base_transform_ron,
|
|
..
|
|
} => {
|
|
if let Ok(parent) = source_parent_for_target(&project_root, &instance, target) {
|
|
*base_parent = parent;
|
|
}
|
|
*base_transform_ron = source_component_for_target(
|
|
&project_root,
|
|
&instance,
|
|
target,
|
|
std::any::type_name::<Transform>(),
|
|
)
|
|
.ok()
|
|
.flatten()
|
|
.map(|component| component.ron);
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { .. } => {}
|
|
}
|
|
}
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
fn take_source_for_conflicts(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
let conflicts: HashSet<_> = prefab_semantic_conflicts(&project_root, &instance, &overrides)
|
|
.into_iter()
|
|
.collect();
|
|
overrides
|
|
.properties
|
|
.retain(|property| !conflicts.contains(&property_conflict_key(property)));
|
|
overrides
|
|
.components
|
|
.retain(|component| !conflicts.contains(&component_conflict_key(component)));
|
|
overrides
|
|
.structural
|
|
.retain(|structural| !conflicts.contains(&structural_conflict_key(structural)));
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
pub fn revert_prefab_instance_overrides(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let mut overrides = PrefabOverrides::default();
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
fn remove_stale_visibility_overrides(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
let known_keys = prefab_legacy_target_keys(&project_root, &instance);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
overrides
|
|
.child_visibility
|
|
.retain(|key, _| known_keys.contains(key));
|
|
overrides
|
|
.properties
|
|
.retain(|property| source_target_exists(&project_root, &instance, &property.target));
|
|
overrides
|
|
.components
|
|
.retain(|component| source_target_exists(&project_root, &instance, &component.target));
|
|
overrides.structural.retain(|structural| match structural {
|
|
PrefabStructuralOverride::RemoveChild { target, .. }
|
|
| PrefabStructuralOverride::ReparentChild { target, .. } => {
|
|
source_target_exists(&project_root, &instance, target)
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { parent, .. } => {
|
|
source_target_exists(&project_root, &instance, parent)
|
|
}
|
|
});
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
fn discard_invalid_overrides(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(mut instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
instance.overrides_ron = None;
|
|
world.entity_mut(entity).insert(instance);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
fn relink_prefab_source(world: &mut World, entity: Entity) {
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
let Some(path) = rfd::FileDialog::new()
|
|
.set_directory(project_root.join("assets/prefabs"))
|
|
.add_filter("Bevy prefab", &["scn.ron", "ron"])
|
|
.pick_file()
|
|
else {
|
|
return;
|
|
};
|
|
let Ok(path) = path.canonicalize() else {
|
|
return;
|
|
};
|
|
let Ok(root) = project_root.canonicalize() else {
|
|
return;
|
|
};
|
|
let Ok(relative) = path.strip_prefix(&root) else {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"Prefab source must remain inside the project".into();
|
|
return;
|
|
};
|
|
let source_path = relative.to_string_lossy().replace('\\', "/");
|
|
if let Err(error) = scene::validate_prefab_graph(&path, &root) {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Prefab relink failed: {error}");
|
|
return;
|
|
}
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(mut instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
instance.source_path = source_path.clone();
|
|
let label = path
|
|
.file_stem()
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or("Prefab")
|
|
.to_string();
|
|
let registration = world
|
|
.get_resource_mut::<AssetRegistry>()
|
|
.ok_or_else(|| "asset registry is unavailable".to_string())
|
|
.and_then(|mut registry| {
|
|
ensure_asset_record(&mut registry, source_path.clone(), label, "Prefab")
|
|
});
|
|
let record = match registration {
|
|
Ok(record) => record,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Prefab relink failed: could not register source: {error}");
|
|
return;
|
|
}
|
|
};
|
|
instance.asset_id = record.id.as_string();
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
instance.overrides_ron = encode_prefab_overrides(&overrides).ok().flatten();
|
|
world
|
|
.entity_mut(entity)
|
|
.remove::<shared::PrefabHydrationBlocked>()
|
|
.insert((instance, PrefabRef::new(source_path)));
|
|
record_prefab_edit_state(world, entity, old);
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status = "Prefab source relinked".into();
|
|
}
|
|
|
|
pub fn read_overrides(instance: &PrefabInstance) -> PrefabOverrides {
|
|
try_read_overrides(instance).unwrap_or_default()
|
|
}
|
|
|
|
pub fn try_read_overrides(instance: &PrefabInstance) -> Result<PrefabOverrides, String> {
|
|
decode_prefab_overrides(instance)
|
|
}
|
|
|
|
fn read_overrides_for_edit(
|
|
world: &mut World,
|
|
instance: &PrefabInstance,
|
|
) -> Option<PrefabOverrides> {
|
|
match try_read_overrides(instance) {
|
|
Ok(overrides) => Some(overrides),
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Prefab edit blocked: {error}. Use Discard invalid data to recover.");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn new_tracked_prefab_instance(
|
|
world: &World,
|
|
asset_id: impl Into<String>,
|
|
source_path: impl Into<String>,
|
|
) -> PrefabInstance {
|
|
let mut instance = PrefabInstance::new(asset_id, source_path);
|
|
let mut overrides = PrefabOverrides::default();
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
if overrides != PrefabOverrides::default() {
|
|
instance.overrides_ron = encode_prefab_overrides(&overrides).ok().flatten();
|
|
}
|
|
instance
|
|
}
|
|
|
|
pub fn write_overrides(world: &mut World, entity: Entity, overrides: &PrefabOverrides) {
|
|
let Some(mut instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
instance.overrides_ron = encode_prefab_overrides(overrides).unwrap_or_default();
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
|
entity_mut.insert(instance);
|
|
}
|
|
}
|
|
|
|
pub fn set_child_visibility_override(
|
|
world: &mut World,
|
|
entity: Entity,
|
|
child: Entity,
|
|
visible: bool,
|
|
) {
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, child) else {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"Cannot store prefab visibility: target has no stable prefab path".into();
|
|
return;
|
|
};
|
|
if instance_root != entity {
|
|
return;
|
|
}
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let component_type = std::any::type_name::<EditorVisibility>().to_string();
|
|
let value = EditorVisibility { visible };
|
|
let value_component_ron = match ron::to_string(&value) {
|
|
Ok(value) => value,
|
|
Err(_) => return,
|
|
};
|
|
let source_base = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
.and_then(|project_root| {
|
|
source_component_for_target(&project_root, &instance, &target, &component_type)
|
|
.ok()
|
|
.flatten()
|
|
.map(|component| component.ron)
|
|
});
|
|
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
overrides.child_visibility.remove(&target.actor_id);
|
|
let base_component_ron = overrides
|
|
.components
|
|
.iter()
|
|
.find(|candidate| candidate.target == target && candidate.component_type == component_type)
|
|
.and_then(|candidate| candidate.base_component_ron.clone())
|
|
.or(source_base);
|
|
overrides.components.retain(|candidate| {
|
|
!(candidate.target == target && candidate.component_type == component_type)
|
|
});
|
|
overrides.components.push(PrefabComponentOverride {
|
|
target,
|
|
component_type,
|
|
base_component_ron,
|
|
value_component_ron: Some(value_component_ron),
|
|
});
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
world.entity_mut(child).insert(value);
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
}
|
|
|
|
pub fn prefab_member_override_target(
|
|
world: &World,
|
|
member: Entity,
|
|
) -> Option<(Entity, PrefabActorPath)> {
|
|
let actor_id = world.get::<ActorId>(member)?.0.clone();
|
|
let mut instance_root = world.get::<HydratedPrefabMember>(member)?.instance_root;
|
|
let mut instance_chain = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
while seen.insert(instance_root) {
|
|
let Some(owner) = world.get::<HydratedPrefabMember>(instance_root) else {
|
|
break;
|
|
};
|
|
instance_chain.insert(0, world.get::<ActorId>(instance_root)?.0.clone());
|
|
instance_root = owner.instance_root;
|
|
}
|
|
world.get::<PrefabInstance>(instance_root)?;
|
|
Some((
|
|
instance_root,
|
|
PrefabActorPath {
|
|
instance_chain,
|
|
actor_id,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn set_prefab_material_property_override(
|
|
world: &mut World,
|
|
member: Entity,
|
|
property_path: &str,
|
|
value: MaterialDesc,
|
|
) {
|
|
record_prefab_property_override(world, member, property_path, value);
|
|
}
|
|
|
|
pub fn set_prefab_transform_property_override(
|
|
world: &mut World,
|
|
member: Entity,
|
|
property_path: &str,
|
|
value: Transform,
|
|
) {
|
|
record_prefab_property_override(world, member, property_path, value);
|
|
}
|
|
|
|
fn record_prefab_property_override<T>(
|
|
world: &mut World,
|
|
member: Entity,
|
|
property_path: &str,
|
|
value: T,
|
|
) where
|
|
T: Component + Clone + serde::Serialize,
|
|
{
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return;
|
|
};
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let Some(current) = world.get::<T>(member).cloned() else {
|
|
return;
|
|
};
|
|
let component_type = std::any::type_name::<T>().to_string();
|
|
let current_ron = match ron::to_string(¤t) {
|
|
Ok(value) => value,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Could not capture prefab property: {error}");
|
|
return;
|
|
}
|
|
};
|
|
let value_ron = match ron::to_string(&value) {
|
|
Ok(value) => value,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Could not encode prefab property: {error}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
let base_component_ron = overrides
|
|
.properties
|
|
.iter()
|
|
.find(|candidate| candidate.target == target && candidate.component_type == component_type)
|
|
.map(|candidate| candidate.base_component_ron.clone())
|
|
.unwrap_or(current_ron);
|
|
overrides.properties.retain(|candidate| {
|
|
!(candidate.target == target
|
|
&& candidate.component_type == component_type
|
|
&& candidate.property_path == property_path)
|
|
});
|
|
overrides.properties.push(PrefabPropertyOverride {
|
|
target,
|
|
component_type,
|
|
property_path: property_path.to_string(),
|
|
base_component_ron,
|
|
value_component_ron: value_ron,
|
|
});
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
world.entity_mut(member).insert(value);
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
}
|
|
|
|
pub fn remove_prefab_component_override<T>(world: &mut World, member: Entity)
|
|
where
|
|
T: Component + Clone + serde::Serialize,
|
|
{
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return;
|
|
};
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let Some(current) = world.get::<T>(member).cloned() else {
|
|
return;
|
|
};
|
|
let Ok(current_component_ron) = ron::to_string(¤t) else {
|
|
return;
|
|
};
|
|
let component_type = std::any::type_name::<T>().to_string();
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
let existing_component = overrides
|
|
.components
|
|
.iter()
|
|
.find(|candidate| candidate.target == target && candidate.component_type == component_type);
|
|
let cancels_local_add = existing_component.is_some_and(|component| {
|
|
component.base_component_ron.is_none() && component.value_component_ron.is_some()
|
|
});
|
|
let base_component_ron = existing_component
|
|
.and_then(|component| component.base_component_ron.clone())
|
|
.or_else(|| {
|
|
overrides
|
|
.properties
|
|
.iter()
|
|
.find(|candidate| {
|
|
candidate.target == target && candidate.component_type == component_type
|
|
})
|
|
.map(|property| property.base_component_ron.clone())
|
|
})
|
|
.or_else(|| {
|
|
world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
.and_then(|project_root| {
|
|
source_component_for_target(&project_root, &instance, &target, &component_type)
|
|
.ok()
|
|
.flatten()
|
|
.map(|component| component.ron)
|
|
})
|
|
})
|
|
.or(Some(current_component_ron));
|
|
overrides.properties.retain(|candidate| {
|
|
!(candidate.target == target && candidate.component_type == component_type)
|
|
});
|
|
overrides.components.retain(|candidate| {
|
|
!(candidate.target == target && candidate.component_type == component_type)
|
|
});
|
|
if !cancels_local_add {
|
|
overrides.components.push(PrefabComponentOverride {
|
|
target,
|
|
component_type,
|
|
base_component_ron,
|
|
value_component_ron: None,
|
|
});
|
|
}
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
world.entity_mut(member).remove::<T>();
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
}
|
|
|
|
pub fn set_prefab_component_override<T>(world: &mut World, member: Entity, value: T)
|
|
where
|
|
T: Component + Clone + serde::Serialize,
|
|
{
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return;
|
|
};
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let value_component_ron = match ron::to_string(&value) {
|
|
Ok(value) => value,
|
|
Err(_) => return,
|
|
};
|
|
let component_type = std::any::type_name::<T>().to_string();
|
|
let base_component_ron = world
|
|
.get::<T>(member)
|
|
.and_then(|current| ron::to_string(current).ok());
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
let base_component_ron = overrides
|
|
.components
|
|
.iter()
|
|
.find(|candidate| candidate.target == target && candidate.component_type == component_type)
|
|
.and_then(|candidate| candidate.base_component_ron.clone())
|
|
.or(base_component_ron);
|
|
overrides.components.retain(|candidate| {
|
|
!(candidate.target == target && candidate.component_type == component_type)
|
|
});
|
|
overrides.components.push(PrefabComponentOverride {
|
|
target,
|
|
component_type,
|
|
base_component_ron,
|
|
value_component_ron: Some(value_component_ron),
|
|
});
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
world.entity_mut(member).insert(value);
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
}
|
|
|
|
pub fn revert_prefab_override_scope(
|
|
world: &mut World,
|
|
member: Entity,
|
|
component_type: Option<&str>,
|
|
property_path: Option<&str>,
|
|
) {
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return;
|
|
};
|
|
revert_prefab_target_scope(world, instance_root, &target, component_type, property_path);
|
|
}
|
|
|
|
fn revert_prefab_target_scope(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
target: &PrefabActorPath,
|
|
component_type: Option<&str>,
|
|
property_path: Option<&str>,
|
|
) {
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
overrides.properties.retain(|candidate| {
|
|
candidate.target != *target
|
|
|| component_type.is_some_and(|value| candidate.component_type != value)
|
|
|| property_path.is_some_and(|value| candidate.property_path != value)
|
|
});
|
|
if property_path.is_none() {
|
|
overrides.components.retain(|candidate| {
|
|
candidate.target != *target
|
|
|| component_type.is_some_and(|value| candidate.component_type != value)
|
|
});
|
|
}
|
|
if component_type.is_none() && property_path.is_none() {
|
|
overrides
|
|
.structural
|
|
.retain(|candidate| structural_target(candidate) != target);
|
|
}
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
}
|
|
|
|
pub fn revert_prefab_actor_path(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
target: &PrefabActorPath,
|
|
) {
|
|
revert_prefab_target_scope(world, instance_root, target, None, None);
|
|
}
|
|
|
|
pub fn apply_prefab_member_scope_to_source(
|
|
world: &mut World,
|
|
member: Entity,
|
|
component_type: Option<&str>,
|
|
property_path: Option<&str>,
|
|
) {
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return;
|
|
};
|
|
apply_prefab_scope_to_source(
|
|
world,
|
|
instance_root,
|
|
Some(&target),
|
|
component_type,
|
|
property_path,
|
|
);
|
|
}
|
|
|
|
pub fn apply_prefab_instance_overrides_to_source(world: &mut World, instance_root: Entity) {
|
|
apply_prefab_scope_to_source(world, instance_root, None, None, None);
|
|
}
|
|
|
|
pub fn apply_prefab_actor_path_to_source(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
target: &PrefabActorPath,
|
|
) {
|
|
apply_prefab_scope_to_source(world, instance_root, Some(target), None, None);
|
|
}
|
|
|
|
fn apply_prefab_scope_to_source(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
target_filter: Option<&PrefabActorPath>,
|
|
component_type: Option<&str>,
|
|
property_path: Option<&str>,
|
|
) {
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
let source_path = project_root.join(&instance.source_path);
|
|
let before_source = match std::fs::read_to_string(&source_path) {
|
|
Ok(source) => source,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Apply to source failed: {error}");
|
|
return;
|
|
}
|
|
};
|
|
let mut document = match SceneDocument::from_ron_text(&before_source) {
|
|
Ok(document) => document,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Apply to source failed: {error}");
|
|
return;
|
|
}
|
|
};
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
if let Err(error) =
|
|
promote_legacy_visibility_overrides(&project_root, &instance, &mut overrides)
|
|
{
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Apply to source failed: {error}");
|
|
return;
|
|
}
|
|
let selected_properties: Vec<_> = overrides
|
|
.properties
|
|
.iter()
|
|
.filter(|candidate| {
|
|
target_filter.is_none_or(|target| candidate.target == *target)
|
|
&& component_type.is_none_or(|value| candidate.component_type == value)
|
|
&& property_path.is_none_or(|value| candidate.property_path == value)
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
let selected_components: Vec<_> = overrides
|
|
.components
|
|
.iter()
|
|
.filter(|candidate| {
|
|
target_filter.is_none_or(|target| candidate.target == *target)
|
|
&& component_type.is_none_or(|value| candidate.component_type == value)
|
|
&& (property_path.is_none()
|
|
|| (candidate.base_component_ron.is_none()
|
|
&& candidate.value_component_ron.is_some()))
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
let selected_structural: Vec<_> = if component_type.is_none() && property_path.is_none() {
|
|
overrides
|
|
.structural
|
|
.iter()
|
|
.filter(|candidate| {
|
|
target_filter.is_none_or(|target| structural_target(candidate) == target)
|
|
})
|
|
.cloned()
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
if selected_properties.is_empty()
|
|
&& selected_components.is_empty()
|
|
&& selected_structural.is_empty()
|
|
{
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"No prefab overrides in this scope".into();
|
|
return;
|
|
}
|
|
|
|
let apply_result = (|| {
|
|
for structural in &selected_structural {
|
|
validate_structural_apply_base(&project_root, &instance, structural)?;
|
|
}
|
|
for component in &selected_components {
|
|
validate_component_apply_base(
|
|
&project_root,
|
|
&instance,
|
|
component,
|
|
&selected_structural,
|
|
)?;
|
|
}
|
|
for property in &selected_properties {
|
|
validate_property_apply_base(
|
|
&project_root,
|
|
&instance,
|
|
property,
|
|
&selected_components,
|
|
&selected_structural,
|
|
)?;
|
|
}
|
|
for structural in &selected_structural {
|
|
if matches!(structural, PrefabStructuralOverride::ReparentChild { .. }) {
|
|
apply_or_transfer_structural(&mut document, structural, &project_root)?;
|
|
}
|
|
}
|
|
for component in &selected_components {
|
|
if component.target.instance_chain.is_empty() {
|
|
apply_component_to_document(&mut document, component)?;
|
|
} else {
|
|
transfer_component_to_nested_layer(
|
|
&mut document,
|
|
component.clone(),
|
|
&project_root,
|
|
)?;
|
|
}
|
|
}
|
|
for property in &selected_properties {
|
|
if property.target.instance_chain.is_empty() {
|
|
apply_property_to_document(&mut document, property)?;
|
|
} else {
|
|
transfer_property_to_nested_layer(&mut document, property.clone(), &project_root)?;
|
|
}
|
|
}
|
|
for structural in &selected_structural {
|
|
if !matches!(structural, PrefabStructuralOverride::ReparentChild { .. }) {
|
|
apply_or_transfer_structural(&mut document, structural, &project_root)?;
|
|
}
|
|
}
|
|
let after_source = document.to_ron_text()?;
|
|
scene::validate_prefab_graph_text(&after_source, &source_path, &project_root)?;
|
|
crate::scene::recovery::atomic_write(&source_path, after_source.as_bytes())?;
|
|
Ok::<_, String>(after_source)
|
|
})();
|
|
let after_source = match apply_result {
|
|
Ok(source) => source,
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Apply to source failed: {error}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
overrides
|
|
.properties
|
|
.retain(|candidate| !selected_properties.contains(candidate));
|
|
overrides
|
|
.components
|
|
.retain(|candidate| !selected_components.contains(candidate));
|
|
overrides
|
|
.structural
|
|
.retain(|candidate| !selected_structural.contains(candidate));
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
record_prefab_source_apply(
|
|
world,
|
|
instance_root,
|
|
source_path,
|
|
before_source,
|
|
after_source,
|
|
old,
|
|
);
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"Prefab overrides applied to source".into();
|
|
}
|
|
|
|
fn apply_or_transfer_structural(
|
|
document: &mut SceneDocument,
|
|
structural: &PrefabStructuralOverride,
|
|
project_root: &Path,
|
|
) -> Result<(), String> {
|
|
if structural_target(structural).instance_chain.is_empty() {
|
|
apply_structural_to_document(document, structural)
|
|
} else {
|
|
transfer_structural_to_nested_layer(document, structural.clone(), project_root)
|
|
}
|
|
}
|
|
|
|
fn validate_property_apply_base(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
property: &PrefabPropertyOverride,
|
|
selected_components: &[PrefabComponentOverride],
|
|
selected_structural: &[PrefabStructuralOverride],
|
|
) -> Result<(), String> {
|
|
let mut current = source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
&property.target,
|
|
&property.component_type,
|
|
)?;
|
|
if property.component_type == std::any::type_name::<Transform>() {
|
|
if let Some(local_transform_ron) =
|
|
selected_structural
|
|
.iter()
|
|
.find_map(|structural| match structural {
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
local_transform_ron,
|
|
..
|
|
} if target == &property.target => local_transform_ron.clone(),
|
|
_ => None,
|
|
})
|
|
{
|
|
current = Some(SceneComponentBlob::from_ron(
|
|
std::any::type_name::<Transform>(),
|
|
local_transform_ron,
|
|
)?);
|
|
}
|
|
}
|
|
for component in selected_components.iter().filter(|component| {
|
|
component.target == property.target && component.component_type == property.component_type
|
|
}) {
|
|
let current_ron = current.as_ref().map(|value| value.ron.as_str());
|
|
let base_matches = match (&component.base_component_ron, current_ron) {
|
|
(None, None) => true,
|
|
(Some(base), Some(current)) => {
|
|
component_values_equal(&component.component_type, current, base)
|
|
}
|
|
_ => false,
|
|
};
|
|
if !base_matches {
|
|
return Err(format!(
|
|
"conflict: source component `{}` changed",
|
|
component.component_type
|
|
));
|
|
}
|
|
current = component
|
|
.value_component_ron
|
|
.as_ref()
|
|
.map(|value| {
|
|
SceneComponentBlob::from_ron(component.component_type.clone(), value.clone())
|
|
})
|
|
.transpose()?;
|
|
}
|
|
let source = current.ok_or_else(|| {
|
|
format!(
|
|
"source actor `{}` is missing component `{}`",
|
|
actor_path_key(&property.target),
|
|
property.component_type
|
|
)
|
|
})?;
|
|
if property.component_type == std::any::type_name::<Transform>() {
|
|
apply_transform_property(&source.ron, property).map(|_| ())
|
|
} else if property.component_type == std::any::type_name::<MaterialDesc>() {
|
|
apply_material_property(&source.ron, property).map(|_| ())
|
|
} else {
|
|
Err(format!(
|
|
"property apply does not support component `{}`",
|
|
property.component_type
|
|
))
|
|
}
|
|
}
|
|
|
|
fn validate_component_apply_base(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
component: &PrefabComponentOverride,
|
|
selected_structural: &[PrefabStructuralOverride],
|
|
) -> Result<(), String> {
|
|
let mut current = source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
&component.target,
|
|
&component.component_type,
|
|
)?;
|
|
if component.component_type == std::any::type_name::<Transform>() {
|
|
if let Some(local_transform_ron) =
|
|
selected_structural
|
|
.iter()
|
|
.find_map(|structural| match structural {
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
local_transform_ron,
|
|
..
|
|
} if target == &component.target => local_transform_ron.clone(),
|
|
_ => None,
|
|
})
|
|
{
|
|
current = Some(SceneComponentBlob::from_ron(
|
|
std::any::type_name::<Transform>(),
|
|
local_transform_ron,
|
|
)?);
|
|
}
|
|
}
|
|
let current_ron = current.as_ref().map(|value| value.ron.as_str());
|
|
let base_matches = match (&component.base_component_ron, current_ron) {
|
|
(None, None) => true,
|
|
(Some(base), Some(current)) => {
|
|
component_values_equal(&component.component_type, current, base)
|
|
}
|
|
_ => false,
|
|
};
|
|
if base_matches {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"conflict: source component `{}` changed",
|
|
component.component_type
|
|
))
|
|
}
|
|
}
|
|
|
|
fn structural_target(structural: &PrefabStructuralOverride) -> &PrefabActorPath {
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild { target, .. }
|
|
| PrefabStructuralOverride::ReparentChild { target, .. } => target,
|
|
PrefabStructuralOverride::AttachLocalChild { parent, .. } => parent,
|
|
}
|
|
}
|
|
|
|
fn promote_legacy_visibility_overrides(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
overrides: &mut PrefabOverrides,
|
|
) -> Result<(), String> {
|
|
if overrides.child_visibility.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let direct = PrefabActorPath::direct("");
|
|
let document = source_document_for_target(project_root, instance, &direct)?;
|
|
let component_type = std::any::type_name::<EditorVisibility>().to_string();
|
|
let legacy = std::mem::take(&mut overrides.child_visibility);
|
|
for (key, visible) in legacy {
|
|
let source = document
|
|
.entities
|
|
.iter()
|
|
.find(|entity| {
|
|
entity.actor_id.as_deref() == Some(key.as_str())
|
|
|| entity.name.as_deref() == Some(key.as_str())
|
|
})
|
|
.ok_or_else(|| format!("legacy visibility target `{key}` no longer exists"))?;
|
|
let actor_id = source
|
|
.actor_id
|
|
.clone()
|
|
.ok_or_else(|| format!("legacy visibility target `{key}` has no ActorId"))?;
|
|
let target = PrefabActorPath::direct(actor_id);
|
|
let base_component_ron = source
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == component_type)
|
|
.map(|component| component.ron.clone());
|
|
overrides.components.retain(|candidate| {
|
|
!(candidate.target == target && candidate.component_type == component_type)
|
|
});
|
|
overrides.components.push(PrefabComponentOverride {
|
|
target,
|
|
component_type: component_type.clone(),
|
|
base_component_ron,
|
|
value_component_ron: Some(
|
|
ron::to_string(&EditorVisibility { visible }).map_err(|error| error.to_string())?,
|
|
),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_property_to_document(
|
|
document: &mut SceneDocument,
|
|
property: &PrefabPropertyOverride,
|
|
) -> Result<(), String> {
|
|
let target = SceneEntityId::ActorId(property.target.actor_id.clone());
|
|
let source = document
|
|
.find_entity(&target)
|
|
.and_then(|entity| {
|
|
entity
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == property.component_type)
|
|
})
|
|
.cloned()
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"source actor `{}` is missing component `{}`",
|
|
property.target.actor_id, property.component_type
|
|
)
|
|
})?;
|
|
let ron = if property.component_type == std::any::type_name::<Transform>() {
|
|
apply_transform_property(&source.ron, property)?
|
|
} else if property.component_type == std::any::type_name::<MaterialDesc>() {
|
|
apply_material_property(&source.ron, property)?
|
|
} else {
|
|
return Err(format!(
|
|
"property apply does not support component `{}`",
|
|
property.component_type
|
|
));
|
|
};
|
|
document.apply_patch(ScenePatch::SetComponent {
|
|
target,
|
|
component: SceneComponentBlob::from_ron(property.component_type.clone(), ron)?,
|
|
})
|
|
}
|
|
|
|
fn apply_transform_property(
|
|
source_ron: &str,
|
|
property: &PrefabPropertyOverride,
|
|
) -> Result<String, String> {
|
|
let mut source: Transform = ron::from_str(source_ron).map_err(|error| error.to_string())?;
|
|
let base: Transform =
|
|
ron::from_str(&property.base_component_ron).map_err(|error| error.to_string())?;
|
|
let value: Transform =
|
|
ron::from_str(&property.value_component_ron).map_err(|error| error.to_string())?;
|
|
let conflict = match property.property_path.as_str() {
|
|
"translation" => {
|
|
let conflict = source.translation != base.translation;
|
|
source.translation = value.translation;
|
|
conflict
|
|
}
|
|
"rotation" => {
|
|
let conflict = source.rotation != base.rotation;
|
|
source.rotation = value.rotation;
|
|
conflict
|
|
}
|
|
"scale" => {
|
|
let conflict = source.scale != base.scale;
|
|
source.scale = value.scale;
|
|
conflict
|
|
}
|
|
path => return Err(format!("unsupported Transform property `{path}`")),
|
|
};
|
|
if conflict {
|
|
return Err(format!(
|
|
"conflict: source Transform.{} changed since the override was created",
|
|
property.property_path
|
|
));
|
|
}
|
|
ron::to_string(&source).map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn apply_material_property(
|
|
source_ron: &str,
|
|
property: &PrefabPropertyOverride,
|
|
) -> Result<String, String> {
|
|
let mut source: MaterialDesc = ron::from_str(source_ron).map_err(|error| error.to_string())?;
|
|
let base: MaterialDesc =
|
|
ron::from_str(&property.base_component_ron).map_err(|error| error.to_string())?;
|
|
let value: MaterialDesc =
|
|
ron::from_str(&property.value_component_ron).map_err(|error| error.to_string())?;
|
|
let conflict = match property.property_path.as_str() {
|
|
"base_color" => {
|
|
let conflict = source.base_color != base.base_color;
|
|
source.base_color = value.base_color;
|
|
conflict
|
|
}
|
|
"metallic" => {
|
|
let conflict = source.metallic != base.metallic;
|
|
source.metallic = value.metallic;
|
|
conflict
|
|
}
|
|
"roughness" => {
|
|
let conflict = source.roughness != base.roughness;
|
|
source.roughness = value.roughness;
|
|
conflict
|
|
}
|
|
path => return Err(format!("unsupported MaterialDesc property `{path}`")),
|
|
};
|
|
if conflict {
|
|
return Err(format!(
|
|
"conflict: source MaterialDesc.{} changed since the override was created",
|
|
property.property_path
|
|
));
|
|
}
|
|
ron::to_string(&source).map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn apply_component_to_document(
|
|
document: &mut SceneDocument,
|
|
component: &PrefabComponentOverride,
|
|
) -> Result<(), String> {
|
|
let target = SceneEntityId::ActorId(component.target.actor_id.clone());
|
|
let current = document.find_entity(&target).and_then(|entity| {
|
|
entity
|
|
.components
|
|
.iter()
|
|
.find(|candidate| candidate.type_name == component.component_type)
|
|
});
|
|
match (&component.base_component_ron, current) {
|
|
(None, Some(_)) => {
|
|
return Err(format!(
|
|
"conflict: source actor `{}` gained component `{}`",
|
|
component.target.actor_id, component.component_type
|
|
));
|
|
}
|
|
(Some(base), Some(current))
|
|
if !component_values_equal(&component.component_type, ¤t.ron, base) =>
|
|
{
|
|
return Err(format!(
|
|
"conflict: source component `{}` changed",
|
|
component.component_type
|
|
));
|
|
}
|
|
(Some(_), None) => {
|
|
return Err(format!(
|
|
"conflict: source component `{}` was removed",
|
|
component.component_type
|
|
));
|
|
}
|
|
_ => {}
|
|
}
|
|
match &component.value_component_ron {
|
|
Some(value) => document.apply_patch(ScenePatch::SetComponent {
|
|
target,
|
|
component: SceneComponentBlob::from_ron(
|
|
component.component_type.clone(),
|
|
value.clone(),
|
|
)?,
|
|
}),
|
|
None => document.apply_patch(ScenePatch::RemoveComponent {
|
|
target,
|
|
component_type: component.component_type.clone(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn component_values_equal(component_type: &str, left: &str, right: &str) -> bool {
|
|
if component_type == std::any::type_name::<Transform>() {
|
|
return ron::from_str::<Transform>(left).ok() == ron::from_str::<Transform>(right).ok();
|
|
}
|
|
if component_type == std::any::type_name::<MaterialDesc>() {
|
|
return ron::from_str::<MaterialDesc>(left).ok()
|
|
== ron::from_str::<MaterialDesc>(right).ok();
|
|
}
|
|
ron::from_str::<ron::Value>(left).ok() == ron::from_str::<ron::Value>(right).ok()
|
|
}
|
|
|
|
fn apply_structural_to_document(
|
|
document: &mut SceneDocument,
|
|
structural: &PrefabStructuralOverride,
|
|
) -> Result<(), String> {
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild { target, .. } => {
|
|
document.apply_patch(ScenePatch::DeleteEntity {
|
|
target: SceneEntityId::ActorId(target.actor_id.clone()),
|
|
})
|
|
}
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
new_parent,
|
|
local_transform_ron,
|
|
..
|
|
} => {
|
|
if new_parent
|
|
.as_ref()
|
|
.is_some_and(|parent| !parent.instance_chain.is_empty())
|
|
{
|
|
return Err("cannot apply a cross-layer parent to this source".to_string());
|
|
}
|
|
if document_reparent_would_cycle(document, target, new_parent.as_ref()) {
|
|
return Err(format!(
|
|
"reparenting `{}` would create a hierarchy cycle",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
document.apply_patch(ScenePatch::Reparent {
|
|
target: SceneEntityId::ActorId(target.actor_id.clone()),
|
|
parent_actor_id: new_parent.as_ref().map(|parent| parent.actor_id.clone()),
|
|
})?;
|
|
if let Some(transform_ron) = local_transform_ron {
|
|
document.apply_patch(ScenePatch::SetComponent {
|
|
target: SceneEntityId::ActorId(target.actor_id.clone()),
|
|
component: SceneComponentBlob::from_ron(
|
|
std::any::type_name::<Transform>(),
|
|
transform_ron.clone(),
|
|
)?,
|
|
})?;
|
|
}
|
|
Ok(())
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { .. } => {
|
|
Err("local child attachment must be converted before source apply".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn document_reparent_would_cycle(
|
|
document: &SceneDocument,
|
|
target: &PrefabActorPath,
|
|
new_parent: Option<&PrefabActorPath>,
|
|
) -> bool {
|
|
let Some(new_parent) = new_parent else {
|
|
return false;
|
|
};
|
|
let mut current = Some(new_parent.actor_id.as_str());
|
|
let mut seen = HashSet::new();
|
|
while let Some(actor_id) = current {
|
|
if actor_id == target.actor_id || !seen.insert(actor_id.to_string()) {
|
|
return true;
|
|
}
|
|
current = document
|
|
.find_entity(&SceneEntityId::ActorId(actor_id.to_string()))
|
|
.and_then(|entity| entity.parent_actor_id.as_deref());
|
|
}
|
|
false
|
|
}
|
|
|
|
fn validate_structural_apply_base(
|
|
project_root: &Path,
|
|
instance: &PrefabInstance,
|
|
structural: &PrefabStructuralOverride,
|
|
) -> Result<(), String> {
|
|
let target = structural_target(structural);
|
|
let document = source_document_for_target(project_root, instance, target)?;
|
|
let _source = document
|
|
.find_entity(&SceneEntityId::ActorId(target.actor_id.clone()))
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"conflict: source actor `{}` no longer exists",
|
|
actor_path_key(target)
|
|
)
|
|
})?;
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision,
|
|
..
|
|
} => {
|
|
let current = prefab_source_revision(project_root, &instance.source_path)?;
|
|
if current != *base_subtree_revision {
|
|
return Err(format!(
|
|
"conflict: source subtree `{}` changed since removal was authored",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
}
|
|
PrefabStructuralOverride::ReparentChild {
|
|
base_parent,
|
|
new_parent,
|
|
base_transform_ron,
|
|
..
|
|
} => {
|
|
let current_parent = source_parent_for_target(project_root, instance, target)?;
|
|
if current_parent != *base_parent {
|
|
return Err(format!(
|
|
"conflict: source parent for `{}` changed",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
if let Some(base) = base_transform_ron {
|
|
let current = source_component_for_target(
|
|
project_root,
|
|
instance,
|
|
target,
|
|
std::any::type_name::<Transform>(),
|
|
)?
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"conflict: source transform for `{}` was removed",
|
|
actor_path_key(target)
|
|
)
|
|
})?;
|
|
if !component_values_equal(std::any::type_name::<Transform>(), ¤t.ron, base) {
|
|
return Err(format!(
|
|
"conflict: source transform for `{}` changed",
|
|
actor_path_key(target)
|
|
));
|
|
}
|
|
}
|
|
if let Some(parent) = new_parent {
|
|
if parent.instance_chain != target.instance_chain
|
|
|| source_parent_for_target(project_root, instance, parent).is_err()
|
|
{
|
|
return Err(format!(
|
|
"conflict: destination parent `{}` is unavailable",
|
|
actor_path_key(parent)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { .. } => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn transfer_property_to_nested_layer(
|
|
document: &mut SceneDocument,
|
|
mut property: PrefabPropertyOverride,
|
|
project_root: &Path,
|
|
) -> Result<(), String> {
|
|
let anchor = property.target.instance_chain.remove(0);
|
|
update_nested_instance(document, &anchor, project_root, move |overrides, _| {
|
|
merge_property_override(overrides, property);
|
|
})
|
|
}
|
|
|
|
fn transfer_component_to_nested_layer(
|
|
document: &mut SceneDocument,
|
|
mut component: PrefabComponentOverride,
|
|
project_root: &Path,
|
|
) -> Result<(), String> {
|
|
let anchor = component.target.instance_chain.remove(0);
|
|
update_nested_instance(document, &anchor, project_root, move |overrides, _| {
|
|
merge_component_override(overrides, component);
|
|
})
|
|
}
|
|
|
|
fn transfer_structural_to_nested_layer(
|
|
document: &mut SceneDocument,
|
|
mut structural: PrefabStructuralOverride,
|
|
project_root: &Path,
|
|
) -> Result<(), String> {
|
|
let anchor = structural_target(&structural)
|
|
.instance_chain
|
|
.first()
|
|
.cloned()
|
|
.ok_or_else(|| "nested structural override has no instance path".to_string())?;
|
|
trim_structural_instance_prefix(&mut structural, &anchor)?;
|
|
update_nested_instance(
|
|
document,
|
|
&anchor,
|
|
project_root,
|
|
move |overrides, nested_instance| {
|
|
let base_revision =
|
|
prefab_source_revision(project_root, &nested_instance.source_path).ok();
|
|
merge_structural_override(overrides, structural, base_revision);
|
|
},
|
|
)
|
|
}
|
|
|
|
fn merge_property_override(overrides: &mut PrefabOverrides, mut property: PrefabPropertyOverride) {
|
|
if let Some(existing) = overrides.properties.iter().find(|candidate| {
|
|
candidate.target == property.target
|
|
&& candidate.component_type == property.component_type
|
|
&& candidate.property_path == property.property_path
|
|
}) {
|
|
property.base_component_ron = existing.base_component_ron.clone();
|
|
}
|
|
overrides.properties.retain(|candidate| {
|
|
!(candidate.target == property.target
|
|
&& candidate.component_type == property.component_type
|
|
&& candidate.property_path == property.property_path)
|
|
});
|
|
overrides.properties.push(property);
|
|
}
|
|
|
|
fn merge_component_override(
|
|
overrides: &mut PrefabOverrides,
|
|
mut component: PrefabComponentOverride,
|
|
) {
|
|
if let Some(existing) = overrides.components.iter().find(|candidate| {
|
|
candidate.target == component.target && candidate.component_type == component.component_type
|
|
}) {
|
|
component.base_component_ron = existing.base_component_ron.clone();
|
|
}
|
|
overrides.components.retain(|candidate| {
|
|
!(candidate.target == component.target
|
|
&& candidate.component_type == component.component_type)
|
|
});
|
|
overrides.components.push(component);
|
|
}
|
|
|
|
fn merge_structural_override(
|
|
overrides: &mut PrefabOverrides,
|
|
mut structural: PrefabStructuralOverride,
|
|
base_revision: Option<String>,
|
|
) {
|
|
let target = structural_target(&structural).clone();
|
|
let existing = overrides
|
|
.structural
|
|
.iter()
|
|
.find(|candidate| {
|
|
std::mem::discriminant(*candidate) == std::mem::discriminant(&structural)
|
|
&& structural_target(candidate) == &target
|
|
})
|
|
.cloned();
|
|
match (&mut structural, existing) {
|
|
(
|
|
PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision,
|
|
..
|
|
},
|
|
Some(PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision: existing,
|
|
..
|
|
}),
|
|
) => *base_subtree_revision = existing,
|
|
(
|
|
PrefabStructuralOverride::ReparentChild {
|
|
base_parent,
|
|
base_transform_ron,
|
|
..
|
|
},
|
|
Some(PrefabStructuralOverride::ReparentChild {
|
|
base_parent: existing_parent,
|
|
base_transform_ron: existing_transform,
|
|
..
|
|
}),
|
|
) => {
|
|
*base_parent = existing_parent;
|
|
*base_transform_ron = existing_transform;
|
|
}
|
|
(
|
|
PrefabStructuralOverride::RemoveChild {
|
|
base_subtree_revision,
|
|
..
|
|
},
|
|
None,
|
|
) => {
|
|
if let Some(revision) = base_revision {
|
|
*base_subtree_revision = revision;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
overrides.structural.retain(|candidate| {
|
|
std::mem::discriminant(candidate) != std::mem::discriminant(&structural)
|
|
|| structural_target(candidate) != &target
|
|
});
|
|
overrides.structural.push(structural);
|
|
}
|
|
|
|
pub fn fold_nested_overrides_for_unpack(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
) -> Result<(), String> {
|
|
let instance = world
|
|
.get::<PrefabInstance>(instance_root)
|
|
.cloned()
|
|
.ok_or_else(|| "prefab instance is missing".to_string())?;
|
|
let outer = try_read_overrides(&instance)?;
|
|
let nested_entities: Vec<(Entity, String, PrefabInstance)> = world
|
|
.query::<(Entity, &HydratedPrefabMember, &ActorId, &PrefabInstance)>()
|
|
.iter(world)
|
|
.filter(|(_, owner, _, _)| owner.instance_root == instance_root)
|
|
.map(|(entity, _, actor_id, nested)| (entity, actor_id.0.clone(), nested.clone()))
|
|
.collect();
|
|
let project_root = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root));
|
|
let mut updates = Vec::new();
|
|
for (entity, anchor_actor_id, mut nested_instance) in nested_entities {
|
|
let mut nested_overrides = try_read_overrides(&nested_instance)?;
|
|
let mut changed = false;
|
|
for mut property in outer
|
|
.properties
|
|
.iter()
|
|
.filter(|property| property.target.instance_chain.first() == Some(&anchor_actor_id))
|
|
.cloned()
|
|
{
|
|
property.target.instance_chain.remove(0);
|
|
merge_property_override(&mut nested_overrides, property);
|
|
changed = true;
|
|
}
|
|
for mut component in outer
|
|
.components
|
|
.iter()
|
|
.filter(|component| component.target.instance_chain.first() == Some(&anchor_actor_id))
|
|
.cloned()
|
|
{
|
|
component.target.instance_chain.remove(0);
|
|
merge_component_override(&mut nested_overrides, component);
|
|
changed = true;
|
|
}
|
|
for mut structural in outer
|
|
.structural
|
|
.iter()
|
|
.filter(|structural| {
|
|
structural_target(structural).instance_chain.first() == Some(&anchor_actor_id)
|
|
})
|
|
.cloned()
|
|
{
|
|
trim_structural_instance_prefix(&mut structural, &anchor_actor_id)?;
|
|
let base_revision = project_root
|
|
.as_ref()
|
|
.and_then(|root| prefab_source_revision(root, &nested_instance.source_path).ok());
|
|
merge_structural_override(&mut nested_overrides, structural, base_revision);
|
|
changed = true;
|
|
}
|
|
if !changed {
|
|
continue;
|
|
}
|
|
if let Some(root) = project_root.as_ref() {
|
|
nested_overrides.source_revision =
|
|
prefab_source_revision(root, &nested_instance.source_path).ok();
|
|
}
|
|
nested_instance.overrides_ron = encode_prefab_overrides(&nested_overrides)?;
|
|
updates.push((entity, nested_instance));
|
|
}
|
|
for (entity, nested_instance) in updates {
|
|
world.entity_mut(entity).insert(nested_instance);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn trim_structural_instance_prefix(
|
|
structural: &mut PrefabStructuralOverride,
|
|
prefix: &str,
|
|
) -> Result<(), String> {
|
|
let trim = |path: &mut PrefabActorPath| -> Result<(), String> {
|
|
if path.instance_chain.first().map(String::as_str) != Some(prefix) {
|
|
return Err("structural override crosses an instance layer".to_string());
|
|
}
|
|
path.instance_chain.remove(0);
|
|
Ok(())
|
|
};
|
|
match structural {
|
|
PrefabStructuralOverride::RemoveChild { target, .. } => trim(target),
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target,
|
|
base_parent,
|
|
new_parent,
|
|
..
|
|
} => {
|
|
trim(target)?;
|
|
if let Some(parent) = base_parent {
|
|
trim(parent)?;
|
|
}
|
|
if let Some(parent) = new_parent {
|
|
trim(parent)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
PrefabStructuralOverride::AttachLocalChild { parent, .. } => trim(parent),
|
|
}
|
|
}
|
|
|
|
fn update_nested_instance(
|
|
document: &mut SceneDocument,
|
|
anchor_actor_id: &str,
|
|
project_root: &Path,
|
|
update: impl FnOnce(&mut PrefabOverrides, &PrefabInstance),
|
|
) -> Result<(), String> {
|
|
let target = SceneEntityId::ActorId(anchor_actor_id.to_string());
|
|
let component = document
|
|
.find_entity(&target)
|
|
.and_then(|entity| {
|
|
entity
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == std::any::type_name::<PrefabInstance>())
|
|
})
|
|
.cloned()
|
|
.ok_or_else(|| format!("nested prefab anchor `{anchor_actor_id}` is missing"))?;
|
|
let mut instance: PrefabInstance =
|
|
ron::from_str(&component.ron).map_err(|error| error.to_string())?;
|
|
let mut overrides = decode_prefab_overrides(&instance)?;
|
|
update(&mut overrides, &instance);
|
|
overrides.source_revision = prefab_source_revision(project_root, &instance.source_path).ok();
|
|
instance.overrides_ron = encode_prefab_overrides(&overrides)?;
|
|
document.apply_patch(ScenePatch::SetComponent {
|
|
target,
|
|
component: SceneComponentBlob::from_ron(
|
|
std::any::type_name::<PrefabInstance>(),
|
|
ron::to_string(&instance).map_err(|error| error.to_string())?,
|
|
)?,
|
|
})
|
|
}
|
|
|
|
pub fn remove_prefab_source_child(world: &mut World, member: Entity) -> bool {
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, member) else {
|
|
return false;
|
|
};
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return false;
|
|
};
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return false;
|
|
};
|
|
overrides
|
|
.properties
|
|
.retain(|candidate| candidate.target != target);
|
|
overrides
|
|
.components
|
|
.retain(|candidate| candidate.target != target);
|
|
overrides
|
|
.structural
|
|
.retain(|candidate| structural_target(candidate) != &target);
|
|
let base_subtree_revision = current_source_revision(world, &instance).unwrap_or_default();
|
|
overrides
|
|
.structural
|
|
.push(PrefabStructuralOverride::RemoveChild {
|
|
target,
|
|
base_subtree_revision,
|
|
});
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
if let Ok(entity_mut) = world.get_entity_mut(member) {
|
|
entity_mut.despawn();
|
|
}
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
true
|
|
}
|
|
|
|
pub fn reparent_prefab_source_child(
|
|
world: &mut World,
|
|
member: Entity,
|
|
new_parent: Option<Entity>,
|
|
) -> bool {
|
|
reparent_prefab_source_children(world, &[member], new_parent)
|
|
}
|
|
|
|
pub fn reparent_prefab_source_children(
|
|
world: &mut World,
|
|
members: &[Entity],
|
|
new_parent: Option<Entity>,
|
|
) -> bool {
|
|
let Some(first) = members.first().copied() else {
|
|
return false;
|
|
};
|
|
let Some((instance_root, first_target)) = prefab_member_override_target(world, first) else {
|
|
return false;
|
|
};
|
|
let new_parent_target = match new_parent {
|
|
Some(parent) => {
|
|
let Some((parent_root, parent_target)) = prefab_member_override_target(world, parent)
|
|
else {
|
|
return false;
|
|
};
|
|
if parent_root != instance_root
|
|
|| parent_target.instance_chain != first_target.instance_chain
|
|
{
|
|
return false;
|
|
}
|
|
Some(parent_target)
|
|
}
|
|
None => None,
|
|
};
|
|
let runtime_parent = new_parent.unwrap_or(instance_root);
|
|
let mut plans = Vec::with_capacity(members.len());
|
|
for member in members.iter().copied() {
|
|
let Some((owner, target)) = prefab_member_override_target(world, member) else {
|
|
return false;
|
|
};
|
|
if owner != instance_root || target.instance_chain != first_target.instance_chain {
|
|
return false;
|
|
}
|
|
if hierarchy_would_cycle(world, member, runtime_parent) {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"Prefab reparent blocked: hierarchy cycle".into();
|
|
return false;
|
|
}
|
|
let new_transform = world
|
|
.get::<GlobalTransform>(member)
|
|
.map(
|
|
|global| match world.get::<GlobalTransform>(runtime_parent) {
|
|
Some(parent_global) => global.reparented_to(parent_global),
|
|
None => global.compute_transform(),
|
|
},
|
|
)
|
|
.or_else(|| world.get::<Transform>(member).copied());
|
|
let local_transform_ron = new_transform
|
|
.as_ref()
|
|
.and_then(|transform| ron::to_string(transform).ok());
|
|
plans.push((member, target, new_transform, local_transform_ron));
|
|
}
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return false;
|
|
};
|
|
let old = capture_prefab_edit_state(world, instance_root);
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return false;
|
|
};
|
|
let project_root = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root));
|
|
for (member, target, _, local_transform_ron) in &plans {
|
|
let existing_base = overrides
|
|
.structural
|
|
.iter()
|
|
.find_map(|candidate| match candidate {
|
|
PrefabStructuralOverride::ReparentChild {
|
|
target: existing,
|
|
base_parent,
|
|
base_transform_ron,
|
|
..
|
|
} if existing == target => Some((base_parent.clone(), base_transform_ron.clone())),
|
|
_ => None,
|
|
});
|
|
let (base_parent, base_transform_ron) = existing_base.unwrap_or_else(|| {
|
|
let source_parent = project_root
|
|
.as_ref()
|
|
.and_then(|root| source_parent_for_target(root, &instance, target).ok());
|
|
let base_parent = source_parent.unwrap_or_else(|| {
|
|
world
|
|
.get::<ChildOf>(*member)
|
|
.map(ChildOf::parent)
|
|
.and_then(|parent| prefab_member_override_target(world, parent))
|
|
.filter(|(root, path)| {
|
|
*root == instance_root && path.instance_chain == target.instance_chain
|
|
})
|
|
.map(|(_, path)| path)
|
|
});
|
|
let base_transform = project_root
|
|
.as_ref()
|
|
.and_then(|root| {
|
|
source_component_for_target(
|
|
root,
|
|
&instance,
|
|
target,
|
|
std::any::type_name::<Transform>(),
|
|
)
|
|
.ok()
|
|
.flatten()
|
|
.map(|component| component.ron)
|
|
})
|
|
.or_else(|| {
|
|
world
|
|
.get::<Transform>(*member)
|
|
.and_then(|transform| ron::to_string(transform).ok())
|
|
});
|
|
(base_parent, base_transform)
|
|
});
|
|
overrides.structural.retain(|candidate| {
|
|
!matches!(candidate, PrefabStructuralOverride::ReparentChild { target: existing, .. } if existing == target)
|
|
});
|
|
if new_parent_target != base_parent {
|
|
overrides
|
|
.structural
|
|
.push(PrefabStructuralOverride::ReparentChild {
|
|
target: target.clone(),
|
|
base_parent,
|
|
new_parent: new_parent_target.clone(),
|
|
base_transform_ron,
|
|
local_transform_ron: local_transform_ron.clone(),
|
|
});
|
|
}
|
|
}
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, instance_root, &overrides);
|
|
for (member, _, new_transform, _) in plans {
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(member) {
|
|
entity_mut.insert(ChildOf(runtime_parent));
|
|
if let Some(transform) = new_transform {
|
|
entity_mut.insert(transform);
|
|
}
|
|
}
|
|
}
|
|
record_prefab_edit_state(world, instance_root, old);
|
|
true
|
|
}
|
|
|
|
fn hierarchy_would_cycle(world: &World, entity: Entity, parent: Entity) -> bool {
|
|
let mut current = Some(parent);
|
|
let mut seen = HashSet::new();
|
|
while let Some(candidate) = current {
|
|
if candidate == entity || !seen.insert(candidate) {
|
|
return true;
|
|
}
|
|
current = world.get::<ChildOf>(candidate).map(ChildOf::parent);
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn revert_child_visibility_overrides(world: &mut World, entity: Entity) {
|
|
let old = capture_prefab_edit_state(world, entity);
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(mut overrides) = read_overrides_for_edit(world, &instance) else {
|
|
return;
|
|
};
|
|
if let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
{
|
|
let _ = promote_legacy_visibility_overrides(&project_root, &instance, &mut overrides);
|
|
}
|
|
let component_type = std::any::type_name::<EditorVisibility>();
|
|
let restorations: Vec<_> = overrides
|
|
.components
|
|
.iter()
|
|
.filter(|component| component.component_type == component_type)
|
|
.map(|component| {
|
|
(
|
|
component.target.clone(),
|
|
component.base_component_ron.clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
for (target, base) in restorations {
|
|
let Some(member) = prefab_member_for_target(world, entity, &target) else {
|
|
continue;
|
|
};
|
|
if let Some(value) = base.and_then(|ron| ron::from_str::<EditorVisibility>(&ron).ok()) {
|
|
world.entity_mut(member).insert(value);
|
|
} else {
|
|
world.entity_mut(member).remove::<EditorVisibility>();
|
|
}
|
|
}
|
|
overrides.child_visibility.clear();
|
|
overrides
|
|
.components
|
|
.retain(|component| component.component_type != component_type);
|
|
track_current_source_revision(world, &instance, &mut overrides);
|
|
write_overrides(world, entity, &overrides);
|
|
record_prefab_edit_state(world, entity, old);
|
|
}
|
|
|
|
pub fn apply_stored_overrides(world: &mut World, entity: Entity) {
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Ok(overrides) = try_read_overrides(&instance) else {
|
|
return;
|
|
};
|
|
for child in prefab_instance_members(world, entity) {
|
|
let is_direct = prefab_member_override_target(world, child)
|
|
.is_some_and(|(root, target)| root == entity && target.instance_chain.is_empty());
|
|
if !is_direct {
|
|
continue;
|
|
}
|
|
let Some(actor_id) = world.get::<ActorId>(child).map(|id| id.0.as_str()) else {
|
|
continue;
|
|
};
|
|
let legacy_name = world.get::<Name>(child).map(|name| name.as_str());
|
|
if let Some(&visible) = overrides
|
|
.child_visibility
|
|
.get(actor_id)
|
|
.or_else(|| legacy_name.and_then(|name| overrides.child_visibility.get(name)))
|
|
{
|
|
if let Ok(mut entity_mut) = world.get_entity_mut(child) {
|
|
entity_mut.insert(EditorVisibility { visible });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn prefab_member_for_target(
|
|
world: &mut World,
|
|
instance_root: Entity,
|
|
target: &PrefabActorPath,
|
|
) -> Option<Entity> {
|
|
let candidates: Vec<Entity> = world
|
|
.query_filtered::<Entity, With<HydratedPrefabMember>>()
|
|
.iter(world)
|
|
.collect();
|
|
candidates.into_iter().find(|candidate| {
|
|
prefab_member_override_target(world, *candidate).is_some_and(|(root, candidate_target)| {
|
|
root == instance_root && candidate_target == *target
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn invalidate_prefab_health_on_ready(
|
|
ready: On<WorldInstanceReady>,
|
|
mut health_cache: Option<ResMut<PrefabHealthCache>>,
|
|
) {
|
|
if let Some(cache) = health_cache.as_deref_mut() {
|
|
cache.entries.remove(&ready.entity);
|
|
}
|
|
}
|
|
|
|
pub fn guard_prefab_reimports(
|
|
mut events: MessageReader<AssetEvent<DynamicWorld>>,
|
|
mut failures: MessageReader<bevy::asset::AssetLoadFailedEvent<DynamicWorld>>,
|
|
roots: Query<(Entity, &DynamicWorldRoot, &PrefabInstance)>,
|
|
workspace: Res<crate::project_io::ProjectWorkspace>,
|
|
mut commands: Commands,
|
|
) {
|
|
let mut changed: std::collections::HashSet<_> = events
|
|
.read()
|
|
.filter_map(|event| match event {
|
|
AssetEvent::Modified { id }
|
|
| AssetEvent::Removed { id }
|
|
| AssetEvent::LoadedWithDependencies { id } => Some(*id),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
let failed: HashMap<_, _> = failures
|
|
.read()
|
|
.map(|failure| {
|
|
(
|
|
failure.id,
|
|
format!("could not load {}: {}", failure.path, failure.error),
|
|
)
|
|
})
|
|
.collect();
|
|
changed.extend(failed.keys().copied());
|
|
if changed.is_empty() {
|
|
return;
|
|
}
|
|
let project_root = Path::new(&workspace.root);
|
|
for (entity, dynamic_root, instance) in &roots {
|
|
if !changed.contains(&dynamic_root.id()) {
|
|
continue;
|
|
}
|
|
if let Some(reason) = failed.get(&dynamic_root.id()) {
|
|
let reason = reason.clone();
|
|
commands.queue(move |world: &mut World| {
|
|
block_prefab_hydration(world, entity, reason);
|
|
});
|
|
continue;
|
|
}
|
|
match scene::validate_prefab_graph(&project_root.join(&instance.source_path), project_root)
|
|
{
|
|
Ok(()) => {
|
|
commands
|
|
.entity(entity)
|
|
.remove::<shared::PrefabHydrationBlocked>();
|
|
}
|
|
Err(reason) => {
|
|
commands.queue(move |world: &mut World| {
|
|
block_prefab_hydration(world, entity, reason);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn block_prefab_hydration(world: &mut World, entity: Entity, reason: String) {
|
|
let mut entity = entity;
|
|
let mut seen = HashSet::new();
|
|
while seen.insert(entity) {
|
|
let Some(owner) = world
|
|
.get::<HydratedPrefabMember>(entity)
|
|
.map(|member| member.instance_root)
|
|
else {
|
|
break;
|
|
};
|
|
entity = owner;
|
|
}
|
|
|
|
// Nested instances are owned by generated members of their parent instance. Walk that
|
|
// ownership graph so an invalid reload cannot leave an unregistered nested tree behind.
|
|
let ownership: Vec<(Entity, Entity)> = world
|
|
.query::<(Entity, &HydratedPrefabMember)>()
|
|
.iter(world)
|
|
.map(|(member, owner)| (member, owner.instance_root))
|
|
.collect();
|
|
let mut owned_roots = HashSet::from([entity]);
|
|
let mut changed = true;
|
|
while changed {
|
|
changed = false;
|
|
for (member, owner) in &ownership {
|
|
if owned_roots.contains(owner) && owned_roots.insert(*member) {
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
owned_roots.remove(&entity);
|
|
for member in owned_roots {
|
|
if let Ok(entity_mut) = world.get_entity_mut(member) {
|
|
entity_mut.despawn();
|
|
}
|
|
}
|
|
|
|
if let Ok(mut root) = world.get_entity_mut(entity) {
|
|
// Remove the asset root while WorldInstance is still present so Bevy unregisters the
|
|
// instance, then remove the stale ID before a repaired source is hydrated.
|
|
root.insert(shared::PrefabHydrationBlocked { reason })
|
|
.remove::<HydratedPrefabReady>()
|
|
.remove::<DynamicWorldRoot>()
|
|
.remove::<WorldInstance>();
|
|
}
|
|
if let Some(mut cache) = world.get_resource_mut::<PrefabHealthCache>() {
|
|
cache.entries.remove(&entity);
|
|
}
|
|
}
|
|
|
|
fn retry_prefab_source(world: &mut World, entity: Entity) {
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let Some(project_root) = world
|
|
.get_resource::<crate::project_io::ProjectWorkspace>()
|
|
.map(|workspace| PathBuf::from(&workspace.root))
|
|
else {
|
|
return;
|
|
};
|
|
if let Err(error) =
|
|
scene::validate_prefab_graph(&project_root.join(&instance.source_path), &project_root)
|
|
{
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Prefab retry failed: {error}");
|
|
return;
|
|
}
|
|
|
|
block_prefab_hydration(world, entity, "restarting prefab hydration".to_string());
|
|
if let Ok(mut root) = world.get_entity_mut(entity) {
|
|
root.remove::<shared::PrefabHydrationBlocked>().insert((
|
|
instance.clone(),
|
|
PrefabRef::new(instance.source_path.clone()),
|
|
));
|
|
}
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
"Prefab source queued for reload".into();
|
|
}
|
|
|
|
pub fn prefab_member_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|
let Some((instance_root, target)) = prefab_member_override_target(world, entity) else {
|
|
ui.label(egui::RichText::new("Prefab target unavailable").color(ERROR));
|
|
return;
|
|
};
|
|
let Some(instance) = world.get::<PrefabInstance>(instance_root).cloned() else {
|
|
return;
|
|
};
|
|
let overrides = match try_read_overrides(&instance) {
|
|
Ok(overrides) => overrides,
|
|
Err(error) => {
|
|
ui.label(egui::RichText::new(error).color(ERROR));
|
|
ui.label("Repair the prefab instance from its root inspector before editing members.");
|
|
return;
|
|
}
|
|
};
|
|
let transform_type = std::any::type_name::<Transform>();
|
|
let material_type = std::any::type_name::<MaterialDesc>();
|
|
let property_active = |component_type: &str, property_path: &str| {
|
|
overrides.properties.iter().any(|candidate| {
|
|
candidate.target == target
|
|
&& candidate.component_type == component_type
|
|
&& candidate.property_path == property_path
|
|
})
|
|
};
|
|
let component_active = |component_type: &str| {
|
|
overrides.components.iter().any(|candidate| {
|
|
candidate.target == target && candidate.component_type == component_type
|
|
})
|
|
};
|
|
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label(
|
|
egui::RichText::new("Override layer")
|
|
.strong()
|
|
.color(WARNING),
|
|
);
|
|
ui.label(
|
|
egui::RichText::new(format!("{} / {}", instance.source_path, target.actor_id))
|
|
.color(TEXT_MUTED),
|
|
);
|
|
});
|
|
ui.add_space(4.0);
|
|
|
|
if let Some(mut transform) = world.get::<Transform>(entity).copied() {
|
|
ui.collapsing("Transform", |ui| {
|
|
let old_translation = transform.translation;
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Position");
|
|
ui.add(egui::DragValue::new(&mut transform.translation.x).speed(0.1));
|
|
ui.add(egui::DragValue::new(&mut transform.translation.y).speed(0.1));
|
|
ui.add(egui::DragValue::new(&mut transform.translation.z).speed(0.1));
|
|
if property_active(transform_type, "translation")
|
|
&& ui.small_button("Revert").clicked()
|
|
{
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("translation"),
|
|
);
|
|
}
|
|
if property_active(transform_type, "translation")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("translation"),
|
|
);
|
|
}
|
|
});
|
|
if transform.translation != old_translation {
|
|
set_prefab_transform_property_override(world, entity, "translation", transform);
|
|
}
|
|
|
|
let old_rotation = transform.rotation;
|
|
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
|
|
let mut degrees = Vec3::new(pitch.to_degrees(), yaw.to_degrees(), roll.to_degrees());
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Rotation");
|
|
ui.add(egui::DragValue::new(&mut degrees.x).speed(0.5));
|
|
ui.add(egui::DragValue::new(&mut degrees.y).speed(0.5));
|
|
ui.add(egui::DragValue::new(&mut degrees.z).speed(0.5));
|
|
if property_active(transform_type, "rotation")
|
|
&& ui.small_button("Revert").clicked()
|
|
{
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("rotation"),
|
|
);
|
|
}
|
|
if property_active(transform_type, "rotation")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("rotation"),
|
|
);
|
|
}
|
|
});
|
|
transform.rotation = Quat::from_euler(
|
|
EulerRot::YXZ,
|
|
degrees.y.to_radians(),
|
|
degrees.x.to_radians(),
|
|
degrees.z.to_radians(),
|
|
);
|
|
if transform.rotation != old_rotation {
|
|
set_prefab_transform_property_override(world, entity, "rotation", transform);
|
|
}
|
|
|
|
let old_scale = transform.scale;
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Scale");
|
|
ui.add(egui::DragValue::new(&mut transform.scale.x).speed(0.05));
|
|
ui.add(egui::DragValue::new(&mut transform.scale.y).speed(0.05));
|
|
ui.add(egui::DragValue::new(&mut transform.scale.z).speed(0.05));
|
|
if property_active(transform_type, "scale") && ui.small_button("Revert").clicked() {
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("scale"),
|
|
);
|
|
}
|
|
if property_active(transform_type, "scale")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(transform_type),
|
|
Some("scale"),
|
|
);
|
|
}
|
|
});
|
|
if transform.scale != old_scale {
|
|
set_prefab_transform_property_override(world, entity, "scale", transform);
|
|
}
|
|
});
|
|
}
|
|
|
|
ui.collapsing("Material", |ui| {
|
|
if let Some(mut material) = world.get::<MaterialDesc>(entity).cloned() {
|
|
let mut color = [
|
|
material.base_color.r,
|
|
material.base_color.g,
|
|
material.base_color.b,
|
|
material.base_color.a,
|
|
];
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Base color");
|
|
if ui.color_edit_button_rgba_unmultiplied(&mut color).changed() {
|
|
material.base_color.r = color[0];
|
|
material.base_color.g = color[1];
|
|
material.base_color.b = color[2];
|
|
material.base_color.a = color[3];
|
|
set_prefab_material_property_override(
|
|
world,
|
|
entity,
|
|
"base_color",
|
|
material.clone(),
|
|
);
|
|
}
|
|
if property_active(material_type, "base_color")
|
|
&& ui.small_button("Revert").clicked()
|
|
{
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("base_color"),
|
|
);
|
|
}
|
|
if property_active(material_type, "base_color")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("base_color"),
|
|
);
|
|
}
|
|
});
|
|
|
|
let old_metallic = material.metallic;
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Metallic");
|
|
ui.add(egui::Slider::new(&mut material.metallic, 0.0..=1.0));
|
|
if property_active(material_type, "metallic") && ui.small_button("Revert").clicked()
|
|
{
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("metallic"),
|
|
);
|
|
}
|
|
if property_active(material_type, "metallic")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("metallic"),
|
|
);
|
|
}
|
|
});
|
|
if material.metallic != old_metallic {
|
|
set_prefab_material_property_override(world, entity, "metallic", material.clone());
|
|
}
|
|
|
|
let old_roughness = material.roughness;
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label("Roughness");
|
|
ui.add(egui::Slider::new(&mut material.roughness, 0.0..=1.0));
|
|
if property_active(material_type, "roughness")
|
|
&& ui.small_button("Revert").clicked()
|
|
{
|
|
revert_prefab_override_scope(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("roughness"),
|
|
);
|
|
}
|
|
if property_active(material_type, "roughness")
|
|
&& ui.small_button("Apply to source").clicked()
|
|
{
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
Some("roughness"),
|
|
);
|
|
}
|
|
});
|
|
if material.roughness != old_roughness {
|
|
set_prefab_material_property_override(world, entity, "roughness", material);
|
|
}
|
|
|
|
if ui.small_button("Remove component").clicked() {
|
|
remove_prefab_component_override::<MaterialDesc>(world, entity);
|
|
}
|
|
if overrides.properties.iter().any(|candidate| {
|
|
candidate.target == target && candidate.component_type == material_type
|
|
}) || component_active(material_type)
|
|
{
|
|
ui.horizontal_wrapped(|ui| {
|
|
if ui.small_button("Apply component to source").clicked() {
|
|
apply_prefab_member_scope_to_source(
|
|
world,
|
|
entity,
|
|
Some(material_type),
|
|
None,
|
|
);
|
|
}
|
|
if ui.small_button("Revert component overrides").clicked() {
|
|
revert_prefab_override_scope(world, entity, Some(material_type), None);
|
|
}
|
|
});
|
|
}
|
|
} else if component_active(material_type) {
|
|
ui.horizontal_wrapped(|ui| {
|
|
if ui.button("Apply removal to source").clicked() {
|
|
apply_prefab_member_scope_to_source(world, entity, Some(material_type), None);
|
|
}
|
|
if ui.button("Restore source component").clicked() {
|
|
revert_prefab_override_scope(world, entity, Some(material_type), None);
|
|
}
|
|
});
|
|
} else if ui.button("Add material component").clicked() {
|
|
set_prefab_component_override(world, entity, MaterialDesc::default());
|
|
}
|
|
});
|
|
|
|
ui.separator();
|
|
ui.horizontal_wrapped(|ui| {
|
|
if ui.button("Apply actor to source").clicked() {
|
|
apply_prefab_member_scope_to_source(world, entity, None, None);
|
|
}
|
|
if ui.button("Revert actor overrides").clicked() {
|
|
revert_prefab_override_scope(world, entity, None, None);
|
|
}
|
|
if ui
|
|
.add(egui::Button::new("Remove source actor"))
|
|
.on_hover_text("Creates an undoable structural override")
|
|
.clicked()
|
|
{
|
|
remove_prefab_source_child(world, entity);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn prefab_instance_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|
let Some(instance) = world.get::<PrefabInstance>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let parsed_overrides = try_read_overrides(&instance);
|
|
let overrides_valid = parsed_overrides.is_ok();
|
|
let overrides = parsed_overrides.clone().unwrap_or_default();
|
|
let health = parsed_overrides.map_or_else(PrefabHealth::InvalidOverrides, |_| {
|
|
prefab_health(world, entity, &instance, &overrides)
|
|
});
|
|
|
|
let mut options = ComponentCardOptions::fixed(
|
|
shared::COMPONENT_PREFAB_INSTANCE,
|
|
"Prefab Instance",
|
|
icons::PACKAGE,
|
|
);
|
|
options.resettable = false;
|
|
let card = component_card_context(world, entity, options);
|
|
let card_response = component_card(ui, &card, |ui| {
|
|
property_row(ui, "Asset ID", |ui| {
|
|
ui.add(egui::Label::new(instance.asset_id.as_str()).truncate());
|
|
});
|
|
property_row(ui, "Source", |ui| {
|
|
ui.add(egui::Label::new(instance.source_path.as_str()).truncate());
|
|
});
|
|
property_row(ui, "Status", |ui| {
|
|
let (label, color, detail) = match &health {
|
|
PrefabHealth::Ready => ("Ready", SUCCESS, None),
|
|
PrefabHealth::Loading => (
|
|
"Loading source",
|
|
TEXT_MUTED,
|
|
Some("Override targets will be available when the prefab finishes loading"),
|
|
),
|
|
PrefabHealth::Untracked => (
|
|
"Base not tracked",
|
|
WARNING,
|
|
Some("Record the current source revision before production use"),
|
|
),
|
|
PrefabHealth::ChangedBase => (
|
|
"Base changed",
|
|
WARNING,
|
|
Some("The source file changed after these overrides were recorded"),
|
|
),
|
|
PrefabHealth::Conflict(stale) => (
|
|
"Override conflict",
|
|
ERROR,
|
|
Some(if stale.len() == 1 {
|
|
"One override conflicts with the current source value, hierarchy, or target"
|
|
} else {
|
|
"Multiple overrides conflict with current source values, hierarchy, or targets"
|
|
}),
|
|
),
|
|
PrefabHealth::Broken(error) => ("Broken source", ERROR, Some(error.as_str())),
|
|
PrefabHealth::InvalidOverrides(error) => {
|
|
("Invalid overrides", ERROR, Some(error.as_str()))
|
|
}
|
|
};
|
|
let response = ui.label(egui::RichText::new(label).strong().color(color));
|
|
if let Some(detail) = detail {
|
|
response.on_hover_text(detail);
|
|
}
|
|
});
|
|
|
|
ui.horizontal_wrapped(|ui| match &health {
|
|
PrefabHealth::Ready => {
|
|
if ui.small_button("Reload source").clicked() {
|
|
world.entity_mut(entity).insert((
|
|
instance.clone(),
|
|
PrefabRef::new(instance.source_path.clone()),
|
|
));
|
|
}
|
|
if ui.small_button("Relink...").clicked() {
|
|
relink_prefab_source(world, entity);
|
|
}
|
|
}
|
|
PrefabHealth::Untracked | PrefabHealth::ChangedBase => {
|
|
if ui.small_button("Accept current base").clicked() {
|
|
accept_current_source(world, entity);
|
|
}
|
|
if ui.small_button("Relink...").clicked() {
|
|
relink_prefab_source(world, entity);
|
|
}
|
|
}
|
|
PrefabHealth::Conflict(stale) => {
|
|
ui.label(
|
|
egui::RichText::new(format!("{} conflicts", stale.len())).color(TEXT_MUTED),
|
|
);
|
|
if ui.small_button("Keep overrides").clicked() {
|
|
keep_prefab_overrides(world, entity);
|
|
}
|
|
if ui.small_button("Take source").clicked() {
|
|
take_source_for_conflicts(world, entity);
|
|
}
|
|
if ui.small_button("Remove missing targets").clicked() {
|
|
remove_stale_visibility_overrides(world, entity);
|
|
}
|
|
if ui.small_button("Relink...").clicked() {
|
|
relink_prefab_source(world, entity);
|
|
}
|
|
}
|
|
PrefabHealth::Broken(_) => {
|
|
if ui.small_button("Retry source").clicked() {
|
|
retry_prefab_source(world, entity);
|
|
}
|
|
if ui.small_button("Relink source...").clicked() {
|
|
relink_prefab_source(world, entity);
|
|
}
|
|
}
|
|
PrefabHealth::Loading => {
|
|
ui.spinner();
|
|
}
|
|
PrefabHealth::InvalidOverrides(_) => {
|
|
if ui.small_button("Discard invalid data").clicked() {
|
|
discard_invalid_overrides(world, entity);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.add_enabled_ui(overrides_valid, |ui| {
|
|
let operation_count = overrides.properties.len()
|
|
+ overrides.components.len()
|
|
+ overrides.structural.len()
|
|
+ overrides.child_visibility.len();
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label(
|
|
egui::RichText::new(format!("{operation_count} override operations"))
|
|
.color(TEXT_MUTED),
|
|
);
|
|
if ui
|
|
.add_enabled(
|
|
operation_count > 0,
|
|
egui::Button::new("Apply all to source"),
|
|
)
|
|
.clicked()
|
|
{
|
|
apply_prefab_instance_overrides_to_source(world, entity);
|
|
}
|
|
if ui
|
|
.add_enabled(operation_count > 0, egui::Button::new("Revert all"))
|
|
.clicked()
|
|
{
|
|
revert_prefab_instance_overrides(world, entity);
|
|
}
|
|
});
|
|
if !overrides.structural.is_empty() {
|
|
let mut structural_action = None;
|
|
ui.collapsing("Structural overrides", |ui| {
|
|
for structural in &overrides.structural {
|
|
let target = structural_target(structural).clone();
|
|
let operation = match structural {
|
|
PrefabStructuralOverride::RemoveChild { .. } => "Removed",
|
|
PrefabStructuralOverride::ReparentChild { .. } => "Reparented",
|
|
PrefabStructuralOverride::AttachLocalChild { .. } => "Attached",
|
|
};
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.label(
|
|
egui::RichText::new(format!(
|
|
"{operation} {}",
|
|
actor_path_key(&target)
|
|
))
|
|
.color(TEXT_MUTED),
|
|
);
|
|
if ui.small_button("Apply actor").clicked() {
|
|
structural_action = Some((true, target.clone()));
|
|
}
|
|
if ui.small_button("Revert actor").clicked() {
|
|
structural_action = Some((false, target));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if let Some((apply, target)) = structural_action {
|
|
if apply {
|
|
apply_prefab_actor_path_to_source(world, entity, &target);
|
|
} else {
|
|
revert_prefab_actor_path(world, entity, &target);
|
|
}
|
|
}
|
|
}
|
|
ui.collapsing("Visibility overrides", |ui| {
|
|
let visibility_type = std::any::type_name::<EditorVisibility>();
|
|
let active = !overrides.child_visibility.is_empty()
|
|
|| overrides
|
|
.components
|
|
.iter()
|
|
.any(|component| component.component_type == visibility_type);
|
|
if active {
|
|
ui.label(egui::RichText::new("Overrides use stable nested actor paths").weak());
|
|
}
|
|
for child in prefab_instance_members(world, entity) {
|
|
let Some((_, target)) = prefab_member_override_target(world, child) else {
|
|
continue;
|
|
};
|
|
let name = world
|
|
.get::<Name>(child)
|
|
.map(|n| n.to_string())
|
|
.unwrap_or_else(|| format!("{child:?}"));
|
|
let mut visible = overrides
|
|
.components
|
|
.iter()
|
|
.find(|component| {
|
|
component.target == target
|
|
&& component.component_type == visibility_type
|
|
})
|
|
.and_then(|component| component.value_component_ron.as_deref())
|
|
.and_then(|ron| ron::from_str::<EditorVisibility>(ron).ok())
|
|
.map(|visibility| visibility.visible)
|
|
.or_else(|| {
|
|
target
|
|
.instance_chain
|
|
.is_empty()
|
|
.then(|| overrides.child_visibility.get(&target.actor_id).copied())
|
|
.flatten()
|
|
})
|
|
.unwrap_or_else(|| {
|
|
world
|
|
.get::<EditorVisibility>(child)
|
|
.map(|visibility| visibility.visible)
|
|
.unwrap_or(true)
|
|
});
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.add(
|
|
egui::Label::new(format!("{name} ({})", actor_path_key(&target)))
|
|
.truncate(),
|
|
);
|
|
if ui.checkbox(&mut visible, "Visible").changed() {
|
|
set_child_visibility_override(world, entity, child, visible);
|
|
}
|
|
});
|
|
}
|
|
ui.horizontal_wrapped(|ui| {
|
|
if ui
|
|
.add_enabled(active, egui::Button::new("Revert all"))
|
|
.clicked()
|
|
{
|
|
revert_child_visibility_overrides(world, entity);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
ui.horizontal_wrapped(|ui| {
|
|
if ui.button("Create Variant...").clicked() {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().request =
|
|
Some(crate::scene_io::SceneIoRequest::SaveSelectionAsPrefab);
|
|
}
|
|
if ui
|
|
.add_enabled(
|
|
world.get::<HydratedPrefabReady>(entity).is_some(),
|
|
egui::Button::new("Unpack Layer"),
|
|
)
|
|
.on_disabled_hover_text("Prefab source has not finished loading")
|
|
.on_hover_text("Make this prefab layer local while preserving nested prefab links")
|
|
.clicked()
|
|
{
|
|
crate::history::unpack_prefab_instance(world, entity);
|
|
}
|
|
if ui
|
|
.add_enabled(
|
|
world.get::<HydratedPrefabReady>(entity).is_some(),
|
|
egui::Button::new("Convert to Local"),
|
|
)
|
|
.on_disabled_hover_text("Prefab source has not finished loading")
|
|
.on_hover_text(
|
|
"Recursively replace this instance and all nested links with local actors",
|
|
)
|
|
.clicked()
|
|
{
|
|
crate::history::convert_prefab_completely_to_local(world, entity);
|
|
}
|
|
});
|
|
});
|
|
apply_component_card_response(world, entity, card_response);
|
|
}
|
|
|
|
fn prefab_instance_members(world: &mut World, root: Entity) -> Vec<Entity> {
|
|
let candidates: Vec<Entity> = world
|
|
.query_filtered::<Entity, With<HydratedPrefabMember>>()
|
|
.iter(world)
|
|
.collect();
|
|
let mut members: Vec<_> = candidates
|
|
.into_iter()
|
|
.filter_map(|candidate| {
|
|
prefab_member_override_target(world, candidate)
|
|
.filter(|(owner, _)| *owner == root)
|
|
.map(|(_, target)| (actor_path_key(&target), candidate))
|
|
})
|
|
.collect();
|
|
members.sort_by(|(left, _), (right, _)| left.cmp(right));
|
|
members
|
|
.into_iter()
|
|
.map(|(_, candidate)| candidate)
|
|
.collect()
|
|
}
|
|
|
|
pub fn list_prefab_dependencies(path: &str) -> Vec<String> {
|
|
vec![path.to_string()]
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use bevy::asset::AssetPlugin;
|
|
use bevy::prelude::DynamicWorldRoot;
|
|
use bevy::world_serialization::{DynamicWorld, WorldInstanceSpawner, WorldSerializationPlugin};
|
|
use shared::LevelObject;
|
|
|
|
fn prefab_source(actor_id: &str, material: Option<&MaterialDesc>) -> String {
|
|
let material = material.map(|material| {
|
|
format!(
|
|
",\"{}\":{}",
|
|
std::any::type_name::<MaterialDesc>(),
|
|
ron::to_string(material).unwrap()
|
|
)
|
|
});
|
|
format!(
|
|
"(schema_version:2,resources:{{}},entities:{{1:(components:{{\"shared::components::LevelObject\":(),\"shared::components::ActorId\":(\"{actor_id}\"),\"shared::components::ActorKind\":Empty,\"{}\":{}{} }})}})",
|
|
std::any::type_name::<Transform>(),
|
|
ron::to_string(&Transform::default()).unwrap(),
|
|
material.unwrap_or_default(),
|
|
)
|
|
}
|
|
|
|
fn hierarchy_prefab_source(child_transform: Transform) -> String {
|
|
format!(
|
|
"(schema_version:2,resources:{{}},entities:{{1:(components:{{\"shared::components::LevelObject\":(),\"shared::components::ActorId\":(\"parent\"),\"shared::components::ActorKind\":Empty,\"{}\":{}}}),2:(components:{{\"shared::components::LevelObject\":(),\"shared::components::ActorId\":(\"child\"),\"shared::components::ActorKind\":Empty,\"{}\":{},\"bevy_ecs::hierarchy::ChildOf\":(1)}})}})",
|
|
std::any::type_name::<Transform>(),
|
|
ron::to_string(&Transform::default()).unwrap(),
|
|
std::any::type_name::<Transform>(),
|
|
ron::to_string(&child_transform).unwrap(),
|
|
)
|
|
}
|
|
|
|
fn prefab_test_world(project_root: &Path) -> World {
|
|
let mut world = World::new();
|
|
world.init_resource::<crate::scene_io::SceneIo>();
|
|
world.init_resource::<crate::history::EditorHistory>();
|
|
world.insert_resource(crate::project_io::ProjectWorkspace {
|
|
root: project_root.display().to_string(),
|
|
..Default::default()
|
|
});
|
|
world
|
|
}
|
|
|
|
#[test]
|
|
fn empty_overrides_roundtrip() {
|
|
let instance = PrefabInstance::new("id", "assets/prefabs/test.scn.ron");
|
|
assert_eq!(read_overrides(&instance), PrefabOverrides::default());
|
|
}
|
|
|
|
#[test]
|
|
fn nested_unpack_fold_is_transactional_when_a_later_link_is_invalid() {
|
|
let mut world = World::new();
|
|
world.init_resource::<crate::scene_io::SceneIo>();
|
|
let material = MaterialDesc::default();
|
|
let mut outer = PrefabInstance::new("outer", "assets/prefabs/outer.scn.ron");
|
|
outer.overrides_ron = encode_prefab_overrides(&PrefabOverrides {
|
|
properties: ["first", "second"]
|
|
.into_iter()
|
|
.map(|anchor| PrefabPropertyOverride {
|
|
target: PrefabActorPath {
|
|
instance_chain: vec![anchor.to_string()],
|
|
actor_id: "mesh".to_string(),
|
|
},
|
|
component_type: std::any::type_name::<MaterialDesc>().to_string(),
|
|
property_path: "roughness".to_string(),
|
|
base_component_ron: ron::to_string(&material).unwrap(),
|
|
value_component_ron: ron::to_string(&material).unwrap(),
|
|
})
|
|
.collect(),
|
|
..default()
|
|
})
|
|
.unwrap();
|
|
let root = world.spawn(outer).id();
|
|
let first = world
|
|
.spawn((
|
|
ActorId::new("first"),
|
|
PrefabInstance::new("first", "assets/prefabs/first.scn.ron"),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
))
|
|
.id();
|
|
let mut invalid = PrefabInstance::new("second", "assets/prefabs/second.scn.ron");
|
|
invalid.overrides_ron = Some("invalid".to_string());
|
|
world.spawn((
|
|
ActorId::new("second"),
|
|
invalid,
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
));
|
|
|
|
assert!(fold_nested_overrides_for_unpack(&mut world, root).is_err());
|
|
assert!(world
|
|
.get::<PrefabInstance>(first)
|
|
.unwrap()
|
|
.overrides_ron
|
|
.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn property_apply_to_source_is_transactional_and_history_guarded() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-source-apply-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("base.scn.ron");
|
|
let base_material = MaterialDesc::default();
|
|
let before_source = prefab_source("mesh", Some(&base_material));
|
|
std::fs::write(&source_path, &before_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance_root = world
|
|
.spawn(PrefabInstance::new("base", "assets/prefabs/base.scn.ron"))
|
|
.id();
|
|
let member = world
|
|
.spawn((
|
|
ActorId::new("mesh"),
|
|
base_material.clone(),
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(instance_root),
|
|
))
|
|
.id();
|
|
let mut overridden = base_material.clone();
|
|
overridden.roughness = 0.2;
|
|
set_prefab_material_property_override(&mut world, member, "roughness", overridden);
|
|
|
|
apply_prefab_member_scope_to_source(
|
|
&mut world,
|
|
member,
|
|
Some(std::any::type_name::<MaterialDesc>()),
|
|
Some("roughness"),
|
|
);
|
|
|
|
let applied_source = std::fs::read_to_string(&source_path).unwrap();
|
|
let document = SceneDocument::from_ron_text(&applied_source).unwrap();
|
|
let material: MaterialDesc = ron::from_str(
|
|
&document.entities[0]
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == std::any::type_name::<MaterialDesc>())
|
|
.unwrap()
|
|
.ron,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(material.roughness, 0.2);
|
|
assert!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.properties
|
|
.is_empty()
|
|
);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
2
|
|
);
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
before_source
|
|
);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.properties
|
|
.len(),
|
|
1
|
|
);
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
applied_source
|
|
);
|
|
|
|
std::fs::write(&source_path, format!("{applied_source}\n")).unwrap();
|
|
let undo_depth = world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth();
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
undo_depth
|
|
);
|
|
assert!(world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.status
|
|
.contains("changed outside"));
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn added_component_field_apply_commits_prerequisite_and_round_trips() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-component-apply-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("base.scn.ron");
|
|
let before_source = prefab_source("mesh", None);
|
|
std::fs::write(&source_path, &before_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance_root = world
|
|
.spawn(PrefabInstance::new("base", "assets/prefabs/base.scn.ron"))
|
|
.id();
|
|
let member = world
|
|
.spawn((
|
|
ActorId::new("mesh"),
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(instance_root),
|
|
))
|
|
.id();
|
|
let base = MaterialDesc::default();
|
|
set_prefab_component_override(&mut world, member, base.clone());
|
|
let mut edited = base;
|
|
edited.roughness = 0.17;
|
|
set_prefab_material_property_override(&mut world, member, "roughness", edited);
|
|
|
|
apply_prefab_member_scope_to_source(
|
|
&mut world,
|
|
member,
|
|
Some(std::any::type_name::<MaterialDesc>()),
|
|
Some("roughness"),
|
|
);
|
|
|
|
let after_source = std::fs::read_to_string(&source_path).unwrap();
|
|
let document = SceneDocument::from_ron_text(&after_source).unwrap();
|
|
let material: MaterialDesc = ron::from_str(
|
|
&document
|
|
.find_entity(&SceneEntityId::ActorId("mesh".into()))
|
|
.unwrap()
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == std::any::type_name::<MaterialDesc>())
|
|
.unwrap()
|
|
.ron,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(material.roughness, 0.17);
|
|
let applied = read_overrides(world.get::<PrefabInstance>(instance_root).unwrap());
|
|
assert!(applied.components.is_empty());
|
|
assert!(applied.properties.is_empty());
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
before_source
|
|
);
|
|
let restored = read_overrides(world.get::<PrefabInstance>(instance_root).unwrap());
|
|
assert_eq!(restored.components.len(), 1);
|
|
assert_eq!(restored.properties.len(), 1);
|
|
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert_eq!(std::fs::read_to_string(&source_path).unwrap(), after_source);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn removed_component_apply_and_undo_preserve_source_bytes() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-component-remove-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("base.scn.ron");
|
|
let material = MaterialDesc::default();
|
|
let before_source = prefab_source("mesh", Some(&material));
|
|
std::fs::write(&source_path, &before_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance_root = world
|
|
.spawn(PrefabInstance::new("base", "assets/prefabs/base.scn.ron"))
|
|
.id();
|
|
let member = world
|
|
.spawn((
|
|
ActorId::new("mesh"),
|
|
material,
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(instance_root),
|
|
))
|
|
.id();
|
|
remove_prefab_component_override::<MaterialDesc>(&mut world, member);
|
|
|
|
apply_prefab_member_scope_to_source(
|
|
&mut world,
|
|
member,
|
|
Some(std::any::type_name::<MaterialDesc>()),
|
|
None,
|
|
);
|
|
|
|
let after_source = std::fs::read_to_string(&source_path).unwrap();
|
|
let document = SceneDocument::from_ron_text(&after_source).unwrap();
|
|
assert!(document.entities[0]
|
|
.components
|
|
.iter()
|
|
.all(|component| component.type_name != std::any::type_name::<MaterialDesc>()));
|
|
assert!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.components
|
|
.is_empty()
|
|
);
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
before_source
|
|
);
|
|
let restored = read_overrides(world.get::<PrefabInstance>(instance_root).unwrap());
|
|
assert_eq!(restored.components.len(), 1);
|
|
assert!(restored.components[0].value_component_ron.is_none());
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn structural_remove_apply_is_exact_byte_undoable() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-structural-apply-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("hierarchy.scn.ron");
|
|
let before_source = hierarchy_prefab_source(Transform::from_xyz(1.0, 2.0, 3.0));
|
|
std::fs::write(&source_path, &before_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance = PrefabInstance::new("hierarchy", "assets/prefabs/hierarchy.scn.ron");
|
|
let instance_root = world.spawn(instance.clone()).id();
|
|
let revision = prefab_source_revision(&root, &instance.source_path).unwrap();
|
|
write_overrides(
|
|
&mut world,
|
|
instance_root,
|
|
&PrefabOverrides {
|
|
source_revision: Some(revision.clone()),
|
|
structural: vec![PrefabStructuralOverride::RemoveChild {
|
|
target: PrefabActorPath::direct("child"),
|
|
base_subtree_revision: revision,
|
|
}],
|
|
..default()
|
|
},
|
|
);
|
|
|
|
apply_prefab_instance_overrides_to_source(&mut world, instance_root);
|
|
|
|
let after_source = std::fs::read_to_string(&source_path).unwrap();
|
|
let document = SceneDocument::from_ron_text(&after_source).unwrap();
|
|
assert!(document
|
|
.find_entity(&SceneEntityId::ActorId("child".into()))
|
|
.is_none());
|
|
assert!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.structural
|
|
.is_empty()
|
|
);
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
before_source
|
|
);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.structural
|
|
.len(),
|
|
1
|
|
);
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert_eq!(std::fs::read_to_string(&source_path).unwrap(), after_source);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn reparent_then_transform_edit_applies_in_layer_order_and_undoes() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-reparent-apply-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("hierarchy.scn.ron");
|
|
let before_source = hierarchy_prefab_source(Transform::from_xyz(1.0, 0.0, 0.0));
|
|
std::fs::write(&source_path, &before_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance_root = world
|
|
.spawn(PrefabInstance::new(
|
|
"hierarchy",
|
|
"assets/prefabs/hierarchy.scn.ron",
|
|
))
|
|
.id();
|
|
let parent = world
|
|
.spawn((
|
|
ActorId::new("parent"),
|
|
Transform::default(),
|
|
GlobalTransform::from_xyz(10.0, 0.0, 0.0),
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(instance_root),
|
|
))
|
|
.id();
|
|
let child = world
|
|
.spawn((
|
|
ActorId::new("child"),
|
|
Transform::from_xyz(1.0, 0.0, 0.0),
|
|
GlobalTransform::from_xyz(11.0, 0.0, 0.0),
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(parent),
|
|
))
|
|
.id();
|
|
|
|
assert!(reparent_prefab_source_child(&mut world, child, None));
|
|
assert_eq!(world.get::<Transform>(child).unwrap().translation.x, 11.0);
|
|
set_prefab_transform_property_override(
|
|
&mut world,
|
|
child,
|
|
"translation",
|
|
Transform::from_xyz(12.0, 0.0, 0.0),
|
|
);
|
|
let instance = world.get::<PrefabInstance>(instance_root).unwrap().clone();
|
|
let overrides = read_overrides(&instance);
|
|
assert!(prefab_semantic_conflicts(&root, &instance, &overrides).is_empty());
|
|
|
|
apply_prefab_actor_path_to_source(
|
|
&mut world,
|
|
instance_root,
|
|
&PrefabActorPath::direct("child"),
|
|
);
|
|
|
|
let after_source = std::fs::read_to_string(&source_path).unwrap();
|
|
let document = SceneDocument::from_ron_text(&after_source).unwrap();
|
|
let child_source = document
|
|
.find_entity(&SceneEntityId::ActorId("child".into()))
|
|
.unwrap();
|
|
assert!(child_source.parent_actor_id.is_none());
|
|
let transform: Transform = ron::from_str(
|
|
&child_source
|
|
.components
|
|
.iter()
|
|
.find(|component| component.type_name == std::any::type_name::<Transform>())
|
|
.unwrap()
|
|
.ron,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(transform.translation.x, 12.0);
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
before_source
|
|
);
|
|
let restored = read_overrides(world.get::<PrefabInstance>(instance_root).unwrap());
|
|
assert_eq!(restored.structural.len(), 1);
|
|
assert_eq!(restored.properties.len(), 1);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn multi_member_reparent_is_atomic_and_records_one_history_entry() {
|
|
let mut world = World::new();
|
|
world.init_resource::<crate::scene_io::SceneIo>();
|
|
world.init_resource::<crate::history::EditorHistory>();
|
|
let root = world
|
|
.spawn(PrefabInstance::new("prefab", "assets/prefabs/test.scn.ron"))
|
|
.id();
|
|
let parent = world
|
|
.spawn((
|
|
ActorId::new("parent"),
|
|
GlobalTransform::from_xyz(10.0, 0.0, 0.0),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
ChildOf(root),
|
|
))
|
|
.id();
|
|
let first = world
|
|
.spawn((
|
|
ActorId::new("first"),
|
|
Transform::default(),
|
|
GlobalTransform::from_xyz(1.0, 0.0, 0.0),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
ChildOf(root),
|
|
))
|
|
.id();
|
|
let second = world
|
|
.spawn((
|
|
ActorId::new("second"),
|
|
Transform::default(),
|
|
GlobalTransform::from_xyz(2.0, 0.0, 0.0),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
ChildOf(root),
|
|
))
|
|
.id();
|
|
|
|
assert!(reparent_prefab_source_children(
|
|
&mut world,
|
|
&[first, second],
|
|
Some(parent),
|
|
));
|
|
assert_eq!(
|
|
world.get::<ChildOf>(first).map(ChildOf::parent),
|
|
Some(parent)
|
|
);
|
|
assert_eq!(
|
|
world.get::<ChildOf>(second).map(ChildOf::parent),
|
|
Some(parent)
|
|
);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.structural
|
|
.len(),
|
|
2
|
|
);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
1
|
|
);
|
|
|
|
let invalid = world.spawn_empty().id();
|
|
let before = world.get::<ChildOf>(first).map(ChildOf::parent);
|
|
assert!(!reparent_prefab_source_children(
|
|
&mut world,
|
|
&[first, invalid],
|
|
None,
|
|
));
|
|
assert_eq!(world.get::<ChildOf>(first).map(ChildOf::parent), before);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_apply_updates_immediate_source_layer() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-nested-apply-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let base_path = prefabs.join("base.scn.ron");
|
|
let variant_path = prefabs.join("variant.scn.ron");
|
|
let base_material = MaterialDesc::default();
|
|
let base_source = prefab_source("mesh", Some(&base_material));
|
|
std::fs::write(&base_path, &base_source).unwrap();
|
|
let mut variant_material = base_material.clone();
|
|
variant_material.roughness = 0.2;
|
|
let mut nested_instance = PrefabInstance::new("base", "assets/prefabs/base.scn.ron");
|
|
nested_instance.overrides_ron = encode_prefab_overrides(&PrefabOverrides {
|
|
properties: vec![PrefabPropertyOverride {
|
|
target: PrefabActorPath::direct("mesh"),
|
|
component_type: std::any::type_name::<MaterialDesc>().to_string(),
|
|
property_path: "roughness".to_string(),
|
|
base_component_ron: ron::to_string(&base_material).unwrap(),
|
|
value_component_ron: ron::to_string(&variant_material).unwrap(),
|
|
}],
|
|
..default()
|
|
})
|
|
.unwrap();
|
|
let variant_source = format!(
|
|
"(schema_version:2,resources:{{}},entities:{{1:(components:{{\"shared::components::LevelObject\":(),\"shared::components::ActorId\":(\"nested\"),\"shared::components::ActorKind\":PrefabAnchor,\"{}\":{},\"{}\":{} }})}})",
|
|
std::any::type_name::<Transform>(),
|
|
ron::to_string(&Transform::default()).unwrap(),
|
|
std::any::type_name::<PrefabInstance>(),
|
|
ron::to_string(&nested_instance).unwrap(),
|
|
);
|
|
std::fs::write(&variant_path, &variant_source).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let outer = world
|
|
.spawn(PrefabInstance::new(
|
|
"variant",
|
|
"assets/prefabs/variant.scn.ron",
|
|
))
|
|
.id();
|
|
let nested = world
|
|
.spawn((
|
|
ActorId::new("nested"),
|
|
nested_instance.clone(),
|
|
HydratedPrefabMember {
|
|
instance_root: outer,
|
|
},
|
|
ChildOf(outer),
|
|
))
|
|
.id();
|
|
let member = world
|
|
.spawn((
|
|
ActorId::new("mesh"),
|
|
variant_material.clone(),
|
|
HydratedPrefabMember {
|
|
instance_root: nested,
|
|
},
|
|
ChildOf(nested),
|
|
))
|
|
.id();
|
|
let mut overridden = variant_material;
|
|
overridden.roughness = 0.1;
|
|
set_prefab_material_property_override(&mut world, member, "roughness", overridden);
|
|
let outer_instance = world.get::<PrefabInstance>(outer).unwrap().clone();
|
|
let outer_overrides = read_overrides(&outer_instance);
|
|
assert!(prefab_semantic_conflicts(&root, &outer_instance, &outer_overrides).is_empty());
|
|
|
|
apply_prefab_member_scope_to_source(
|
|
&mut world,
|
|
member,
|
|
Some(std::any::type_name::<MaterialDesc>()),
|
|
Some("roughness"),
|
|
);
|
|
|
|
assert_eq!(std::fs::read_to_string(&base_path).unwrap(), base_source);
|
|
let document =
|
|
SceneDocument::from_ron_text(&std::fs::read_to_string(&variant_path).unwrap()).unwrap();
|
|
let nested_overrides = decode_prefab_overrides(&document.prefab_instances[0]).unwrap();
|
|
assert_eq!(nested_overrides.properties.len(), 1);
|
|
assert!(nested_overrides.properties[0]
|
|
.target
|
|
.instance_chain
|
|
.is_empty());
|
|
assert_eq!(nested_overrides.properties[0].target.actor_id, "mesh");
|
|
let preserved_base: MaterialDesc =
|
|
ron::from_str(&nested_overrides.properties[0].base_component_ron).unwrap();
|
|
let committed_value: MaterialDesc =
|
|
ron::from_str(&nested_overrides.properties[0].value_component_ron).unwrap();
|
|
assert_eq!(preserved_base.roughness, base_material.roughness);
|
|
assert_eq!(committed_value.roughness, 0.1);
|
|
let effective = source_component_for_target(
|
|
&root,
|
|
world.get::<PrefabInstance>(outer).unwrap(),
|
|
&PrefabActorPath {
|
|
instance_chain: vec!["nested".to_string()],
|
|
actor_id: "mesh".to_string(),
|
|
},
|
|
std::any::type_name::<MaterialDesc>(),
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
ron::from_str::<MaterialDesc>(&effective.ron)
|
|
.unwrap()
|
|
.roughness,
|
|
0.1
|
|
);
|
|
assert!(read_overrides(world.get::<PrefabInstance>(outer).unwrap())
|
|
.properties
|
|
.is_empty());
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
std::fs::read_to_string(&variant_path).unwrap(),
|
|
variant_source
|
|
);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(outer).unwrap())
|
|
.properties
|
|
.len(),
|
|
1
|
|
);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn source_field_conflict_keeps_override_recoverable() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-source-conflict-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let prefabs = root.join("assets/prefabs");
|
|
std::fs::create_dir_all(&prefabs).unwrap();
|
|
std::fs::write(root.join("assets/project.ron"), "()").unwrap();
|
|
let source_path = prefabs.join("base.scn.ron");
|
|
let base = MaterialDesc::default();
|
|
std::fs::write(&source_path, prefab_source("mesh", Some(&base))).unwrap();
|
|
|
|
let mut world = prefab_test_world(&root);
|
|
let instance_root = world
|
|
.spawn(PrefabInstance::new("base", "assets/prefabs/base.scn.ron"))
|
|
.id();
|
|
let member = world
|
|
.spawn((
|
|
ActorId::new("mesh"),
|
|
base.clone(),
|
|
HydratedPrefabMember { instance_root },
|
|
ChildOf(instance_root),
|
|
))
|
|
.id();
|
|
let mut overridden = base.clone();
|
|
overridden.roughness = 0.1;
|
|
set_prefab_material_property_override(&mut world, member, "roughness", overridden);
|
|
let mut externally_changed = base;
|
|
externally_changed.roughness = 0.4;
|
|
let changed_source = prefab_source("mesh", Some(&externally_changed));
|
|
std::fs::write(&source_path, &changed_source).unwrap();
|
|
|
|
apply_prefab_member_scope_to_source(
|
|
&mut world,
|
|
member,
|
|
Some(std::any::type_name::<MaterialDesc>()),
|
|
Some("roughness"),
|
|
);
|
|
|
|
assert_eq!(
|
|
std::fs::read_to_string(&source_path).unwrap(),
|
|
changed_source
|
|
);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(instance_root).unwrap())
|
|
.properties
|
|
.len(),
|
|
1
|
|
);
|
|
assert!(world
|
|
.resource::<crate::scene_io::SceneIo>()
|
|
.status
|
|
.contains("conflict"));
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
1
|
|
);
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn visibility_component_override_survives_rename_by_actor_path() {
|
|
let mut world = World::new();
|
|
world.init_resource::<crate::scene_io::SceneIo>();
|
|
world.init_resource::<crate::history::EditorHistory>();
|
|
let root = world
|
|
.spawn((
|
|
LevelObject,
|
|
PrefabInstance::new("prefab-id", "assets/prefabs/test.scn.ron"),
|
|
))
|
|
.id();
|
|
let child = world
|
|
.spawn((
|
|
LevelObject,
|
|
ActorId::new("stable-child"),
|
|
Name::new("Old Name"),
|
|
EditorVisibility { visible: false },
|
|
ChildOf(root),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
))
|
|
.id();
|
|
|
|
set_child_visibility_override(&mut world, root, child, false);
|
|
let overrides = read_overrides(world.get::<PrefabInstance>(root).unwrap());
|
|
assert!(overrides.child_visibility.is_empty());
|
|
assert_eq!(overrides.components.len(), 1);
|
|
assert_eq!(
|
|
overrides.components[0].target,
|
|
PrefabActorPath::direct("stable-child")
|
|
);
|
|
assert_eq!(
|
|
overrides.components[0].component_type,
|
|
std::any::type_name::<EditorVisibility>()
|
|
);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
1
|
|
);
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert!(read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.components
|
|
.is_empty());
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap()).components[0].target,
|
|
PrefabActorPath::direct("stable-child")
|
|
);
|
|
|
|
world.entity_mut(child).insert(Name::new("Renamed Child"));
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap()).components[0].target,
|
|
PrefabActorPath::direct("stable-child")
|
|
);
|
|
|
|
revert_child_visibility_overrides(&mut world, root);
|
|
assert_eq!(
|
|
world
|
|
.resource::<crate::history::EditorHistory>()
|
|
.undo_depth(),
|
|
2
|
|
);
|
|
assert!(world.get::<EditorVisibility>(child).is_none());
|
|
assert!(read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.components
|
|
.is_empty());
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap()).components[0].target,
|
|
PrefabActorPath::direct("stable-child")
|
|
);
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert!(read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.components
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn nested_visibility_overrides_distinguish_repeated_leaf_actor_ids() {
|
|
let mut world = World::new();
|
|
world.init_resource::<crate::scene_io::SceneIo>();
|
|
world.init_resource::<crate::history::EditorHistory>();
|
|
let root = world
|
|
.spawn(PrefabInstance::new("outer", "assets/prefabs/outer.scn.ron"))
|
|
.id();
|
|
let nested_a = world
|
|
.spawn((
|
|
ActorId::new("nested-a"),
|
|
PrefabInstance::new("leaf", "assets/prefabs/leaf.scn.ron"),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
ChildOf(root),
|
|
))
|
|
.id();
|
|
let nested_b = world
|
|
.spawn((
|
|
ActorId::new("nested-b"),
|
|
PrefabInstance::new("leaf", "assets/prefabs/leaf.scn.ron"),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
ChildOf(root),
|
|
))
|
|
.id();
|
|
let leaf_a = world
|
|
.spawn((
|
|
ActorId::new("shared-leaf"),
|
|
EditorVisibility::default(),
|
|
HydratedPrefabMember {
|
|
instance_root: nested_a,
|
|
},
|
|
ChildOf(nested_a),
|
|
))
|
|
.id();
|
|
let leaf_b = world
|
|
.spawn((
|
|
ActorId::new("shared-leaf"),
|
|
EditorVisibility::default(),
|
|
HydratedPrefabMember {
|
|
instance_root: nested_b,
|
|
},
|
|
ChildOf(nested_b),
|
|
))
|
|
.id();
|
|
|
|
set_child_visibility_override(&mut world, root, leaf_a, false);
|
|
set_child_visibility_override(&mut world, root, leaf_b, false);
|
|
|
|
let overrides = read_overrides(world.get::<PrefabInstance>(root).unwrap());
|
|
let targets: HashSet<_> = overrides
|
|
.components
|
|
.iter()
|
|
.map(|component| component.target.clone())
|
|
.collect();
|
|
assert_eq!(targets.len(), 2);
|
|
assert!(targets.contains(&PrefabActorPath {
|
|
instance_chain: vec!["nested-a".to_string()],
|
|
actor_id: "shared-leaf".to_string(),
|
|
}));
|
|
assert!(targets.contains(&PrefabActorPath {
|
|
instance_chain: vec!["nested-b".to_string()],
|
|
actor_id: "shared-leaf".to_string(),
|
|
}));
|
|
|
|
crate::history::apply_command_undo(&mut world);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.components
|
|
.len(),
|
|
1
|
|
);
|
|
crate::history::apply_command_redo(&mut world);
|
|
assert_eq!(
|
|
read_overrides(world.get::<PrefabInstance>(root).unwrap())
|
|
.components
|
|
.len(),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ready_instance_reapplies_child_visibility_by_actor_id() {
|
|
let mut app = App::new();
|
|
app.add_plugins((AssetPlugin::default(), WorldSerializationPlugin))
|
|
.register_type::<ActorId>()
|
|
.add_observer(shared::apply_prefab_overrides_on_ready);
|
|
|
|
let mut source = World::new();
|
|
source.spawn(ActorId::new("stable-child"));
|
|
let dynamic_world = DynamicWorld::from_world_with(
|
|
&source,
|
|
&app.world().resource::<AppTypeRegistry>().read(),
|
|
);
|
|
let handle = app
|
|
.world_mut()
|
|
.resource_mut::<Assets<DynamicWorld>>()
|
|
.add(dynamic_world);
|
|
let root = app
|
|
.world_mut()
|
|
.spawn((
|
|
PrefabInstance::new("prefab-id", "assets/prefabs/test.scn.ron"),
|
|
DynamicWorldRoot(handle),
|
|
))
|
|
.id();
|
|
write_overrides(
|
|
app.world_mut(),
|
|
root,
|
|
&PrefabOverrides {
|
|
child_visibility: HashMap::from([("stable-child".to_string(), false)]),
|
|
..default()
|
|
},
|
|
);
|
|
|
|
app.update();
|
|
|
|
let world = app.world_mut();
|
|
let (_, visibility) = world
|
|
.query::<(&ActorId, &EditorVisibility)>()
|
|
.single(world)
|
|
.expect("ready observer should apply the stable child override");
|
|
assert!(!visibility.visible);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_reimport_cleanup_allows_single_clean_retry() {
|
|
let mut app = App::new();
|
|
app.add_plugins((AssetPlugin::default(), WorldSerializationPlugin))
|
|
.register_type::<ActorId>();
|
|
|
|
let mut source = World::new();
|
|
source.spawn(ActorId::new("source-child"));
|
|
let dynamic_world = DynamicWorld::from_world_with(
|
|
&source,
|
|
&app.world().resource::<AppTypeRegistry>().read(),
|
|
);
|
|
let handle = app
|
|
.world_mut()
|
|
.resource_mut::<Assets<DynamicWorld>>()
|
|
.add(dynamic_world);
|
|
let root = app
|
|
.world_mut()
|
|
.spawn((
|
|
PrefabInstance::new("prefab-id", "assets/prefabs/test.scn.ron"),
|
|
DynamicWorldRoot(handle.clone()),
|
|
))
|
|
.id();
|
|
app.update();
|
|
|
|
let first_instance = **app
|
|
.world()
|
|
.get::<WorldInstance>(root)
|
|
.expect("initial prefab should be registered");
|
|
let first_members: Vec<_> = app
|
|
.world()
|
|
.resource::<WorldInstanceSpawner>()
|
|
.iter_instance_entities(first_instance)
|
|
.collect();
|
|
assert_eq!(first_members.len(), 1);
|
|
for member in first_members {
|
|
app.world_mut()
|
|
.entity_mut(member)
|
|
.insert(HydratedPrefabMember {
|
|
instance_root: root,
|
|
});
|
|
}
|
|
|
|
block_prefab_hydration(app.world_mut(), root, "invalid source".to_string());
|
|
|
|
assert!(app.world().get::<DynamicWorldRoot>(root).is_none());
|
|
assert!(app.world().get::<WorldInstance>(root).is_none());
|
|
assert!(app
|
|
.world()
|
|
.get::<shared::PrefabHydrationBlocked>(root)
|
|
.is_some());
|
|
assert_eq!(
|
|
app.world_mut()
|
|
.query::<&HydratedPrefabMember>()
|
|
.iter(app.world())
|
|
.count(),
|
|
0
|
|
);
|
|
assert!(!app
|
|
.world()
|
|
.resource::<WorldInstanceSpawner>()
|
|
.instance_is_ready(first_instance));
|
|
|
|
app.world_mut()
|
|
.entity_mut(root)
|
|
.remove::<shared::PrefabHydrationBlocked>()
|
|
.insert(DynamicWorldRoot(handle));
|
|
app.update();
|
|
|
|
let repaired_instance = **app
|
|
.world()
|
|
.get::<WorldInstance>(root)
|
|
.expect("repaired prefab should register a fresh instance");
|
|
let repaired_members: Vec<_> = app
|
|
.world()
|
|
.resource::<WorldInstanceSpawner>()
|
|
.iter_instance_entities(repaired_instance)
|
|
.collect();
|
|
assert_ne!(repaired_instance, first_instance);
|
|
assert_eq!(repaired_members.len(), 1);
|
|
assert_eq!(
|
|
app.world_mut()
|
|
.query::<&ActorId>()
|
|
.iter(app.world())
|
|
.count(),
|
|
1,
|
|
"retry must not duplicate generated source state"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn removed_asset_event_blocks_prefab_and_cleans_generated_state() {
|
|
let project_root = std::env::temp_dir().join(format!(
|
|
"blacksite-prefab-removed-event-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(project_root.join("assets/prefabs")).unwrap();
|
|
std::fs::write(project_root.join("assets/project.ron"), "()").unwrap();
|
|
|
|
let mut app = App::new();
|
|
app.add_plugins((AssetPlugin::default(), WorldSerializationPlugin))
|
|
.init_resource::<crate::scene_io::SceneIo>()
|
|
.insert_resource(crate::project_io::ProjectWorkspace {
|
|
root: project_root.display().to_string(),
|
|
..Default::default()
|
|
})
|
|
.add_systems(Update, guard_prefab_reimports);
|
|
let handle = app
|
|
.world_mut()
|
|
.resource_mut::<Assets<DynamicWorld>>()
|
|
.add(DynamicWorld::default());
|
|
let root = app
|
|
.world_mut()
|
|
.spawn((
|
|
DynamicWorldRoot(handle.clone()),
|
|
PrefabInstance::new("missing", "assets/prefabs/missing.scn.ron"),
|
|
))
|
|
.id();
|
|
let generated = app
|
|
.world_mut()
|
|
.spawn(HydratedPrefabMember {
|
|
instance_root: root,
|
|
})
|
|
.id();
|
|
app.world_mut()
|
|
.resource_mut::<Messages<AssetEvent<DynamicWorld>>>()
|
|
.write(AssetEvent::Removed { id: handle.id() });
|
|
|
|
app.update();
|
|
|
|
assert!(app.world().get_entity(generated).is_err());
|
|
assert!(app
|
|
.world()
|
|
.get::<shared::PrefabHydrationBlocked>(root)
|
|
.is_some());
|
|
assert!(app.world().get::<DynamicWorldRoot>(root).is_none());
|
|
std::fs::remove_dir_all(project_root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn nested_invalid_reimport_blocks_top_authored_owner() {
|
|
let mut world = World::new();
|
|
let root = world
|
|
.spawn((
|
|
PrefabInstance::new("outer", "assets/prefabs/outer.scn.ron"),
|
|
DynamicWorldRoot::default(),
|
|
))
|
|
.id();
|
|
let nested = world
|
|
.spawn((
|
|
PrefabInstance::new("nested", "assets/prefabs/nested.scn.ron"),
|
|
HydratedPrefabMember {
|
|
instance_root: root,
|
|
},
|
|
))
|
|
.id();
|
|
let nested_child = world
|
|
.spawn(HydratedPrefabMember {
|
|
instance_root: nested,
|
|
})
|
|
.id();
|
|
|
|
block_prefab_hydration(&mut world, nested, "nested source invalid".to_string());
|
|
|
|
assert!(world
|
|
.get::<shared::PrefabHydrationBlocked>(root)
|
|
.is_some_and(|blocked| blocked.reason == "nested source invalid"));
|
|
assert!(world.get::<DynamicWorldRoot>(root).is_none());
|
|
assert!(world.get_entity(nested).is_err());
|
|
assert!(world.get_entity(nested_child).is_err());
|
|
}
|
|
}
|