Add brush authoring schema
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
This commit is contained in:
parent
cc6acb188d
commit
9f95ba3082
@ -181,6 +181,8 @@ Viewport shortcut keys require the pointer to be in the viewport and are suspend
|
|||||||
details when you need full `SceneRoot` loading for animation/skinning/scene data. Expanding a
|
details when you need full `SceneRoot` loading for animation/skinning/scene data. Expanding a
|
||||||
model in the Asset Browser exposes normalized mesh/material/texture subassets; dragging a mesh
|
model in the Asset Browser exposes normalized mesh/material/texture subassets; dragging a mesh
|
||||||
subasset places that part through the same static mesh renderer path.
|
subasset places that part through the same static mesh renderer path.
|
||||||
|
- Brush actors are persisted as `ActorKind::Brush + BrushDesc`; the MVP hydrates additive convex
|
||||||
|
cube brushes into generated preview meshes while face/edge/CSG editing remains roadmap work.
|
||||||
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
||||||
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
||||||
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
||||||
@ -295,6 +297,7 @@ crates/
|
|||||||
- [x] Advanced rendering: `GiMode`, post-process volumes, Rendering panel, requested/effective render stack, Solari integration, emissive materials, post FX assets ([ADR 0013](docs/adr/0013-rendering-tiers-and-post-process-volumes.md), [ADR 0016](docs/adr/0016-unified-rendering-contract.md), [rendering guide](docs/editor/rendering.md))
|
- [x] Advanced rendering: `GiMode`, post-process volumes, Rendering panel, requested/effective render stack, Solari integration, emissive materials, post FX assets ([ADR 0013](docs/adr/0013-rendering-tiers-and-post-process-volumes.md), [ADR 0016](docs/adr/0016-unified-rendering-contract.md), [rendering guide](docs/editor/rendering.md))
|
||||||
- [x] Static mesh renderer component, generated normalized mesh artifacts, source/one-actor hierarchy placement, and inspector renderer slots ([ADR 0017](docs/adr/0017-normalized-static-mesh-assets.md))
|
- [x] Static mesh renderer component, generated normalized mesh artifacts, source/one-actor hierarchy placement, and inspector renderer slots ([ADR 0017](docs/adr/0017-normalized-static-mesh-assets.md))
|
||||||
- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware actor material data, and texture picker/drop refs ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md))
|
- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware actor material data, and texture picker/drop refs ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md))
|
||||||
|
- [x] Brush authoring schema MVP with `ActorKind::Brush`, cube `BrushDesc`, generated mesh hydration, scene migration, and inspector Add Component support ([ADR 0021](docs/adr/0021-brush-authoring-schema.md))
|
||||||
|
|
||||||
## Notes / Future Work
|
## Notes / Future Work
|
||||||
|
|
||||||
|
|||||||
@ -455,6 +455,7 @@ pub fn snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> Option<Edit
|
|||||||
name: Some(asset.label.clone()),
|
name: Some(asset.label.clone()),
|
||||||
transform: Transform::from_translation(translation),
|
transform: Transform::from_translation(translation),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -654,6 +655,7 @@ pub fn spawn_subasset_at(
|
|||||||
name: Some(label.clone()),
|
name: Some(label.clone()),
|
||||||
transform: Transform::from_translation(translation + Vec3::Y * 0.1),
|
transform: Transform::from_translation(translation + Vec3::Y * 0.1),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(slot)),
|
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(slot)),
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -723,6 +725,7 @@ fn apply_static_mesh_placement_mode(
|
|||||||
}),
|
}),
|
||||||
transform,
|
transform,
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(entry)),
|
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(entry)),
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use shared::{
|
use shared::{
|
||||||
ActorId, ActorKind, ActorName, ColliderDesc, EditorVisibility, InspectorOrder, LightDesc,
|
ActorId, ActorKind, ActorName, BrushDesc, ColliderDesc, EditorVisibility, InspectorOrder,
|
||||||
MaterialDesc, MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody, PostProcessVolumeDesc,
|
LightDesc, MaterialDesc, MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody,
|
||||||
PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn,
|
PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer,
|
||||||
TriggerVolume, WeaponSpawn,
|
TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||||
};
|
};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EditorEntitySnapshot {
|
pub struct EditorEntitySnapshot {
|
||||||
@ -13,6 +13,7 @@ pub struct EditorEntitySnapshot {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub transform: Transform,
|
pub transform: Transform,
|
||||||
pub primitive: Option<Primitive>,
|
pub primitive: Option<Primitive>,
|
||||||
|
pub brush: Option<BrushDesc>,
|
||||||
pub static_mesh_renderer: Option<StaticMeshRenderer>,
|
pub static_mesh_renderer: Option<StaticMeshRenderer>,
|
||||||
pub material: Option<MaterialDesc>,
|
pub material: Option<MaterialDesc>,
|
||||||
pub material_override: Option<MaterialOverride>,
|
pub material_override: Option<MaterialOverride>,
|
||||||
@ -102,6 +103,11 @@ pub enum EditorCommand {
|
|||||||
old: Option<Primitive>,
|
old: Option<Primitive>,
|
||||||
new: Primitive,
|
new: Primitive,
|
||||||
},
|
},
|
||||||
|
SetBrush {
|
||||||
|
entity: Entity,
|
||||||
|
old: Option<BrushDesc>,
|
||||||
|
new: BrushDesc,
|
||||||
|
},
|
||||||
SetStaticMeshRenderer {
|
SetStaticMeshRenderer {
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
old: Option<StaticMeshRenderer>,
|
old: Option<StaticMeshRenderer>,
|
||||||
@ -165,6 +171,7 @@ impl EditorCommand {
|
|||||||
EditorCommand::SetRigidBody { .. } => "Set Rigid Body",
|
EditorCommand::SetRigidBody { .. } => "Set Rigid Body",
|
||||||
EditorCommand::SetCollider { .. } => "Set Collider",
|
EditorCommand::SetCollider { .. } => "Set Collider",
|
||||||
EditorCommand::SetPrimitive { .. } => "Set Primitive",
|
EditorCommand::SetPrimitive { .. } => "Set Primitive",
|
||||||
|
EditorCommand::SetBrush { .. } => "Set Brush",
|
||||||
EditorCommand::SetStaticMeshRenderer { .. } => "Set Static Mesh Renderer",
|
EditorCommand::SetStaticMeshRenderer { .. } => "Set Static Mesh Renderer",
|
||||||
EditorCommand::SetPostProcessVolume { .. } => "Set Post Process Volume",
|
EditorCommand::SetPostProcessVolume { .. } => "Set Post Process Volume",
|
||||||
EditorCommand::SetTransformGroup { .. } => "Move Selection",
|
EditorCommand::SetTransformGroup { .. } => "Move Selection",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use shared::{
|
use shared::{
|
||||||
infer_actor_kind, ActorId, ActorKind, ActorName, ColliderDesc, EditorVisibility,
|
infer_actor_kind, ActorId, ActorKind, ActorName, BrushDesc, ColliderDesc, EditorVisibility,
|
||||||
HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride,
|
HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride,
|
||||||
ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance,
|
ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance,
|
||||||
PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||||
@ -127,6 +127,7 @@ pub fn snapshot_entity(world: &World, entity: Entity) -> Option<EditorEntitySnap
|
|||||||
.map(|name| name.as_str().to_string()),
|
.map(|name| name.as_str().to_string()),
|
||||||
transform: entity_ref.get::<Transform>().copied().unwrap_or_default(),
|
transform: entity_ref.get::<Transform>().copied().unwrap_or_default(),
|
||||||
primitive: entity_ref.get::<Primitive>().cloned(),
|
primitive: entity_ref.get::<Primitive>().cloned(),
|
||||||
|
brush: entity_ref.get::<BrushDesc>().cloned(),
|
||||||
static_mesh_renderer: entity_ref.get::<StaticMeshRenderer>().cloned(),
|
static_mesh_renderer: entity_ref.get::<StaticMeshRenderer>().cloned(),
|
||||||
material: entity_ref.get::<MaterialDesc>().cloned(),
|
material: entity_ref.get::<MaterialDesc>().cloned(),
|
||||||
material_override: entity_ref.get::<MaterialOverride>().cloned(),
|
material_override: entity_ref.get::<MaterialOverride>().cloned(),
|
||||||
@ -185,6 +186,9 @@ fn spawn_snapshot_with_parent(
|
|||||||
if let Some(primitive) = &snapshot.primitive {
|
if let Some(primitive) = &snapshot.primitive {
|
||||||
entity_mut.insert(primitive.clone());
|
entity_mut.insert(primitive.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(brush) = &snapshot.brush {
|
||||||
|
entity_mut.insert(brush.clone());
|
||||||
|
}
|
||||||
if let Some(renderer) = &snapshot.static_mesh_renderer {
|
if let Some(renderer) = &snapshot.static_mesh_renderer {
|
||||||
entity_mut.insert(renderer.clone());
|
entity_mut.insert(renderer.clone());
|
||||||
}
|
}
|
||||||
@ -453,6 +457,20 @@ pub fn set_primitive_with_history(world: &mut World, entity: Entity, new: Primit
|
|||||||
push_history(world, EditorCommand::SetPrimitive { entity, old, new });
|
push_history(world, EditorCommand::SetPrimitive { entity, old, new });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_brush_with_history(world: &mut World, entity: Entity, new: BrushDesc) {
|
||||||
|
if !is_level_object(world, entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let old = world.get::<BrushDesc>(entity).cloned();
|
||||||
|
if old.as_ref() == Some(&new) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||||
|
entity_mut.insert(new.clone());
|
||||||
|
}
|
||||||
|
push_history(world, EditorCommand::SetBrush { entity, old, new });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_static_mesh_renderer_with_history(
|
pub fn set_static_mesh_renderer_with_history(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
@ -558,6 +576,7 @@ pub fn group_selection_with_history(world: &mut World, entities: &[Entity]) {
|
|||||||
name: Some("Group".to_string()),
|
name: Some("Group".to_string()),
|
||||||
transform: Transform::default(),
|
transform: Transform::default(),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -600,6 +619,7 @@ pub fn create_empty_child_with_history(world: &mut World, parent: Entity) {
|
|||||||
name: Some("Empty".to_string()),
|
name: Some("Empty".to_string()),
|
||||||
transform: Transform::default(),
|
transform: Transform::default(),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -777,6 +797,9 @@ fn undo_command(world: &mut World, command: &mut EditorCommand) {
|
|||||||
EditorCommand::SetPrimitive { entity, old, .. } => {
|
EditorCommand::SetPrimitive { entity, old, .. } => {
|
||||||
apply_primitive(world, *entity, old);
|
apply_primitive(world, *entity, old);
|
||||||
}
|
}
|
||||||
|
EditorCommand::SetBrush { entity, old, .. } => {
|
||||||
|
apply_brush(world, *entity, old);
|
||||||
|
}
|
||||||
EditorCommand::SetStaticMeshRenderer { entity, old, .. } => {
|
EditorCommand::SetStaticMeshRenderer { entity, old, .. } => {
|
||||||
apply_static_mesh_renderer(world, *entity, old);
|
apply_static_mesh_renderer(world, *entity, old);
|
||||||
}
|
}
|
||||||
@ -889,6 +912,11 @@ fn redo_command(world: &mut World, command: &mut EditorCommand) {
|
|||||||
entity_mut.insert(new.clone());
|
entity_mut.insert(new.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EditorCommand::SetBrush { entity, new, .. } => {
|
||||||
|
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
||||||
|
entity_mut.insert(new.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
EditorCommand::SetStaticMeshRenderer { entity, new, .. } => {
|
EditorCommand::SetStaticMeshRenderer { entity, new, .. } => {
|
||||||
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
||||||
entity_mut.insert(new.clone());
|
entity_mut.insert(new.clone());
|
||||||
@ -1045,6 +1073,19 @@ fn apply_primitive(world: &mut World, entity: Entity, primitive: &Option<Primiti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_brush(world: &mut World, entity: Entity, brush: &Option<BrushDesc>) {
|
||||||
|
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||||
|
match brush {
|
||||||
|
Some(value) => {
|
||||||
|
entity_mut.insert(value.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
entity_mut.remove::<BrushDesc>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_static_mesh_renderer(
|
fn apply_static_mesh_renderer(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
@ -1098,6 +1139,9 @@ fn apply_authored_components(world: &mut World, entity: Entity, snapshot: &Edito
|
|||||||
if let Some(v) = &snapshot.primitive {
|
if let Some(v) = &snapshot.primitive {
|
||||||
entity_mut.insert(v.clone());
|
entity_mut.insert(v.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(v) = &snapshot.brush {
|
||||||
|
entity_mut.insert(v.clone());
|
||||||
|
}
|
||||||
if let Some(v) = &snapshot.static_mesh_renderer {
|
if let Some(v) = &snapshot.static_mesh_renderer {
|
||||||
entity_mut.insert(v.clone());
|
entity_mut.insert(v.clone());
|
||||||
}
|
}
|
||||||
@ -1154,6 +1198,9 @@ fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &Edit
|
|||||||
if snapshot.primitive.is_some() {
|
if snapshot.primitive.is_some() {
|
||||||
entity_mut.remove::<Primitive>();
|
entity_mut.remove::<Primitive>();
|
||||||
}
|
}
|
||||||
|
if snapshot.brush.is_some() {
|
||||||
|
entity_mut.remove::<BrushDesc>();
|
||||||
|
}
|
||||||
if snapshot.static_mesh_renderer.is_some() {
|
if snapshot.static_mesh_renderer.is_some() {
|
||||||
entity_mut.remove::<StaticMeshRenderer>();
|
entity_mut.remove::<StaticMeshRenderer>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use scene::{document::SceneDocument, strip_schema_version, validate_level_text};
|
|||||||
use serde::de::DeserializeSeed;
|
use serde::de::DeserializeSeed;
|
||||||
use shared::{
|
use shared::{
|
||||||
flush_level_object_hydration, infer_actor_kind, strip_hydrated_entity, validate_actor, ActorId,
|
flush_level_object_hydration, infer_actor_kind, strip_hydrated_entity, validate_actor, ActorId,
|
||||||
ActorKind, ActorName, ActorValidationError, ColliderDesc, EditorVisibility,
|
ActorKind, ActorName, ActorValidationError, BrushDesc, ColliderDesc, EditorVisibility,
|
||||||
HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride,
|
HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride,
|
||||||
ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance,
|
ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance,
|
||||||
PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||||
@ -389,6 +389,21 @@ fn format_actor_validation(err: ActorValidationError) -> String {
|
|||||||
ActorValidationError::MissingTransform => {
|
ActorValidationError::MissingTransform => {
|
||||||
"Save failed: level object missing Transform".into()
|
"Save failed: level object missing Transform".into()
|
||||||
}
|
}
|
||||||
|
ActorValidationError::BrushMissingDesc => {
|
||||||
|
"Save failed: Brush actor requires BrushDesc".into()
|
||||||
|
}
|
||||||
|
ActorValidationError::BrushHasPrimitive => {
|
||||||
|
"Save failed: Brush actor cannot have Primitive".into()
|
||||||
|
}
|
||||||
|
ActorValidationError::BrushHasStaticMeshRenderer => {
|
||||||
|
"Save failed: Brush actor cannot have StaticMeshRenderer".into()
|
||||||
|
}
|
||||||
|
ActorValidationError::BrushHasLight => {
|
||||||
|
"Save failed: Brush actor cannot have LightDesc".into()
|
||||||
|
}
|
||||||
|
ActorValidationError::BrushHasModelRef => {
|
||||||
|
"Save failed: Brush actor cannot have ModelRef".into()
|
||||||
|
}
|
||||||
ActorValidationError::StaticMeshMissingPrimitive => {
|
ActorValidationError::StaticMeshMissingPrimitive => {
|
||||||
"Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot"
|
"Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot"
|
||||||
.into()
|
.into()
|
||||||
@ -462,6 +477,7 @@ fn save_entities(world: &mut World, path: &Path, entities: Vec<Entity>) -> Resul
|
|||||||
.allow_component::<ActorKind>()
|
.allow_component::<ActorKind>()
|
||||||
.allow_component::<InspectorOrder>()
|
.allow_component::<InspectorOrder>()
|
||||||
.allow_component::<Primitive>()
|
.allow_component::<Primitive>()
|
||||||
|
.allow_component::<BrushDesc>()
|
||||||
.allow_component::<StaticMeshRenderer>()
|
.allow_component::<StaticMeshRenderer>()
|
||||||
.allow_component::<MaterialDesc>()
|
.allow_component::<MaterialDesc>()
|
||||||
.allow_component::<MaterialOverride>()
|
.allow_component::<MaterialOverride>()
|
||||||
|
|||||||
@ -86,6 +86,7 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
|||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
ActorKind::StaticMesh
|
ActorKind::StaticMesh
|
||||||
|
| ActorKind::Brush
|
||||||
| ActorKind::ImportedModel
|
| ActorKind::ImportedModel
|
||||||
| ActorKind::Light
|
| ActorKind::Light
|
||||||
| ActorKind::Empty
|
| ActorKind::Empty
|
||||||
@ -110,7 +111,7 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
|||||||
|
|
||||||
fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
||||||
match kind {
|
match kind {
|
||||||
ActorKind::StaticMesh | ActorKind::ImportedModel => icons::CUBE,
|
ActorKind::StaticMesh | ActorKind::Brush | ActorKind::ImportedModel => icons::CUBE,
|
||||||
ActorKind::Light => icons::LIGHTBULB,
|
ActorKind::Light => icons::LIGHTBULB,
|
||||||
ActorKind::PrefabAnchor => icons::PACKAGE,
|
ActorKind::PrefabAnchor => icons::PACKAGE,
|
||||||
ActorKind::PlayerSpawn => icons::PERSON_SIMPLE_RUN,
|
ActorKind::PlayerSpawn => icons::PERSON_SIMPLE_RUN,
|
||||||
|
|||||||
@ -51,10 +51,31 @@ impl Default for EditorComponentRegistry {
|
|||||||
description: "Renders one or more normalized imported mesh slots.",
|
description: "Renders one or more normalized imported mesh slots.",
|
||||||
search_terms: &["mesh", "renderer", "rendering", "model", "static"],
|
search_terms: &["mesh", "renderer", "rendering", "model", "static"],
|
||||||
recommended: &[],
|
recommended: &[],
|
||||||
conflicts_with: &["shared::components::Primitive"],
|
conflicts_with: &[
|
||||||
|
"shared::components::Primitive",
|
||||||
|
"shared::components::BrushDesc",
|
||||||
|
],
|
||||||
hydration_effect:
|
hydration_effect:
|
||||||
"Hydrates into generated mesh children and material bindings.",
|
"Hydrates into generated mesh children and material bindings.",
|
||||||
},
|
},
|
||||||
|
EditorComponentDescriptor {
|
||||||
|
type_name: "shared::components::BrushDesc",
|
||||||
|
display_name: "Brush",
|
||||||
|
category: EditorComponentCategory::Authoring,
|
||||||
|
addable: true,
|
||||||
|
removable: true,
|
||||||
|
reorderable: true,
|
||||||
|
hidden: false,
|
||||||
|
icon: icons::CUBE.as_str(),
|
||||||
|
description: "Creates authored convex brush geometry for blockout.",
|
||||||
|
search_terms: &["brush", "blockout", "convex", "csg", "geometry"],
|
||||||
|
recommended: &["shared::components::MaterialDesc"],
|
||||||
|
conflicts_with: &[
|
||||||
|
"shared::components::Primitive",
|
||||||
|
"shared::components::StaticMeshRenderer",
|
||||||
|
],
|
||||||
|
hydration_effect: "Hydrates into generated brush mesh children.",
|
||||||
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::MaterialDesc",
|
type_name: "shared::components::MaterialDesc",
|
||||||
display_name: "Authoring Material",
|
display_name: "Authoring Material",
|
||||||
@ -127,7 +148,10 @@ impl Default for EditorComponentRegistry {
|
|||||||
description: "Creates an authored built-in primitive shape.",
|
description: "Creates an authored built-in primitive shape.",
|
||||||
search_terms: &["box", "sphere", "ramp", "primitive"],
|
search_terms: &["box", "sphere", "ramp", "primitive"],
|
||||||
recommended: &[],
|
recommended: &[],
|
||||||
conflicts_with: &["shared::components::StaticMeshRenderer"],
|
conflicts_with: &[
|
||||||
|
"shared::components::StaticMeshRenderer",
|
||||||
|
"shared::components::BrushDesc",
|
||||||
|
],
|
||||||
hydration_effect:
|
hydration_effect:
|
||||||
"Hydrates into generated primitive render and collision data.",
|
"Hydrates into generated primitive render and collision data.",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -89,6 +89,7 @@ pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot {
|
|||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
transform: Transform::from_translation(translation),
|
transform: Transform::from_translation(translation),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -136,6 +137,7 @@ pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity {
|
|||||||
name: Some("Player Start".to_string()),
|
name: Some("Player Start".to_string()),
|
||||||
transform,
|
transform,
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
@ -223,6 +225,7 @@ pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Ent
|
|||||||
name: Some("Scene Sun".to_string()),
|
name: Some("Scene Sun".to_string()),
|
||||||
transform,
|
transform,
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
|
|||||||
@ -106,7 +106,7 @@ fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 {
|
|||||||
.copied()
|
.copied()
|
||||||
.map(|kind| match kind {
|
.map(|kind| match kind {
|
||||||
ActorKind::Empty => 0,
|
ActorKind::Empty => 0,
|
||||||
ActorKind::StaticMesh | ActorKind::ImportedModel => 1,
|
ActorKind::Brush | ActorKind::StaticMesh | ActorKind::ImportedModel => 1,
|
||||||
ActorKind::Light => 2,
|
ActorKind::Light => 2,
|
||||||
ActorKind::PrefabAnchor => 3,
|
ActorKind::PrefabAnchor => 3,
|
||||||
ActorKind::PlayerSpawn => 4,
|
ActorKind::PlayerSpawn => 4,
|
||||||
@ -336,6 +336,7 @@ pub fn actor_kind_icon(kind: ActorKind) -> &'static str {
|
|||||||
use egui_phosphor_icons::icons;
|
use egui_phosphor_icons::icons;
|
||||||
match kind {
|
match kind {
|
||||||
ActorKind::Empty => icons::FOLDER.as_str(),
|
ActorKind::Empty => icons::FOLDER.as_str(),
|
||||||
|
ActorKind::Brush => icons::CUBE.as_str(),
|
||||||
ActorKind::StaticMesh => icons::CUBE.as_str(),
|
ActorKind::StaticMesh => icons::CUBE.as_str(),
|
||||||
ActorKind::ImportedModel => icons::CUBE_TRANSPARENT.as_str(),
|
ActorKind::ImportedModel => icons::CUBE_TRANSPARENT.as_str(),
|
||||||
ActorKind::Light => icons::LIGHTBULB.as_str(),
|
ActorKind::Light => icons::LIGHTBULB.as_str(),
|
||||||
|
|||||||
@ -7,25 +7,25 @@ use bevy::prelude::*;
|
|||||||
use bevy_egui::egui;
|
use bevy_egui::egui;
|
||||||
use egui_phosphor_icons::{icons, Icon};
|
use egui_phosphor_icons::{icons, Icon};
|
||||||
use shared::{
|
use shared::{
|
||||||
inspector_component_active, AuthoringLightKind, AuthoringRigidBody, ColliderDesc,
|
inspector_component_active, AuthoringLightKind, AuthoringRigidBody, BrushDesc, BrushKind,
|
||||||
ColliderShapeDesc, ColorDesc, ComponentInstanceId, EditorAssetRef, InspectorOrder, LevelObject,
|
ColliderDesc, ColliderShapeDesc, ColorDesc, ComponentInstanceId, EditorAssetRef,
|
||||||
LightDesc, MaterialDesc, MaterialParameter, MaterialParameterValue, MaterialShaderKind,
|
InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialParameter,
|
||||||
ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, Primitive,
|
MaterialParameterValue, MaterialShaderKind, ObjectiveMarker, PhysicsBody, PlayerSpawn,
|
||||||
PrimitiveShape, ProjectSun, RigidBodyDesc, StaticMeshRenderer, StaticMeshRendererEntry,
|
PostProcessVolumeDesc, PrefabInstance, Primitive, PrimitiveShape, ProjectSun, RigidBodyDesc,
|
||||||
TeamSpawn, TriggerVolume, WeaponSpawn, AUTHORING_DIRECTIONAL_LUX_MAX,
|
StaticMeshRenderer, StaticMeshRendererEntry, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||||
AUTHORING_POINT_SPOT_LUMENS_MAX, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC,
|
AUTHORING_DIRECTIONAL_LUX_MAX, AUTHORING_POINT_SPOT_LUMENS_MAX, COMPONENT_BRUSH_DESC,
|
||||||
COMPONENT_MATERIAL_DESC, COMPONENT_OBJECTIVE_MARKER, COMPONENT_PHYSICS_BODY,
|
COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC,
|
||||||
COMPONENT_PLAYER_SPAWN, COMPONENT_POST_PROCESS_VOLUME, COMPONENT_PREFAB_INSTANCE,
|
COMPONENT_OBJECTIVE_MARKER, COMPONENT_PHYSICS_BODY, COMPONENT_PLAYER_SPAWN,
|
||||||
COMPONENT_PRIMITIVE, COMPONENT_PROJECT_SUN, COMPONENT_RIGID_BODY_DESC,
|
COMPONENT_POST_PROCESS_VOLUME, COMPONENT_PREFAB_INSTANCE, COMPONENT_PRIMITIVE,
|
||||||
COMPONENT_STATIC_MESH_RENDERER, COMPONENT_TEAM_SPAWN, COMPONENT_TRIGGER_VOLUME,
|
COMPONENT_PROJECT_SUN, COMPONENT_RIGID_BODY_DESC, COMPONENT_STATIC_MESH_RENDERER,
|
||||||
COMPONENT_WEAPON_SPAWN,
|
COMPONENT_TEAM_SPAWN, COMPONENT_TRIGGER_VOLUME, COMPONENT_WEAPON_SPAWN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::history::{
|
use crate::history::{
|
||||||
material_eq, set_collider_with_history, set_inspector_order_with_history,
|
material_eq, set_brush_with_history, set_collider_with_history,
|
||||||
set_light_with_history, set_material_with_history, set_physics_with_history,
|
set_inspector_order_with_history, set_light_with_history, set_material_with_history,
|
||||||
set_post_process_volume_with_history, set_primitive_with_history, set_rigid_body_with_history,
|
set_physics_with_history, set_post_process_volume_with_history, set_primitive_with_history,
|
||||||
set_static_mesh_renderer_with_history, EditorEntitySnapshot,
|
set_rigid_body_with_history, set_static_mesh_renderer_with_history, EditorEntitySnapshot,
|
||||||
};
|
};
|
||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::ui::theme::{
|
use crate::ui::theme::{
|
||||||
@ -131,6 +131,7 @@ enum AddComponentShelfDirection {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum CopiedComponent {
|
enum CopiedComponent {
|
||||||
Primitive(Primitive),
|
Primitive(Primitive),
|
||||||
|
BrushDesc(BrushDesc),
|
||||||
StaticMeshRenderer(StaticMeshRenderer),
|
StaticMeshRenderer(StaticMeshRenderer),
|
||||||
MaterialDesc(MaterialDesc),
|
MaterialDesc(MaterialDesc),
|
||||||
LightDesc(LightDesc),
|
LightDesc(LightDesc),
|
||||||
@ -149,6 +150,7 @@ impl CopiedComponent {
|
|||||||
fn type_name(&self) -> &'static str {
|
fn type_name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Primitive(_) => COMPONENT_PRIMITIVE,
|
Self::Primitive(_) => COMPONENT_PRIMITIVE,
|
||||||
|
Self::BrushDesc(_) => COMPONENT_BRUSH_DESC,
|
||||||
Self::StaticMeshRenderer(_) => COMPONENT_STATIC_MESH_RENDERER,
|
Self::StaticMeshRenderer(_) => COMPONENT_STATIC_MESH_RENDERER,
|
||||||
Self::MaterialDesc(_) => COMPONENT_MATERIAL_DESC,
|
Self::MaterialDesc(_) => COMPONENT_MATERIAL_DESC,
|
||||||
Self::LightDesc(_) => COMPONENT_LIGHT_DESC,
|
Self::LightDesc(_) => COMPONENT_LIGHT_DESC,
|
||||||
@ -357,6 +359,9 @@ fn present_component_type_names(world: &World, entity: Entity) -> Vec<&'static s
|
|||||||
if world.get::<Primitive>(entity).is_some() {
|
if world.get::<Primitive>(entity).is_some() {
|
||||||
present.push(COMPONENT_PRIMITIVE);
|
present.push(COMPONENT_PRIMITIVE);
|
||||||
}
|
}
|
||||||
|
if world.get::<BrushDesc>(entity).is_some() {
|
||||||
|
present.push(COMPONENT_BRUSH_DESC);
|
||||||
|
}
|
||||||
if world.get::<MaterialDesc>(entity).is_some() || world.get::<Primitive>(entity).is_some() {
|
if world.get::<MaterialDesc>(entity).is_some() || world.get::<Primitive>(entity).is_some() {
|
||||||
present.push(COMPONENT_MATERIAL_DESC);
|
present.push(COMPONENT_MATERIAL_DESC);
|
||||||
}
|
}
|
||||||
@ -405,6 +410,10 @@ fn copy_component(world: &mut World, entity: Entity, type_name: &str) {
|
|||||||
.get::<Primitive>(entity)
|
.get::<Primitive>(entity)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(CopiedComponent::Primitive),
|
.map(CopiedComponent::Primitive),
|
||||||
|
COMPONENT_BRUSH_DESC => world
|
||||||
|
.get::<BrushDesc>(entity)
|
||||||
|
.cloned()
|
||||||
|
.map(CopiedComponent::BrushDesc),
|
||||||
COMPONENT_STATIC_MESH_RENDERER => world
|
COMPONENT_STATIC_MESH_RENDERER => world
|
||||||
.get::<StaticMeshRenderer>(entity)
|
.get::<StaticMeshRenderer>(entity)
|
||||||
.cloned()
|
.cloned()
|
||||||
@ -468,6 +477,9 @@ fn paste_component(world: &mut World, entity: Entity, type_name: &str) {
|
|||||||
Some(CopiedComponent::Primitive(value)) if type_name == COMPONENT_PRIMITIVE => {
|
Some(CopiedComponent::Primitive(value)) if type_name == COMPONENT_PRIMITIVE => {
|
||||||
set_primitive_with_history(world, entity, value);
|
set_primitive_with_history(world, entity, value);
|
||||||
}
|
}
|
||||||
|
Some(CopiedComponent::BrushDesc(value)) if type_name == COMPONENT_BRUSH_DESC => {
|
||||||
|
set_brush_with_history(world, entity, value);
|
||||||
|
}
|
||||||
Some(CopiedComponent::StaticMeshRenderer(value))
|
Some(CopiedComponent::StaticMeshRenderer(value))
|
||||||
if type_name == COMPONENT_STATIC_MESH_RENDERER =>
|
if type_name == COMPONENT_STATIC_MESH_RENDERER =>
|
||||||
{
|
{
|
||||||
@ -517,6 +529,7 @@ fn paste_component(world: &mut World, entity: Entity, type_name: &str) {
|
|||||||
fn reset_component(world: &mut World, entity: Entity, type_name: &str) {
|
fn reset_component(world: &mut World, entity: Entity, type_name: &str) {
|
||||||
match type_name {
|
match type_name {
|
||||||
COMPONENT_PRIMITIVE => set_primitive_with_history(world, entity, Primitive::default()),
|
COMPONENT_PRIMITIVE => set_primitive_with_history(world, entity, Primitive::default()),
|
||||||
|
COMPONENT_BRUSH_DESC => set_brush_with_history(world, entity, BrushDesc::default()),
|
||||||
COMPONENT_STATIC_MESH_RENDERER => {
|
COMPONENT_STATIC_MESH_RENDERER => {
|
||||||
set_static_mesh_renderer_with_history(world, entity, StaticMeshRenderer::default());
|
set_static_mesh_renderer_with_history(world, entity, StaticMeshRenderer::default());
|
||||||
}
|
}
|
||||||
@ -575,6 +588,10 @@ fn remove_registered_component(world: &mut World, entity: Entity, type_name: &st
|
|||||||
entity_mut.remove::<Primitive>();
|
entity_mut.remove::<Primitive>();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
COMPONENT_BRUSH_DESC if before.brush.is_some() => {
|
||||||
|
entity_mut.remove::<BrushDesc>();
|
||||||
|
true
|
||||||
|
}
|
||||||
COMPONENT_STATIC_MESH_RENDERER if before.static_mesh_renderer.is_some() => {
|
COMPONENT_STATIC_MESH_RENDERER if before.static_mesh_renderer.is_some() => {
|
||||||
entity_mut.remove::<StaticMeshRenderer>();
|
entity_mut.remove::<StaticMeshRenderer>();
|
||||||
true
|
true
|
||||||
@ -670,6 +687,7 @@ fn draw_authoring_component_by_type(
|
|||||||
) {
|
) {
|
||||||
match type_name {
|
match type_name {
|
||||||
COMPONENT_STATIC_MESH_RENDERER => static_mesh_renderer_ui(world, ui, entity),
|
COMPONENT_STATIC_MESH_RENDERER => static_mesh_renderer_ui(world, ui, entity),
|
||||||
|
COMPONENT_BRUSH_DESC => brush_editor_ui(world, ui, entity),
|
||||||
COMPONENT_PRIMITIVE => primitive_editor_ui(world, ui, entity),
|
COMPONENT_PRIMITIVE => primitive_editor_ui(world, ui, entity),
|
||||||
COMPONENT_MATERIAL_DESC => material_editor_ui(world, ui, entity),
|
COMPONENT_MATERIAL_DESC => material_editor_ui(world, ui, entity),
|
||||||
COMPONENT_LIGHT_DESC => light_editor_ui(world, ui, entity),
|
COMPONENT_LIGHT_DESC => light_editor_ui(world, ui, entity),
|
||||||
@ -1090,6 +1108,7 @@ fn component_display_name(
|
|||||||
fn component_present(world: &World, entity: Entity, type_name: &str) -> bool {
|
fn component_present(world: &World, entity: Entity, type_name: &str) -> bool {
|
||||||
match type_name {
|
match type_name {
|
||||||
"shared::components::Primitive" => world.get::<Primitive>(entity).is_some(),
|
"shared::components::Primitive" => world.get::<Primitive>(entity).is_some(),
|
||||||
|
"shared::components::BrushDesc" => world.get::<BrushDesc>(entity).is_some(),
|
||||||
"shared::components::StaticMeshRenderer" => {
|
"shared::components::StaticMeshRenderer" => {
|
||||||
world.get::<StaticMeshRenderer>(entity).is_some()
|
world.get::<StaticMeshRenderer>(entity).is_some()
|
||||||
}
|
}
|
||||||
@ -1112,10 +1131,19 @@ fn component_present(world: &World, entity: Entity, type_name: &str) -> bool {
|
|||||||
fn insert_registered_component(world: &mut World, entity: Entity, type_name: &str) {
|
fn insert_registered_component(world: &mut World, entity: Entity, type_name: &str) {
|
||||||
match type_name {
|
match type_name {
|
||||||
"shared::components::Primitive" => insert_component(world, entity, |world, e| {
|
"shared::components::Primitive" => insert_component(world, entity, |world, e| {
|
||||||
world.entity_mut(e).insert(Primitive::cuboid(Vec3::ONE));
|
world
|
||||||
|
.entity_mut(e)
|
||||||
|
.insert((shared::ActorKind::StaticMesh, Primitive::cuboid(Vec3::ONE)));
|
||||||
|
}),
|
||||||
|
"shared::components::BrushDesc" => insert_component(world, entity, |world, e| {
|
||||||
|
world
|
||||||
|
.entity_mut(e)
|
||||||
|
.insert((shared::ActorKind::Brush, BrushDesc::default()));
|
||||||
}),
|
}),
|
||||||
"shared::components::StaticMeshRenderer" => insert_component(world, entity, |world, e| {
|
"shared::components::StaticMeshRenderer" => insert_component(world, entity, |world, e| {
|
||||||
world.entity_mut(e).insert(StaticMeshRenderer::default());
|
world
|
||||||
|
.entity_mut(e)
|
||||||
|
.insert((shared::ActorKind::StaticMesh, StaticMeshRenderer::default()));
|
||||||
}),
|
}),
|
||||||
"shared::components::MaterialDesc" => insert_component(world, entity, |world, e| {
|
"shared::components::MaterialDesc" => insert_component(world, entity, |world, e| {
|
||||||
world.entity_mut(e).insert(MaterialDesc::default());
|
world.entity_mut(e).insert(MaterialDesc::default());
|
||||||
@ -1189,6 +1217,7 @@ fn diff_added(before: &EditorEntitySnapshot, after: &EditorEntitySnapshot) -> Ed
|
|||||||
.primitive
|
.primitive
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|_| before.primitive.is_none()),
|
.filter(|_| before.primitive.is_none()),
|
||||||
|
brush: after.brush.clone().filter(|_| before.brush.is_none()),
|
||||||
static_mesh_renderer: after
|
static_mesh_renderer: after
|
||||||
.static_mesh_renderer
|
.static_mesh_renderer
|
||||||
.clone()
|
.clone()
|
||||||
@ -2370,6 +2399,62 @@ pub fn material_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||||
|
let Some(mut brush) = world.get::<BrushDesc>(entity).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let original = brush.clone();
|
||||||
|
let mut changed = false;
|
||||||
|
let mut reset_cube = false;
|
||||||
|
let card = component_card_context(
|
||||||
|
world,
|
||||||
|
entity,
|
||||||
|
ComponentCardOptions::removable(COMPONENT_BRUSH_DESC, "Brush", icons::CUBE),
|
||||||
|
);
|
||||||
|
let card_response = component_card(ui, &card, |ui| {
|
||||||
|
property_row(ui, "Kind", |ui| {
|
||||||
|
egui::ComboBox::from_id_salt("brush_kind")
|
||||||
|
.selected_text(match brush.kind {
|
||||||
|
BrushKind::Additive => "Additive",
|
||||||
|
BrushKind::SubtractiveMarker => "Subtractive Marker",
|
||||||
|
})
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
changed |= ui
|
||||||
|
.selectable_value(&mut brush.kind, BrushKind::Additive, "Additive")
|
||||||
|
.changed();
|
||||||
|
changed |= ui
|
||||||
|
.selectable_value(
|
||||||
|
&mut brush.kind,
|
||||||
|
BrushKind::SubtractiveMarker,
|
||||||
|
"Subtractive Marker",
|
||||||
|
)
|
||||||
|
.changed();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
property_row(ui, "Faces", |ui| {
|
||||||
|
ui.label(format!("{}", brush.faces.len()));
|
||||||
|
});
|
||||||
|
property_row(ui, "Shadows", |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
changed |= ui.checkbox(&mut brush.cast_shadows, "Cast").changed();
|
||||||
|
changed |= ui.checkbox(&mut brush.receive_shadows, "Receive").changed();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if ui.button("Reset Cube Brush").clicked() {
|
||||||
|
reset_cube = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
apply_component_card_response(world, entity, card_response);
|
||||||
|
|
||||||
|
if reset_cube {
|
||||||
|
brush = BrushDesc::default();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if changed && brush != original {
|
||||||
|
set_brush_with_history(world, entity, brush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||||
let Some(mut primitive) = world.get::<Primitive>(entity).cloned() else {
|
let Some(mut primitive) = world.get::<Primitive>(entity).cloned() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -603,6 +603,10 @@ fn icon_for_actor_components(components: ActorIconComponents) -> ActorIconSpec {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
ActorKind::Brush => ActorIconSpec {
|
||||||
|
image: ActorIconImage::Mesh,
|
||||||
|
category: ActorIconCategory::Mesh,
|
||||||
|
},
|
||||||
ActorKind::StaticMesh => {
|
ActorKind::StaticMesh => {
|
||||||
if components.has_primitive || components.has_static_mesh_renderer {
|
if components.has_primitive || components.has_static_mesh_renderer {
|
||||||
ActorIconSpec {
|
ActorIconSpec {
|
||||||
|
|||||||
@ -455,6 +455,7 @@ pub fn spawn_post_process_volume_at_camera(world: &mut World) {
|
|||||||
name: Some("Post Process Volume".into()),
|
name: Some("Post Process Volume".into()),
|
||||||
transform: Transform::from_translation(translation),
|
transform: Transform::from_translation(translation),
|
||||||
primitive: None,
|
primitive: None,
|
||||||
|
brush: None,
|
||||||
static_mesh_renderer: None,
|
static_mesh_renderer: None,
|
||||||
material: None,
|
material: None,
|
||||||
material_override: None,
|
material_override: None,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use bevy::ecs::system::SystemParam;
|
||||||
use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility};
|
use bevy::picking::mesh_picking::ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility};
|
||||||
use bevy::picking::pointer::{PointerAction, PointerInput, PointerInteraction};
|
use bevy::picking::pointer::{PointerAction, PointerInput, PointerInteraction};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
@ -36,6 +37,15 @@ pub struct ViewportPickStack {
|
|||||||
|
|
||||||
pub struct EditorSelectionPlugin;
|
pub struct EditorSelectionPlugin;
|
||||||
|
|
||||||
|
#[derive(SystemParam)]
|
||||||
|
struct PickTargetQueries<'w, 's> {
|
||||||
|
level_objects: Query<'w, 's, Entity, (With<LevelObject>, Without<EditorOnly>)>,
|
||||||
|
proxies: Query<'w, 's, &'static EditorVisualizerProxy>,
|
||||||
|
icon_proxies: Query<'w, 's, &'static ActorIconProxy>,
|
||||||
|
parents: Query<'w, 's, &'static ChildOf>,
|
||||||
|
editor_only: Query<'w, 's, (), With<EditorOnly>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for EditorSelectionPlugin {
|
impl Plugin for EditorSelectionPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<SelectedEntity>()
|
app.init_resource::<SelectedEntity>()
|
||||||
@ -88,9 +98,7 @@ fn handle_pick_events(
|
|||||||
pointers: Query<&PointerInteraction>,
|
pointers: Query<&PointerInteraction>,
|
||||||
editor_cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
editor_cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
mut mesh_ray_cast: MeshRayCast,
|
mut mesh_ray_cast: MeshRayCast,
|
||||||
level_objects: Query<Entity, (With<LevelObject>, Without<EditorOnly>)>,
|
pick_targets: PickTargetQueries,
|
||||||
proxies: Query<&EditorVisualizerProxy>,
|
|
||||||
icon_proxies: Query<&ActorIconProxy>,
|
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
buttons: Res<ButtonInput<MouseButton>>,
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
@ -132,9 +140,7 @@ fn handle_pick_events(
|
|||||||
&mut pick_stack,
|
&mut pick_stack,
|
||||||
&editor_cameras,
|
&editor_cameras,
|
||||||
&mut mesh_ray_cast,
|
&mut mesh_ray_cast,
|
||||||
&level_objects,
|
&pick_targets,
|
||||||
&proxies,
|
|
||||||
&icon_proxies,
|
|
||||||
pointer_pos,
|
pointer_pos,
|
||||||
additive,
|
additive,
|
||||||
);
|
);
|
||||||
@ -152,9 +158,11 @@ fn handle_pick_events(
|
|||||||
|
|
||||||
let mut picked = None;
|
let mut picked = None;
|
||||||
for interaction in &pointers {
|
for interaction in &pointers {
|
||||||
if let Some(entity) = interaction.as_slice().iter().find_map(|(entity, _hit)| {
|
if let Some(entity) = interaction
|
||||||
pick_target_for_entity(*entity, &level_objects, &proxies, &icon_proxies)
|
.as_slice()
|
||||||
}) {
|
.iter()
|
||||||
|
.find_map(|(entity, _hit)| pick_target_for_entity(*entity, &pick_targets))
|
||||||
|
{
|
||||||
picked = Some(entity);
|
picked = Some(entity);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -167,9 +175,7 @@ fn handle_pick_events(
|
|||||||
let hits = collect_viewport_pick_hits(
|
let hits = collect_viewport_pick_hits(
|
||||||
&editor_cameras,
|
&editor_cameras,
|
||||||
&mut mesh_ray_cast,
|
&mut mesh_ray_cast,
|
||||||
&level_objects,
|
&pick_targets,
|
||||||
&proxies,
|
|
||||||
&icon_proxies,
|
|
||||||
ui_state.viewport_rect,
|
ui_state.viewport_rect,
|
||||||
pointer_pos,
|
pointer_pos,
|
||||||
);
|
);
|
||||||
@ -231,18 +237,14 @@ fn apply_viewport_pick(
|
|||||||
pick_stack: &mut ViewportPickStack,
|
pick_stack: &mut ViewportPickStack,
|
||||||
editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
mesh_ray_cast: &mut MeshRayCast,
|
mesh_ray_cast: &mut MeshRayCast,
|
||||||
level_objects: &Query<Entity, (With<LevelObject>, Without<EditorOnly>)>,
|
pick_targets: &PickTargetQueries,
|
||||||
proxies: &Query<&EditorVisualizerProxy>,
|
|
||||||
icon_proxies: &Query<&ActorIconProxy>,
|
|
||||||
pointer_pos: egui::Pos2,
|
pointer_pos: egui::Pos2,
|
||||||
additive: bool,
|
additive: bool,
|
||||||
) {
|
) {
|
||||||
let hits = collect_viewport_pick_hits(
|
let hits = collect_viewport_pick_hits(
|
||||||
editor_cameras,
|
editor_cameras,
|
||||||
mesh_ray_cast,
|
mesh_ray_cast,
|
||||||
level_objects,
|
pick_targets,
|
||||||
proxies,
|
|
||||||
icon_proxies,
|
|
||||||
ui_state.viewport_rect,
|
ui_state.viewport_rect,
|
||||||
pointer_pos,
|
pointer_pos,
|
||||||
);
|
);
|
||||||
@ -277,9 +279,7 @@ fn apply_pick_result(
|
|||||||
fn collect_viewport_pick_hits(
|
fn collect_viewport_pick_hits(
|
||||||
editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
editor_cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
mesh_ray_cast: &mut MeshRayCast,
|
mesh_ray_cast: &mut MeshRayCast,
|
||||||
level_objects: &Query<Entity, (With<LevelObject>, Without<EditorOnly>)>,
|
pick_targets: &PickTargetQueries,
|
||||||
proxies: &Query<&EditorVisualizerProxy>,
|
|
||||||
icon_proxies: &Query<&ActorIconProxy>,
|
|
||||||
scene_rect: egui::Rect,
|
scene_rect: egui::Rect,
|
||||||
pointer_pos: egui::Pos2,
|
pointer_pos: egui::Pos2,
|
||||||
) -> Vec<Entity> {
|
) -> Vec<Entity> {
|
||||||
@ -298,15 +298,7 @@ fn collect_viewport_pick_hits(
|
|||||||
let Some(ray) = scene_view_ray(camera, transform, pointer_pos, scene_rect) else {
|
let Some(ray) = scene_view_ray(camera, transform, pointer_pos, scene_rect) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let filter = |entity: Entity| {
|
let filter = |entity: Entity| pick_target_for_entity(entity, pick_targets).is_some();
|
||||||
level_objects.contains(entity)
|
|
||||||
|| proxies
|
|
||||||
.get(entity)
|
|
||||||
.is_ok_and(|proxy| level_objects.contains(proxy.target) || proxy.target != entity)
|
|
||||||
|| icon_proxies
|
|
||||||
.get(entity)
|
|
||||||
.is_ok_and(|proxy| level_objects.contains(proxy.target))
|
|
||||||
};
|
|
||||||
let settings = MeshRayCastSettings::default()
|
let settings = MeshRayCastSettings::default()
|
||||||
.with_visibility(RayCastVisibility::Any)
|
.with_visibility(RayCastVisibility::Any)
|
||||||
.with_filter(&filter)
|
.with_filter(&filter)
|
||||||
@ -317,13 +309,11 @@ fn collect_viewport_pick_hits(
|
|||||||
let mut ordered = Vec::new();
|
let mut ordered = Vec::new();
|
||||||
for icon_priority in [true, false] {
|
for icon_priority in [true, false] {
|
||||||
for (entity, _) in hits {
|
for (entity, _) in hits {
|
||||||
let is_icon = icon_proxies.get(*entity).is_ok();
|
let is_icon = pick_targets.icon_proxies.get(*entity).is_ok();
|
||||||
if is_icon != icon_priority {
|
if is_icon != icon_priority {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(target) =
|
let Some(target) = pick_target_for_entity(*entity, pick_targets) else {
|
||||||
pick_target_for_entity(*entity, level_objects, proxies, icon_proxies)
|
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if seen.insert(target) {
|
if seen.insert(target) {
|
||||||
@ -334,19 +324,41 @@ fn collect_viewport_pick_hits(
|
|||||||
ordered
|
ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pick_target_for_entity(
|
fn pick_target_for_entity(entity: Entity, pick_targets: &PickTargetQueries) -> Option<Entity> {
|
||||||
entity: Entity,
|
if let Ok(proxy) = pick_targets.icon_proxies.get(entity) {
|
||||||
level_objects: &Query<Entity, (With<LevelObject>, Without<EditorOnly>)>,
|
return pick_targets
|
||||||
proxies: &Query<&EditorVisualizerProxy>,
|
.level_objects
|
||||||
icon_proxies: &Query<&ActorIconProxy>,
|
.contains(proxy.target)
|
||||||
) -> Option<Entity> {
|
.then_some(proxy.target);
|
||||||
if let Ok(proxy) = icon_proxies.get(entity) {
|
|
||||||
return level_objects.contains(proxy.target).then_some(proxy.target);
|
|
||||||
}
|
}
|
||||||
if let Ok(proxy) = proxies.get(entity) {
|
if let Ok(proxy) = pick_targets.proxies.get(entity) {
|
||||||
return Some(proxy.target);
|
return Some(proxy.target);
|
||||||
}
|
}
|
||||||
level_objects.contains(entity).then_some(entity)
|
if pick_targets.editor_only.contains(entity) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if pick_targets.level_objects.contains(entity) {
|
||||||
|
return Some(entity);
|
||||||
|
}
|
||||||
|
let mut current = pick_targets
|
||||||
|
.parents
|
||||||
|
.get(entity)
|
||||||
|
.ok()
|
||||||
|
.map(|parent| parent.parent());
|
||||||
|
while let Some(parent) = current {
|
||||||
|
if pick_targets.editor_only.contains(parent) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if pick_targets.level_objects.contains(parent) {
|
||||||
|
return Some(parent);
|
||||||
|
}
|
||||||
|
current = pick_targets
|
||||||
|
.parents
|
||||||
|
.get(parent)
|
||||||
|
.ok()
|
||||||
|
.map(|parent| parent.parent());
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_gizmo_targets(
|
fn sync_gizmo_targets(
|
||||||
|
|||||||
@ -92,6 +92,9 @@ fn infer_kind_from_block(block: &str) -> &'static str {
|
|||||||
if block.contains("\"shared::components::ModelRef\"") {
|
if block.contains("\"shared::components::ModelRef\"") {
|
||||||
return actor_kind_ron(ActorKind::ImportedModel);
|
return actor_kind_ron(ActorKind::ImportedModel);
|
||||||
}
|
}
|
||||||
|
if block.contains("\"shared::components::BrushDesc\"") {
|
||||||
|
return actor_kind_ron(ActorKind::Brush);
|
||||||
|
}
|
||||||
if block.contains("\"shared::components::Primitive\"")
|
if block.contains("\"shared::components::Primitive\"")
|
||||||
|| block.contains("\"shared::components::StaticMeshRenderer\"")
|
|| block.contains("\"shared::components::StaticMeshRenderer\"")
|
||||||
{
|
{
|
||||||
@ -108,6 +111,7 @@ fn infer_kind_from_block(block: &str) -> &'static str {
|
|||||||
fn actor_kind_ron(kind: ActorKind) -> &'static str {
|
fn actor_kind_ron(kind: ActorKind) -> &'static str {
|
||||||
match kind {
|
match kind {
|
||||||
ActorKind::Empty => "Empty",
|
ActorKind::Empty => "Empty",
|
||||||
|
ActorKind::Brush => "Brush",
|
||||||
ActorKind::StaticMesh => "StaticMesh",
|
ActorKind::StaticMesh => "StaticMesh",
|
||||||
ActorKind::ImportedModel => "ImportedModel",
|
ActorKind::ImportedModel => "ImportedModel",
|
||||||
ActorKind::Light => "Light",
|
ActorKind::Light => "Light",
|
||||||
@ -208,6 +212,29 @@ mod tests {
|
|||||||
assert!(migrated.contains("StaticMesh"));
|
assert!(migrated.contains("StaticMesh"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backfills_actor_kind_for_brush_entity() {
|
||||||
|
let body = r#"(
|
||||||
|
resources: {},
|
||||||
|
entities: {
|
||||||
|
1: (
|
||||||
|
components: {
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::BrushDesc": (
|
||||||
|
kind: Additive,
|
||||||
|
faces: [],
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)"#;
|
||||||
|
let migrated = migrate_v1_to_v2(body);
|
||||||
|
assert!(migrated.contains("ActorKind"));
|
||||||
|
assert!(migrated.contains("Brush"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixes_low_directional_lux() {
|
fn fixes_low_directional_lux() {
|
||||||
let body = r#"(
|
let body = r#"(
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use bevy::ecs::world::EntityRef;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ActorKind, LevelObject, LightDesc, ModelRef, ObjectiveMarker, PlayerSpawn,
|
ActorKind, BrushDesc, LevelObject, LightDesc, ModelRef, ObjectiveMarker, PlayerSpawn,
|
||||||
PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, StaticMeshRenderer, TeamSpawn,
|
PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, StaticMeshRenderer, TeamSpawn,
|
||||||
TriggerVolume, WeaponSpawn,
|
TriggerVolume, WeaponSpawn,
|
||||||
};
|
};
|
||||||
@ -35,6 +35,9 @@ pub fn infer_actor_kind(entity: EntityRef<'_>) -> Option<ActorKind> {
|
|||||||
if entity.get::<LightDesc>().is_some() {
|
if entity.get::<LightDesc>().is_some() {
|
||||||
return Some(ActorKind::Light);
|
return Some(ActorKind::Light);
|
||||||
}
|
}
|
||||||
|
if entity.get::<BrushDesc>().is_some() {
|
||||||
|
return Some(ActorKind::Brush);
|
||||||
|
}
|
||||||
if entity.get::<Primitive>().is_some() || entity.get::<StaticMeshRenderer>().is_some() {
|
if entity.get::<Primitive>().is_some() || entity.get::<StaticMeshRenderer>().is_some() {
|
||||||
return Some(ActorKind::StaticMesh);
|
return Some(ActorKind::StaticMesh);
|
||||||
}
|
}
|
||||||
@ -51,6 +54,11 @@ pub fn infer_actor_kind(entity: EntityRef<'_>) -> Option<ActorKind> {
|
|||||||
pub enum ActorValidationError {
|
pub enum ActorValidationError {
|
||||||
MissingActorKind,
|
MissingActorKind,
|
||||||
MissingTransform,
|
MissingTransform,
|
||||||
|
BrushMissingDesc,
|
||||||
|
BrushHasPrimitive,
|
||||||
|
BrushHasStaticMeshRenderer,
|
||||||
|
BrushHasLight,
|
||||||
|
BrushHasModelRef,
|
||||||
StaticMeshMissingPrimitive,
|
StaticMeshMissingPrimitive,
|
||||||
StaticMeshHasLight,
|
StaticMeshHasLight,
|
||||||
StaticMeshHasModelRef,
|
StaticMeshHasModelRef,
|
||||||
@ -90,6 +98,23 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
ActorKind::Brush => {
|
||||||
|
if entity.get::<BrushDesc>().is_none() {
|
||||||
|
return Err(ActorValidationError::BrushMissingDesc);
|
||||||
|
}
|
||||||
|
if entity.get::<Primitive>().is_some() {
|
||||||
|
return Err(ActorValidationError::BrushHasPrimitive);
|
||||||
|
}
|
||||||
|
if entity.get::<StaticMeshRenderer>().is_some() {
|
||||||
|
return Err(ActorValidationError::BrushHasStaticMeshRenderer);
|
||||||
|
}
|
||||||
|
if entity.get::<LightDesc>().is_some() {
|
||||||
|
return Err(ActorValidationError::BrushHasLight);
|
||||||
|
}
|
||||||
|
if entity.get::<ModelRef>().is_some() {
|
||||||
|
return Err(ActorValidationError::BrushHasModelRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
ActorKind::StaticMesh => {
|
ActorKind::StaticMesh => {
|
||||||
let has_static_mesh_renderer = entity
|
let has_static_mesh_renderer = entity
|
||||||
.get::<StaticMeshRenderer>()
|
.get::<StaticMeshRenderer>()
|
||||||
@ -213,6 +238,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_brush_ok() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let e = level_entity(&mut world, (ActorKind::Brush, BrushDesc::default()));
|
||||||
|
assert!(validate_actor(world.entity(e)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_brush_rejects_static_mesh_renderer() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let e = level_entity(
|
||||||
|
&mut world,
|
||||||
|
(
|
||||||
|
ActorKind::Brush,
|
||||||
|
BrushDesc::default(),
|
||||||
|
StaticMeshRenderer::default(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
validate_actor(world.entity(e)),
|
||||||
|
Err(ActorValidationError::BrushHasStaticMeshRenderer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_post_process_volume_ok() {
|
fn validate_post_process_volume_ok() {
|
||||||
let mut world = World::new();
|
let mut world = World::new();
|
||||||
|
|||||||
@ -146,6 +146,7 @@ pub fn inspector_component_active(order: Option<&InspectorOrder>, type_name: &st
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const COMPONENT_PRIMITIVE: &str = "shared::components::Primitive";
|
pub const COMPONENT_PRIMITIVE: &str = "shared::components::Primitive";
|
||||||
|
pub const COMPONENT_BRUSH_DESC: &str = "shared::components::BrushDesc";
|
||||||
pub const COMPONENT_STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer";
|
pub const COMPONENT_STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer";
|
||||||
pub const COMPONENT_MATERIAL_DESC: &str = "shared::components::MaterialDesc";
|
pub const COMPONENT_MATERIAL_DESC: &str = "shared::components::MaterialDesc";
|
||||||
pub const COMPONENT_MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride";
|
pub const COMPONENT_MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride";
|
||||||
@ -267,6 +268,185 @@ impl Default for Primitive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Brush boolean role. Subtractive brushes are authored now but CSG is applied
|
||||||
|
/// by later tooling.
|
||||||
|
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum BrushKind {
|
||||||
|
#[default]
|
||||||
|
Additive,
|
||||||
|
SubtractiveMarker,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializable convex brush plane. Plane equation is `normal.dot(point) = distance`.
|
||||||
|
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BrushPlaneDesc {
|
||||||
|
pub normal: Vec3,
|
||||||
|
pub distance: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrushPlaneDesc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
normal: Vec3::Y,
|
||||||
|
distance: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One polygon face on a convex authoring brush.
|
||||||
|
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BrushFaceDesc {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: ComponentInstanceId,
|
||||||
|
pub plane: BrushPlaneDesc,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vertices: Vec<Vec3>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub material: Option<EditorAssetRef>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub texture: Option<EditorAssetRef>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub uv_offset: Vec2,
|
||||||
|
#[serde(default = "default_uv_scale")]
|
||||||
|
pub uv_scale: Vec2,
|
||||||
|
#[serde(default)]
|
||||||
|
pub uv_rotation: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub smoothing_group: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrushFaceDesc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: ComponentInstanceId::default(),
|
||||||
|
plane: BrushPlaneDesc::default(),
|
||||||
|
vertices: Vec::new(),
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: Vec2::ZERO,
|
||||||
|
uv_scale: default_uv_scale(),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_uv_scale() -> Vec2 {
|
||||||
|
Vec2::ONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convex brush authoring data. Hydration turns this into generated mesh
|
||||||
|
/// children while the scene stores only this component.
|
||||||
|
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BrushDesc {
|
||||||
|
pub kind: BrushKind,
|
||||||
|
#[serde(default)]
|
||||||
|
pub faces: Vec<BrushFaceDesc>,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub cast_shadows: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub receive_shadows: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrushDesc {
|
||||||
|
pub fn cuboid(size: Vec3) -> Self {
|
||||||
|
let hx = size.x.max(0.001) * 0.5;
|
||||||
|
let hy = size.y.max(0.001) * 0.5;
|
||||||
|
let hz = size.z.max(0.001) * 0.5;
|
||||||
|
let faces = vec![
|
||||||
|
brush_face(
|
||||||
|
"face:+x",
|
||||||
|
Vec3::X,
|
||||||
|
vec![
|
||||||
|
Vec3::new(hx, -hy, -hz),
|
||||||
|
Vec3::new(hx, hy, -hz),
|
||||||
|
Vec3::new(hx, hy, hz),
|
||||||
|
Vec3::new(hx, -hy, hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
brush_face(
|
||||||
|
"face:-x",
|
||||||
|
Vec3::NEG_X,
|
||||||
|
vec![
|
||||||
|
Vec3::new(-hx, -hy, hz),
|
||||||
|
Vec3::new(-hx, hy, hz),
|
||||||
|
Vec3::new(-hx, hy, -hz),
|
||||||
|
Vec3::new(-hx, -hy, -hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
brush_face(
|
||||||
|
"face:+y",
|
||||||
|
Vec3::Y,
|
||||||
|
vec![
|
||||||
|
Vec3::new(-hx, hy, -hz),
|
||||||
|
Vec3::new(-hx, hy, hz),
|
||||||
|
Vec3::new(hx, hy, hz),
|
||||||
|
Vec3::new(hx, hy, -hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
brush_face(
|
||||||
|
"face:-y",
|
||||||
|
Vec3::NEG_Y,
|
||||||
|
vec![
|
||||||
|
Vec3::new(-hx, -hy, hz),
|
||||||
|
Vec3::new(-hx, -hy, -hz),
|
||||||
|
Vec3::new(hx, -hy, -hz),
|
||||||
|
Vec3::new(hx, -hy, hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
brush_face(
|
||||||
|
"face:+z",
|
||||||
|
Vec3::Z,
|
||||||
|
vec![
|
||||||
|
Vec3::new(hx, -hy, hz),
|
||||||
|
Vec3::new(hx, hy, hz),
|
||||||
|
Vec3::new(-hx, hy, hz),
|
||||||
|
Vec3::new(-hx, -hy, hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
brush_face(
|
||||||
|
"face:-z",
|
||||||
|
Vec3::NEG_Z,
|
||||||
|
vec![
|
||||||
|
Vec3::new(-hx, -hy, -hz),
|
||||||
|
Vec3::new(-hx, hy, -hz),
|
||||||
|
Vec3::new(hx, hy, -hz),
|
||||||
|
Vec3::new(hx, -hy, -hz),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
Self {
|
||||||
|
kind: BrushKind::Additive,
|
||||||
|
faces,
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrushDesc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::cuboid(Vec3::ONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn brush_face(id: &str, normal: Vec3, vertices: Vec<Vec3>) -> BrushFaceDesc {
|
||||||
|
let distance = vertices
|
||||||
|
.first()
|
||||||
|
.map(|vertex| normal.dot(*vertex))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
BrushFaceDesc {
|
||||||
|
id: ComponentInstanceId::new(id),
|
||||||
|
plane: BrushPlaneDesc { normal, distance },
|
||||||
|
vertices,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Legacy static mesh collision setting kept only for loading older scenes.
|
/// Legacy static mesh collision setting kept only for loading older scenes.
|
||||||
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
@ -736,6 +916,7 @@ impl Default for EditorVisibility {
|
|||||||
#[reflect(Component, Debug, PartialEq, Serialize, Deserialize)]
|
#[reflect(Component, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ActorKind {
|
pub enum ActorKind {
|
||||||
Empty,
|
Empty,
|
||||||
|
Brush,
|
||||||
StaticMesh,
|
StaticMesh,
|
||||||
ImportedModel,
|
ImportedModel,
|
||||||
Light,
|
Light,
|
||||||
|
|||||||
231
crates/shared/src/hydration/brushes.rs
Normal file
231
crates/shared/src/hydration/brushes.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
//! Convex brush hydration from authored face polygons.
|
||||||
|
|
||||||
|
use avian3d::prelude::ColliderConstructor;
|
||||||
|
use bevy::asset::RenderAssetUsages;
|
||||||
|
use bevy::light::{NotShadowCaster, NotShadowReceiver};
|
||||||
|
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
inspector_component_active, BrushDesc, BrushFaceDesc, ColliderDesc, InspectorOrder,
|
||||||
|
LevelObject, MaterialDesc, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC,
|
||||||
|
COMPONENT_MATERIAL_DESC,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::materials::material_from_desc;
|
||||||
|
|
||||||
|
/// Runtime marker for generated brush render/collider children.
|
||||||
|
#[derive(Component, Reflect, Default, Debug, Clone, Copy)]
|
||||||
|
#[reflect(Component, Default, Debug)]
|
||||||
|
pub struct HydratedBrushMesh;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn hydrate_brushes(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
brushes: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&BrushDesc,
|
||||||
|
Option<&MaterialDesc>,
|
||||||
|
Option<&ColliderDesc>,
|
||||||
|
Option<&InspectorOrder>,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
With<LevelObject>,
|
||||||
|
Or<(
|
||||||
|
Added<BrushDesc>,
|
||||||
|
Changed<BrushDesc>,
|
||||||
|
Changed<MaterialDesc>,
|
||||||
|
Changed<ColliderDesc>,
|
||||||
|
Changed<InspectorOrder>,
|
||||||
|
)>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
children: Query<&Children>,
|
||||||
|
generated_brushes: Query<(), With<HydratedBrushMesh>>,
|
||||||
|
) {
|
||||||
|
for (entity, brush, material, collider, order) in &brushes {
|
||||||
|
despawn_brush_mesh(&mut commands, entity, &children, &generated_brushes);
|
||||||
|
if !inspector_component_active(order, COMPONENT_BRUSH_DESC) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spawn_brush_mesh(
|
||||||
|
&mut commands,
|
||||||
|
&asset_server,
|
||||||
|
&mut meshes,
|
||||||
|
&mut materials,
|
||||||
|
entity,
|
||||||
|
brush,
|
||||||
|
material.filter(|_| inspector_component_active(order, COMPONENT_MATERIAL_DESC)),
|
||||||
|
collider.filter(|_| inspector_component_active(order, COMPONENT_COLLIDER_DESC)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_brush_mesh(
|
||||||
|
commands: &mut Commands,
|
||||||
|
asset_server: &AssetServer,
|
||||||
|
meshes: &mut Assets<Mesh>,
|
||||||
|
materials: &mut Assets<StandardMaterial>,
|
||||||
|
parent: Entity,
|
||||||
|
brush: &BrushDesc,
|
||||||
|
material: Option<&MaterialDesc>,
|
||||||
|
collider: Option<&ColliderDesc>,
|
||||||
|
) {
|
||||||
|
let Some(mesh) = brush_mesh(brush) else {
|
||||||
|
warn!("Brush hydration skipped invalid brush on {parent:?}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mesh = meshes.add(mesh);
|
||||||
|
let material = material
|
||||||
|
.map(|material| materials.add(material_from_desc(asset_server, material)))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.64, 0.68, 0.72),
|
||||||
|
perceptual_roughness: 0.8,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut brush_child = commands.spawn((
|
||||||
|
HydratedBrushMesh,
|
||||||
|
Name::new("Hydrated Brush Mesh"),
|
||||||
|
Mesh3d(mesh),
|
||||||
|
MeshMaterial3d(material),
|
||||||
|
Transform::default(),
|
||||||
|
Visibility::Visible,
|
||||||
|
ChildOf(parent),
|
||||||
|
));
|
||||||
|
if !brush.cast_shadows {
|
||||||
|
brush_child.insert(NotShadowCaster);
|
||||||
|
}
|
||||||
|
if !brush.receive_shadows {
|
||||||
|
brush_child.insert(NotShadowReceiver);
|
||||||
|
}
|
||||||
|
if collider.is_some_and(|collider| collider.enabled) {
|
||||||
|
brush_child.insert(ColliderConstructor::TrimeshFromMesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn despawn_brush_mesh(
|
||||||
|
commands: &mut Commands,
|
||||||
|
entity: Entity,
|
||||||
|
children: &Query<&Children>,
|
||||||
|
generated_brushes: &Query<(), With<HydratedBrushMesh>>,
|
||||||
|
) {
|
||||||
|
let Ok(child_list) = children.get(entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for child in child_list.iter() {
|
||||||
|
if generated_brushes.get(child).is_ok() {
|
||||||
|
commands.entity(child).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn brush_mesh(brush: &BrushDesc) -> Option<Mesh> {
|
||||||
|
let mut positions = Vec::<[f32; 3]>::new();
|
||||||
|
let mut normals = Vec::<[f32; 3]>::new();
|
||||||
|
let mut uvs = Vec::<[f32; 2]>::new();
|
||||||
|
let mut indices = Vec::<u32>::new();
|
||||||
|
|
||||||
|
for face in &brush.faces {
|
||||||
|
append_face(face, &mut positions, &mut normals, &mut uvs, &mut indices)?;
|
||||||
|
}
|
||||||
|
if positions.is_empty() || indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mesh = Mesh::new(
|
||||||
|
PrimitiveTopology::TriangleList,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
);
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||||
|
mesh.insert_indices(Indices::U32(indices));
|
||||||
|
if let Err(error) = mesh.generate_tangents() {
|
||||||
|
warn!("Brush mesh is not Solari-compatible: failed to generate tangents: {error:?}");
|
||||||
|
}
|
||||||
|
Some(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_face(
|
||||||
|
face: &BrushFaceDesc,
|
||||||
|
positions: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
uvs: &mut Vec<[f32; 2]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) -> Option<()> {
|
||||||
|
if face.vertices.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !face.vertices.iter().all(|vertex| vertex.is_finite()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let normal = face.plane.normal.try_normalize()?;
|
||||||
|
if !normal.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let base = u32::try_from(positions.len()).ok()?;
|
||||||
|
let (u_axis, v_axis) = face_axes(normal);
|
||||||
|
for vertex in &face.vertices {
|
||||||
|
positions.push([vertex.x, vertex.y, vertex.z]);
|
||||||
|
normals.push([normal.x, normal.y, normal.z]);
|
||||||
|
let uv = Vec2::new(vertex.dot(u_axis), vertex.dot(v_axis)) * face.uv_scale + face.uv_offset;
|
||||||
|
uvs.push([uv.x, uv.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let flip = (face.vertices[1] - face.vertices[0])
|
||||||
|
.cross(face.vertices[2] - face.vertices[0])
|
||||||
|
.dot(normal)
|
||||||
|
< 0.0;
|
||||||
|
for i in 1..(face.vertices.len() - 1) {
|
||||||
|
let i = u32::try_from(i).ok()?;
|
||||||
|
if flip {
|
||||||
|
indices.extend_from_slice(&[base, base + i + 1, base + i]);
|
||||||
|
} else {
|
||||||
|
indices.extend_from_slice(&[base, base + i, base + i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_axes(normal: Vec3) -> (Vec3, Vec3) {
|
||||||
|
let tangent_seed = if normal.y.abs() > 0.9 {
|
||||||
|
Vec3::X
|
||||||
|
} else {
|
||||||
|
Vec3::Y
|
||||||
|
};
|
||||||
|
let u = tangent_seed.cross(normal).normalize_or_zero();
|
||||||
|
let v = normal.cross(u).normalize_or_zero();
|
||||||
|
(u, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::BrushDesc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cube_brush_builds_triangle_mesh() {
|
||||||
|
let mesh = brush_mesh(&BrushDesc::default()).expect("cube brush mesh");
|
||||||
|
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
|
||||||
|
assert!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).is_some());
|
||||||
|
assert!(mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some());
|
||||||
|
assert!(mesh.attribute(Mesh::ATTRIBUTE_UV_0).is_some());
|
||||||
|
assert!(mesh.attribute(Mesh::ATTRIBUTE_TANGENT).is_some());
|
||||||
|
assert!(matches!(mesh.indices(), Some(Indices::U32(indices)) if indices.len() == 36));
|
||||||
|
assert!(mesh.enable_raytracing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_brush_returns_none() {
|
||||||
|
let mut brush = BrushDesc::default();
|
||||||
|
brush.faces[0].vertices.clear();
|
||||||
|
assert!(brush_mesh(&brush).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
//! Generates runtime render/physics/light components from reflectable authoring data.
|
//! Generates runtime render/physics/light components from reflectable authoring data.
|
||||||
|
|
||||||
|
mod brushes;
|
||||||
mod lights;
|
mod lights;
|
||||||
mod materials;
|
mod materials;
|
||||||
mod models;
|
mod models;
|
||||||
@ -16,6 +17,7 @@ pub use strip::{strip_hydrated, strip_hydrated_entity};
|
|||||||
use bevy::ecs::system::SystemState;
|
use bevy::ecs::system::SystemState;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use brushes::{hydrate_brushes, spawn_brush_mesh, HydratedBrushMesh};
|
||||||
use lights::{hydrate_lights, reconcile_missing_runtime_lights};
|
use lights::{hydrate_lights, reconcile_missing_runtime_lights};
|
||||||
use materials::hydrate_materials;
|
use materials::hydrate_materials;
|
||||||
pub use materials::material_from_desc;
|
pub use materials::material_from_desc;
|
||||||
@ -30,10 +32,10 @@ use static_meshes::{
|
|||||||
use visibility::{init_editor_visibility_on_spawn, sync_editor_visibility};
|
use visibility::{init_editor_visibility_on_spawn, sync_editor_visibility};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
inspector_component_active, ColliderDesc, EditorVisibility, InspectorOrder, LevelObject,
|
inspector_component_active, BrushDesc, ColliderDesc, EditorVisibility, InspectorOrder,
|
||||||
MaterialDesc, MaterialOverride, Primitive, StaticMeshRenderer, COMPONENT_COLLIDER_DESC,
|
LevelObject, MaterialDesc, MaterialOverride, Primitive, StaticMeshRenderer,
|
||||||
COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC, COMPONENT_PRIMITIVE,
|
COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC,
|
||||||
COMPONENT_STATIC_MESH_RENDERER,
|
COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Registers hydration systems in deterministic order.
|
/// Registers hydration systems in deterministic order.
|
||||||
@ -46,6 +48,7 @@ impl Plugin for HydrationPlugin {
|
|||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
hydrate_primitives,
|
hydrate_primitives,
|
||||||
|
hydrate_brushes,
|
||||||
hydrate_materials,
|
hydrate_materials,
|
||||||
hydrate_lights,
|
hydrate_lights,
|
||||||
reconcile_missing_runtime_lights,
|
reconcile_missing_runtime_lights,
|
||||||
@ -83,6 +86,45 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
.map(|(entity, desc, _)| (entity, desc.clone()))
|
.map(|(entity, desc, _)| (entity, desc.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let brush_entities_for_cleanup: Vec<Entity> = world
|
||||||
|
.query_filtered::<(Entity, &BrushDesc), With<LevelObject>>()
|
||||||
|
.iter(world)
|
||||||
|
.map(|(entity, _)| entity)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let brush_targets: Vec<(
|
||||||
|
Entity,
|
||||||
|
BrushDesc,
|
||||||
|
Option<MaterialDesc>,
|
||||||
|
Option<ColliderDesc>,
|
||||||
|
Option<InspectorOrder>,
|
||||||
|
)> = world
|
||||||
|
.query_filtered::<(
|
||||||
|
Entity,
|
||||||
|
&BrushDesc,
|
||||||
|
Option<&MaterialDesc>,
|
||||||
|
Option<&ColliderDesc>,
|
||||||
|
Option<&InspectorOrder>,
|
||||||
|
), With<LevelObject>>()
|
||||||
|
.iter(world)
|
||||||
|
.map(|(entity, brush, material, collider, order)| {
|
||||||
|
(
|
||||||
|
entity,
|
||||||
|
brush.clone(),
|
||||||
|
material
|
||||||
|
.filter(|_| inspector_component_active(order, COMPONENT_MATERIAL_DESC))
|
||||||
|
.cloned(),
|
||||||
|
collider
|
||||||
|
.filter(|_| inspector_component_active(order, COMPONENT_COLLIDER_DESC))
|
||||||
|
.cloned(),
|
||||||
|
order.cloned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|(_, _, _, _, order)| {
|
||||||
|
inspector_component_active(order.as_ref(), COMPONENT_BRUSH_DESC)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let static_mesh_entities_for_cleanup: Vec<Entity> = world
|
let static_mesh_entities_for_cleanup: Vec<Entity> = world
|
||||||
.query_filtered::<(Entity, &StaticMeshRenderer), With<LevelObject>>()
|
.query_filtered::<(Entity, &StaticMeshRenderer), With<LevelObject>>()
|
||||||
.iter(world)
|
.iter(world)
|
||||||
@ -138,6 +180,17 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut generated_brush_meshes = Vec::new();
|
||||||
|
for entity in &brush_entities_for_cleanup {
|
||||||
|
if let Some(children) = world.get::<Children>(*entity) {
|
||||||
|
for child in children.iter() {
|
||||||
|
if world.get::<HydratedBrushMesh>(child).is_some() {
|
||||||
|
generated_brush_meshes.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let visibility_targets: Vec<(Entity, EditorVisibility)> = world
|
let visibility_targets: Vec<(Entity, EditorVisibility)> = world
|
||||||
.query_filtered::<(Entity, &EditorVisibility), With<LevelObject>>()
|
.query_filtered::<(Entity, &EditorVisibility), With<LevelObject>>()
|
||||||
.iter(world)
|
.iter(world)
|
||||||
@ -187,6 +240,23 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
commands.entity(entity).insert(MeshMaterial3d(material));
|
commands.entity(entity).insert(MeshMaterial3d(material));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for child in generated_brush_meshes {
|
||||||
|
commands.entity(child).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, brush, material, collider, _) in &brush_targets {
|
||||||
|
spawn_brush_mesh(
|
||||||
|
&mut commands,
|
||||||
|
&asset_server,
|
||||||
|
&mut meshes,
|
||||||
|
&mut materials,
|
||||||
|
*entity,
|
||||||
|
brush,
|
||||||
|
material.as_ref(),
|
||||||
|
collider.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for child in generated_static_mesh_parts {
|
for child in generated_static_mesh_parts {
|
||||||
commands.entity(child).despawn();
|
commands.entity(child).despawn();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,10 @@ impl Plugin for SharedTypesPlugin {
|
|||||||
.register_type::<ProjectSun>()
|
.register_type::<ProjectSun>()
|
||||||
.register_type::<Primitive>()
|
.register_type::<Primitive>()
|
||||||
.register_type::<PrimitiveShape>()
|
.register_type::<PrimitiveShape>()
|
||||||
|
.register_type::<BrushDesc>()
|
||||||
|
.register_type::<BrushKind>()
|
||||||
|
.register_type::<BrushPlaneDesc>()
|
||||||
|
.register_type::<BrushFaceDesc>()
|
||||||
.register_type::<StaticMeshRenderer>()
|
.register_type::<StaticMeshRenderer>()
|
||||||
.register_type::<StaticMeshRendererEntry>()
|
.register_type::<StaticMeshRendererEntry>()
|
||||||
.register_type::<StaticMeshAssetRef>()
|
.register_type::<StaticMeshAssetRef>()
|
||||||
|
|||||||
@ -35,6 +35,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
||||||
| [0019](adr/0019-local-bevy-render-timeout-patch.md) | Local `bevy_render` patch for transient Linux swapchain timeouts |
|
| [0019](adr/0019-local-bevy-render-timeout-patch.md) | Local `bevy_render` patch for transient Linux swapchain timeouts |
|
||||||
| [0020](adr/0020-jackdaw-inspired-editor-roadmap.md) | Jackdaw-inspired roadmap source policy and attribution requirements |
|
| [0020](adr/0020-jackdaw-inspired-editor-roadmap.md) | Jackdaw-inspired roadmap source policy and attribution requirements |
|
||||||
|
| [0021](adr/0021-brush-authoring-schema.md) | Brush authoring schema and hydration contract |
|
||||||
|
|
||||||
## Editor framework
|
## Editor framework
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [editor/roadmap.md](editor/roadmap.md) | Phased editor roadmap and status |
|
| [editor/roadmap.md](editor/roadmap.md) | Phased editor roadmap and status |
|
||||||
| [editor/brp.md](editor/brp.md) | BRP automation and authoring-only policy |
|
| [editor/brp.md](editor/brp.md) | BRP automation and authoring-only policy |
|
||||||
| [editor/debt-audit.md](editor/debt-audit.md) | Zero-debt phase gates |
|
| [editor/debt-audit.md](editor/debt-audit.md) | Zero-debt phase gates |
|
||||||
|
| [editor/brushes.md](editor/brushes.md) | Brush authoring schema, hydration behavior, and MVP limits |
|
||||||
|
|
||||||
## Working plans (not canonical long-term)
|
## Working plans (not canonical long-term)
|
||||||
|
|
||||||
|
|||||||
21
docs/adr/0021-brush-authoring-schema.md
Normal file
21
docs/adr/0021-brush-authoring-schema.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# ADR 0021: Brush Authoring Schema
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Jackdaw roadmap work needs blockout geometry that is editable as authored data, survives scene saves, hydrates into runtime render/physics components, and can later support face/edge/vertex tools and CSG operators. Treating brushes as imported static meshes would hide the authoring intent and make later geometry edits depend on generated artifacts.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add `ActorKind::Brush` and `BrushDesc` as persisted shared authoring data. A brush stores convex polygon faces with stable face IDs, plane data, material/texture references, UV parameters, smoothing group, and shadow flags. The MVP supports additive cube brushes and hydrates active brush components into generated mesh children; generated children are stripped from saved scenes.
|
||||||
|
|
||||||
|
Brush actors conflict with `Primitive`, `StaticMeshRenderer`, `LightDesc`, and `ModelRef` in actor validation. Scene v1-to-v2 kind backfill infers `ActorKind::Brush` when an older level object contains `BrushDesc`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Brush geometry is now a first-class scene authoring type rather than a renderer slot variant. Future face editing, clipping, and CSG tools should mutate `BrushDesc` and rebuild the generated preview mesh through hydration.
|
||||||
|
|
||||||
|
The MVP intentionally does not implement viewport face/edge/vertex tools, non-convex validation UI, or subtractive CSG evaluation. Those remain roadmap work.
|
||||||
@ -11,12 +11,14 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| [brp.md](brp.md) | Remote protocol, authoring-only policy, commands |
|
| [brp.md](brp.md) | Remote protocol, authoring-only policy, commands |
|
||||||
| [debt-audit.md](debt-audit.md) | Zero-debt phase gates and remaining items |
|
| [debt-audit.md](debt-audit.md) | Zero-debt phase gates and remaining items |
|
||||||
| [rendering.md](rendering.md) | GI modes, post-process volumes, presets, post FX assets |
|
| [rendering.md](rendering.md) | GI modes, post-process volumes, presets, post FX assets |
|
||||||
|
| [brushes.md](brushes.md) | Brush authoring schema, hydration behavior, and MVP limits |
|
||||||
|
|
||||||
## Subsystems (code → doc)
|
## Subsystems (code → doc)
|
||||||
|
|
||||||
| Code module | Responsibility | Doc |
|
| Code module | Responsibility | Doc |
|
||||||
|-------------|----------------|-----|
|
|-------------|----------------|-----|
|
||||||
| `lib.rs` / `EditorPluginGroup` | Ordered plugin bundle, lib/bin split | ADR 0008, architecture.md |
|
| `lib.rs` / `EditorPluginGroup` | Ordered plugin bundle, lib/bin split | ADR 0008, architecture.md |
|
||||||
|
| `shared::components::BrushDesc` / `shared::hydration::brushes` | Persisted brush authoring data and generated runtime mesh hydration | brushes.md, ADR 0021 |
|
||||||
| `scene/` | Level I/O, schema, viewport render-target setup | architecture.md |
|
| `scene/` | Level I/O, schema, viewport render-target setup | architecture.md |
|
||||||
| `viewport/` | Camera, selection, gizmos, render views | architecture.md |
|
| `viewport/` | Camera, selection, gizmos, render views | architecture.md |
|
||||||
| `play/` | PIE session, editor mode state | architecture.md |
|
| `play/` | PIE session, editor mode state | architecture.md |
|
||||||
@ -41,6 +43,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
|
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
|
||||||
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`.
|
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`.
|
||||||
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
|
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
|
||||||
|
- **Brush authoring** — `ActorKind::Brush + BrushDesc` stores persisted convex blockout faces and hydrates active brushes into generated mesh children. See [brushes.md](brushes.md).
|
||||||
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
||||||
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits live on the actor `MaterialDesc`; static mesh slot material refs are source/default selectors, and content-browser asset inspectors are the path for editing shared material assets.
|
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits live on the actor `MaterialDesc`; static mesh slot material refs are source/default selectors, and content-browser asset inspectors are the path for editing shared material assets.
|
||||||
- **Prefab overrides** — `PrefabInstance.overrides_ron` stores transform/material/child-visibility overrides; inspector collapsible groups with **Apply** / **Revert** (replaces transform-only revert v1).
|
- **Prefab overrides** — `PrefabInstance.overrides_ron` stores transform/material/child-visibility overrides; inspector collapsible groups with **Apply** / **Revert** (replaces transform-only revert v1).
|
||||||
@ -57,7 +60,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
||||||
- **Viewport shading** (toolbar): **Lit** (default WYSIWYG), **Unlit** (base-color debug), **Colliders** (hide meshes, emphasize collider gizmos). Light gizmos show range spheres, spot cones, and directional sun arrows. A **mode badge** (bottom-right of the viewport) shows the active mode; Collider mode displays a warning banner.
|
- **Viewport shading** (toolbar): **Lit** (default WYSIWYG), **Unlit** (base-color debug), **Colliders** (hide meshes, emphasize collider gizmos). Light gizmos show range spheres, spot cones, and directional sun arrows. A **mode badge** (bottom-right of the viewport) shows the active mode; Collider mode displays a warning banner.
|
||||||
- **Hierarchy** sort modes (**Manual**, **Name**, **Type**) use stable tie-breakers so duplicate names (e.g. several `Pillar` rows) do not reorder on every click. Drag-and-drop reparenting disables panel scroll-to-drag; drop indicators appear only while dragging.
|
- **Hierarchy** sort modes (**Manual**, **Name**, **Type**) use stable tie-breakers so duplicate names (e.g. several `Pillar` rows) do not reorder on every click. Drag-and-drop reparenting disables panel scroll-to-drag; drop indicators appear only while dragging.
|
||||||
- **Viewport picking**: click selects nearest object, actor root icon, or visualizer hit, with actor icons preferred when clicked over meshes; **Tab** cycles overlapping targets at the last click (selection pin deferred).
|
- **Viewport picking**: click selects nearest object, generated mesh child resolved to its authored actor, actor root icon, or visualizer hit, with actor icons preferred when clicked over meshes; **Tab** cycles overlapping targets at the last click (selection pin deferred).
|
||||||
- **Settings data** lives in `crates/settings`; **settings UI** lives in `settings_ui.rs`.
|
- **Settings data** lives in `crates/settings`; **settings UI** lives in `settings_ui.rs`.
|
||||||
|
|
||||||
When implementing or changing any of the above, update this file and the root README in the same change.
|
When implementing or changing any of the above, update this file and the root README in the same change.
|
||||||
|
|||||||
@ -147,6 +147,7 @@ On play enter/exit the editor snapshots **player sim state only** (transform, ve
|
|||||||
3. Player placement is stored as `PlayerSpawn`; the runtime `Player` is never serialized.
|
3. Player placement is stored as `PlayerSpawn`; the runtime `Player` is never serialized.
|
||||||
4. `SceneIo` writes native Bevy dynamic scenes under `assets/levels/`, routed through the Bevy-free `scene::document::SceneDocument` seam. The document layer preserves the current RON storage while exposing stable actor/component document concepts and patch vocabulary for tools.
|
4. `SceneIo` writes native Bevy dynamic scenes under `assets/levels/`, routed through the Bevy-free `scene::document::SceneDocument` seam. The document layer preserves the current RON storage while exposing stable actor/component document concepts and patch vocabulary for tools.
|
||||||
5. Hydration systems in `game` spawn meshes, colliders, lights, and static mesh renderer parts at runtime. `ActorId` and `ComponentInstanceId` are the persisted identities; raw Bevy `Entity` IDs are runtime-only and must not become tool-facing document IDs.
|
5. Hydration systems in `game` spawn meshes, colliders, lights, and static mesh renderer parts at runtime. `ActorId` and `ComponentInstanceId` are the persisted identities; raw Bevy `Entity` IDs are runtime-only and must not become tool-facing document IDs.
|
||||||
|
6. Brush actors store authored convex geometry as `BrushDesc`; hydration rebuilds generated mesh children from those faces and strips the children before save.
|
||||||
|
|
||||||
## Model import (glTF + FBX)
|
## Model import (glTF + FBX)
|
||||||
|
|
||||||
|
|||||||
17
docs/editor/brushes.md
Normal file
17
docs/editor/brushes.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Brush Authoring
|
||||||
|
|
||||||
|
Brushes are persisted blockout geometry stored as `ActorKind::Brush + BrushDesc`.
|
||||||
|
|
||||||
|
## MVP Scope
|
||||||
|
|
||||||
|
- `BrushDesc::default()` creates an additive cube brush.
|
||||||
|
- Each face stores a stable ID, plane, vertices, optional material/texture refs, UV offset/scale/rotation, and smoothing group.
|
||||||
|
- Hydration builds generated mesh children from active brush components and strips those children before scene save.
|
||||||
|
- Brush actors validate separately from primitives and imported/static mesh actors.
|
||||||
|
- The Actor Inspector Add Component shelf can add a **Brush** component. The Brush card exposes kind, face count, shadow flags, and cube reset.
|
||||||
|
|
||||||
|
## Current Limits
|
||||||
|
|
||||||
|
- Only convex authored faces are supported by the mesh builder.
|
||||||
|
- Subtractive brushes are stored as markers but are not evaluated as CSG.
|
||||||
|
- Face, edge, vertex, clipping, and primitive brush creation tools are future roadmap work.
|
||||||
Loading…
Reference in New Issue
Block a user