Add collider authoring diagnostics
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
0e7aed40f1
commit
0ce43fc399
31
.cursor/plans/collider_diagnostics_2026-07-12.plan.md
Normal file
31
.cursor/plans/collider_diagnostics_2026-07-12.plan.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Collider Authoring Diagnostics - 2026-07-12
|
||||||
|
|
||||||
|
Issue: Gitea #26 (`BS-JD-305`)
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Collider authoring exposes one consistent health result to the inspector, Collider viewport,
|
||||||
|
Diagnostics panel, and physics placement preflight. Shape changes preserve useful dimensions and
|
||||||
|
remain one typed undo operation.
|
||||||
|
|
||||||
|
## Contract
|
||||||
|
|
||||||
|
- Authored box, sphere, and capsule overlays include actor world scale.
|
||||||
|
- Static mesh colliders show cooked runtime bounds when available.
|
||||||
|
- Green is ready, cyan is a trigger, gray is disabled, amber is warning/stale, and red is invalid
|
||||||
|
or missing.
|
||||||
|
- Invalid dimensions and mesh references block dependent workflows. Missing hydration and
|
||||||
|
colliders over 1,000 meters in world extent are warnings.
|
||||||
|
- Physics placement consumes the same collider-health checks instead of maintaining a separate
|
||||||
|
prerequisite definition.
|
||||||
|
|
||||||
|
## Work
|
||||||
|
|
||||||
|
- [x] Add shared authored/runtime collider diagnostics and focused fixtures.
|
||||||
|
- [x] Add shape/status-aware viewport overlays and selected-collider HUD state.
|
||||||
|
- [x] Add inspector health/repair feedback and dimension-preserving undoable shape switching.
|
||||||
|
- [x] Add scene-wide selectable collider findings to Diagnostics.
|
||||||
|
- [x] Reuse diagnostics in physics placement prerequisites.
|
||||||
|
- [x] Add a committed non-blocking showcase scene and update documentation.
|
||||||
|
- [x] Run source gates and live Wayland acceptance; packaged tests remain skipped by request.
|
||||||
|
- [x] Commit, push, attach native Gitea evidence, close #26, and close M3 when empty.
|
||||||
@ -144,6 +144,7 @@ deep-stale variants.
|
|||||||
| Brush selected: `1` / `2` / `3` / `4` | Vertex / edge / face / clip edit modes |
|
| Brush selected: `1` / `2` / `3` / `4` | Vertex / edge / face / clip edit modes |
|
||||||
| Brush edit mode: LMB / `Shift+LMB` / `W` / `E` / `R` / `Esc` | Select element / toggle element selection / move / rotate / scale selected brush elements / return to object mode |
|
| Brush edit mode: LMB / `Shift+LMB` / `W` / `E` / `R` / `Esc` | Select element / toggle element selection / move / rotate / scale selected brush elements / return to object mode |
|
||||||
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
||||||
|
| Collider viewport mode | Inspect world-scale box/sphere/capsule overlays and cooked mesh bounds; green/cyan/gray/amber/red identify ready/trigger/disabled/warning/invalid state, with selected health in the lower-left HUD |
|
||||||
| Viewport eye/options | Toggle actor root icon categories, adjust icon/gizmo size, and control colliders, lights, spawns, gameplay markers, volumes, prefab/model anchors, and runtime player/camera visualizers |
|
| Viewport eye/options | Toggle actor root icon categories, adjust icon/gizmo size, and control colliders, lights, spawns, gameplay markers, volumes, prefab/model anchors, and runtime player/camera visualizers |
|
||||||
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
||||||
| Viewport selection/orientation HUD | Identify the primary selection, multi-selection count, overlapping-pick position, camera axes, shading mode, and active render path |
|
| Viewport selection/orientation HUD | Identify the primary selection, multi-selection count, overlapping-pick position, camera axes, shading mode, and active render path |
|
||||||
@ -406,6 +407,7 @@ crates/
|
|||||||
- [x] Modal terrain Raise/Lower/Flatten/Smooth/Noise sculpting with a terrain-following footprint, deterministic noise, safe cancel restore, and one undo transaction per stroke ([terrain guide](docs/editor/terrain.md), [Gitea #23](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/23))
|
- [x] Modal terrain Raise/Lower/Flatten/Smooth/Noise sculpting with a terrain-following footprint, deterministic noise, safe cancel restore, and one undo transaction per stroke ([terrain guide](docs/editor/terrain.md), [Gitea #23](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/23))
|
||||||
- [x] Four shared terrain Material/Material Instance layers with compact normalized sample weights, blended raster hydration, drag/browse assignment, Paint/Erase preview, exact cancel, and one undo transaction per stroke ([ADR 0040](docs/adr/0040-terrain-material-layer-weights.md), [terrain guide](docs/editor/terrain.md), [Gitea #24](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/24))
|
- [x] Four shared terrain Material/Material Instance layers with compact normalized sample weights, blended raster hydration, drag/browse assignment, Paint/Erase preview, exact cancel, and one undo transaction per stroke ([ADR 0040](docs/adr/0040-terrain-material-layer-weights.md), [terrain guide](docs/editor/terrain.md), [Gitea #24](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/24))
|
||||||
- [x] Transactional physics placement with real Avian gravity/colliders, paused Edit-mode physics, prerequisite diagnostics, isolated non-selected bodies, exact cancel, and grouped transform undo ([ADR 0041](docs/adr/0041-transactional-editor-physics-placement.md), [workflow guide](docs/editor/physics-placement.md), [Gitea #25](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/25))
|
- [x] Transactional physics placement with real Avian gravity/colliders, paused Edit-mode physics, prerequisite diagnostics, isolated non-selected bodies, exact cancel, and grouped transform undo ([ADR 0041](docs/adr/0041-transactional-editor-physics-placement.md), [workflow guide](docs/editor/physics-placement.md), [Gitea #25](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/25))
|
||||||
|
- [x] Collider authoring health shared by the inspector, Collider viewport, Diagnostics panel, and physics placement, with scaled shape overlays, cooked mesh bounds, missing/stale/invalid/oversized findings, and undoable dimension-preserving shape switching ([collider guide](docs/editor/collider-authoring.md), [Gitea #26](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/26))
|
||||||
- [x] Non-blocking native file/folder/confirmation broker across scene, asset, prefab, composition, collaboration, and Project Browser workflows ([ADR 0038](docs/adr/0038-non-blocking-native-dialog-broker.md), [workflow guide](docs/editor/native-dialogs.md), [Gitea #52](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/52))
|
- [x] Non-blocking native file/folder/confirmation broker across scene, asset, prefab, composition, collaboration, and Project Browser workflows ([ADR 0038](docs/adr/0038-non-blocking-native-dialog-broker.md), [workflow guide](docs/editor/native-dialogs.md), [Gitea #52](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/52))
|
||||||
- [x] Asset import, static mesh/prefab placement, texture assignment, and selection export
|
- [x] Asset import, static mesh/prefab placement, texture assignment, and selection export
|
||||||
- [x] PIE player-only snapshot/restore (authored `LevelObject` edits persist on stop)
|
- [x] PIE player-only snapshot/restore (authored `LevelObject` edits persist on stop)
|
||||||
|
|||||||
@ -251,6 +251,24 @@
|
|||||||
),
|
),
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
id: ("a7ef65b4-24d6-4c3a-b637-5bb52d8e9a26"),
|
||||||
|
path: "assets/levels/collider_diagnostics_showcase.scn.ron",
|
||||||
|
label: "collider_diagnostics_showcase.scn",
|
||||||
|
kind_tag: "Level",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
|
animation_manifest_path: None,
|
||||||
|
default_animation_clip_id: None,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
(
|
(
|
||||||
id: ("f3dfcd11-aa5b-4fd4-872b-06832b865c99"),
|
id: ("f3dfcd11-aa5b-4fd4-872b-06832b865c99"),
|
||||||
path: "assets/levels/audio_authoring_showcase.scn.ron",
|
path: "assets/levels/audio_authoring_showcase.scn.ron",
|
||||||
|
|||||||
175
assets/levels/collider_diagnostics_showcase.scn.ron
Normal file
175
assets/levels/collider_diagnostics_showcase.scn.ron
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
(schema_version: 4, resources: {}, entities: {
|
||||||
|
1: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Valid Box Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (-4.5, 1.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-box"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (0),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.18, g: 0.62, b: 0.36, a: 1.0),
|
||||||
|
metallic: 0.05, roughness: 0.55,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (1.8, 2.0, 1.8)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
"shared::components::ColliderDesc": (
|
||||||
|
enabled: true, is_trigger: false,
|
||||||
|
shape: Cuboid(x_length: 1.8, y_length: 2.0, z_length: 1.8),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
2: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Valid Sphere Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (-2.25, 1.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.35, 0.8),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-sphere"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (1),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.16, g: 0.52, b: 0.78, a: 1.0),
|
||||||
|
metallic: 0.08, roughness: 0.42,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Sphere, size: (2.0, 2.0, 2.0)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
"shared::components::ColliderDesc": (
|
||||||
|
enabled: true, is_trigger: false, shape: Sphere(radius: 1.0),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
3: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Valid Capsule Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 1.4, 0.0),
|
||||||
|
rotation: (0.0, 0.1950903, 0.0, 0.9807853),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-capsule"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (2),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.58, g: 0.34, b: 0.78, a: 1.0),
|
||||||
|
metallic: 0.06, roughness: 0.48,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (1.4, 2.8, 1.4)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
"shared::components::ColliderDesc": (
|
||||||
|
enabled: true, is_trigger: false, shape: Capsule(radius: 0.7, height: 2.8),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
4: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Trigger Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (2.25, 1.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-trigger"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (3),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.12, g: 0.68, b: 0.82, a: 1.0),
|
||||||
|
metallic: 0.03, roughness: 0.4,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Sphere, size: (2.0, 2.0, 2.0)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
"shared::components::ColliderDesc": (
|
||||||
|
enabled: true, is_trigger: true, shape: Sphere(radius: 1.0),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
5: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Disabled Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (4.5, 1.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-disabled"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (4),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.38, g: 0.40, b: 0.46, a: 1.0),
|
||||||
|
metallic: 0.02, roughness: 0.68,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (1.8, 2.0, 1.8)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
"shared::components::ColliderDesc": (
|
||||||
|
enabled: false, is_trigger: false,
|
||||||
|
shape: Cuboid(x_length: 1.8, y_length: 2.0, z_length: 1.8),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
6: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Missing Collider",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 1.0, -3.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-missing"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (5),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.78, g: 0.16, b: 0.20, a: 1.0),
|
||||||
|
metallic: 0.02, roughness: 0.58,
|
||||||
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0), emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None, emissive_texture: None, normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None, material_asset_path: None, parameters: [], textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (1.8, 2.0, 1.8)),
|
||||||
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
|
}),
|
||||||
|
7: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Collider Showcase Sun",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 8.0, 4.0),
|
||||||
|
rotation: (-0.3826834, 0.0, 0.0, 0.9238795),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("collider-showcase-sun"),
|
||||||
|
"shared::components::ActorKind": Light,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (6),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::LightDesc": (
|
||||||
|
kind: Directional, color: (r: 1.0, g: 0.95, b: 0.88, a: 1.0),
|
||||||
|
intensity: 100000.0, range: 0.0, shadows: true,
|
||||||
|
inner_angle_deg: 25.0, outer_angle_deg: 35.0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
@ -15,6 +15,9 @@ use crate::project_io::ProjectWorkspace;
|
|||||||
use crate::scene_io::{SceneIo, SceneIoEventSeverity};
|
use crate::scene_io::{SceneIo, SceneIoEventSeverity};
|
||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::ui::UiState;
|
use crate::ui::UiState;
|
||||||
|
use crate::viewport::collider_diagnostics::{
|
||||||
|
ColliderDiagnosticSeverity, ColliderDiagnosticsState, ColliderOverlayStatus,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct DiagnosticsPanel {
|
pub struct DiagnosticsPanel {
|
||||||
@ -138,6 +141,9 @@ pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
collider_diagnostics_ui(world, ui);
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.strong("Support bundle");
|
ui.strong("Support bundle");
|
||||||
@ -193,6 +199,73 @@ pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
|||||||
ui.small("BRP is enabled on the editor for external tooling.");
|
ui.small("BRP is enabled on the editor for external tooling.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collider_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||||
|
let Some(state) = world.get_resource::<ColliderDiagnosticsState>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut rows = state.entries.values().cloned().collect::<Vec<_>>();
|
||||||
|
rows.sort_by(|left, right| left.actor_name.cmp(&right.actor_name));
|
||||||
|
let valid = state.valid_actors;
|
||||||
|
let disabled = state.disabled_actors;
|
||||||
|
let warnings = state.warning_actors;
|
||||||
|
let errors = state.error_actors;
|
||||||
|
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.strong("Collider health");
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(125, 210, 145),
|
||||||
|
format!("{valid} ready"),
|
||||||
|
);
|
||||||
|
if disabled > 0 {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(143, 151, 163),
|
||||||
|
format!("{disabled} disabled"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if warnings > 0 {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(242, 173, 72),
|
||||||
|
format!("{warnings} warning"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if errors > 0 {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(244, 91, 99),
|
||||||
|
format!("{errors} invalid"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for row in rows.into_iter().filter(|row| {
|
||||||
|
matches!(
|
||||||
|
row.overlay_status,
|
||||||
|
ColliderOverlayStatus::Warning | ColliderOverlayStatus::Error
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
let color = match row.overlay_status {
|
||||||
|
ColliderOverlayStatus::Error => egui::Color32::from_rgb(244, 91, 99),
|
||||||
|
_ => egui::Color32::from_rgb(242, 173, 72),
|
||||||
|
};
|
||||||
|
ui.colored_label(color, row.shape_label);
|
||||||
|
ui.label(&row.actor_name);
|
||||||
|
if ui.small_button("Select").clicked() {
|
||||||
|
world.resource_mut::<SelectedEntity>().0 = Some(row.entity);
|
||||||
|
world
|
||||||
|
.resource_mut::<UiState>()
|
||||||
|
.selected_entities
|
||||||
|
.select_replace(row.entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for diagnostic in &row.diagnostics {
|
||||||
|
if diagnostic.severity == ColliderDiagnosticSeverity::Info {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ui.small(format!("{}: {}", diagnostic.code, diagnostic.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn brush_diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) {
|
pub fn brush_diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) {
|
||||||
if !*open {
|
if !*open {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -44,6 +44,9 @@ use crate::ui::theme::{
|
|||||||
};
|
};
|
||||||
use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
|
use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
|
||||||
use crate::viewport::brush_edit::{BrushElementKey, BrushElementSelection};
|
use crate::viewport::brush_edit::{BrushElementKey, BrushElementSelection};
|
||||||
|
use crate::viewport::collider_diagnostics::{
|
||||||
|
diagnose_collider, ColliderDiagnosticSeverity, ColliderOverlayStatus,
|
||||||
|
};
|
||||||
|
|
||||||
use super::component_registry::{
|
use super::component_registry::{
|
||||||
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
|
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
|
||||||
@ -4213,6 +4216,7 @@ fn rigid_body_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|||||||
|
|
||||||
fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||||
let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh);
|
let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh);
|
||||||
|
let diagnostic_entry = diagnose_collider(world, entity);
|
||||||
let Some(mut collider) = world.get::<ColliderDesc>(entity).cloned() else {
|
let Some(mut collider) = world.get::<ColliderDesc>(entity).cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -4235,13 +4239,60 @@ fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|||||||
ComponentCardOptions::removable(COMPONENT_COLLIDER_DESC, "Collider", icons::SELECTION),
|
ComponentCardOptions::removable(COMPONENT_COLLIDER_DESC, "Collider", icons::SELECTION),
|
||||||
);
|
);
|
||||||
let card_response = component_card(ui, &card, |ui| {
|
let card_response = component_card(ui, &card, |ui| {
|
||||||
|
if let Some(entry) = diagnostic_entry.as_ref() {
|
||||||
|
let (label, color) = match entry.overlay_status {
|
||||||
|
ColliderOverlayStatus::Valid => ("Ready", egui::Color32::from_rgb(112, 210, 144)),
|
||||||
|
ColliderOverlayStatus::Trigger => {
|
||||||
|
("Trigger", egui::Color32::from_rgb(86, 195, 235))
|
||||||
|
}
|
||||||
|
ColliderOverlayStatus::Disabled => {
|
||||||
|
("Disabled", egui::Color32::from_rgb(143, 151, 163))
|
||||||
|
}
|
||||||
|
ColliderOverlayStatus::Warning => {
|
||||||
|
("Warning", egui::Color32::from_rgb(242, 173, 72))
|
||||||
|
}
|
||||||
|
ColliderOverlayStatus::Error => ("Invalid", egui::Color32::from_rgb(244, 91, 99)),
|
||||||
|
};
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.colored_label(color, egui::RichText::new(label).strong());
|
||||||
|
ui.label(egui::RichText::new(entry.shape_label).color(TEXT_DIM));
|
||||||
|
if entry.runtime_ready {
|
||||||
|
ui.label(egui::RichText::new("Hydrated").color(TEXT_MUTED).small());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for diagnostic in &entry.diagnostics {
|
||||||
|
let color = match diagnostic.severity {
|
||||||
|
ColliderDiagnosticSeverity::Info => TEXT_MUTED,
|
||||||
|
ColliderDiagnosticSeverity::Warning => egui::Color32::from_rgb(242, 173, 72),
|
||||||
|
ColliderDiagnosticSeverity::Error => egui::Color32::from_rgb(244, 91, 99),
|
||||||
|
};
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(&diagnostic.message)
|
||||||
|
.color(color)
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(&diagnostic.repair)
|
||||||
|
.color(TEXT_MUTED)
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if entry.highest_severity() == Some(ColliderDiagnosticSeverity::Error)
|
||||||
|
&& ui.small_button("Reset shape").clicked()
|
||||||
|
{
|
||||||
|
collider.shape = ColliderShapeDesc::default();
|
||||||
|
collider.enabled = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
ui.add_space(2.0);
|
||||||
|
}
|
||||||
property_row(ui, "Mode", |ui| {
|
property_row(ui, "Mode", |ui| {
|
||||||
changed |= ui.checkbox(&mut collider.enabled, "Enabled").changed();
|
changed |= ui.checkbox(&mut collider.enabled, "Enabled").changed();
|
||||||
changed |= ui.checkbox(&mut collider.is_trigger, "Trigger").changed();
|
changed |= ui.checkbox(&mut collider.is_trigger, "Trigger").changed();
|
||||||
});
|
});
|
||||||
property_row(ui, "Shape", |ui| {
|
property_row(ui, "Shape", |ui| {
|
||||||
let mut shape_kind = collider_shape_kind(&collider.shape);
|
let mut shape_kind = collider_shape_kind(&collider.shape);
|
||||||
egui::ComboBox::from_id_salt("collider_shape_kind")
|
egui::ComboBox::from_id_salt(("collider_shape_kind", entity))
|
||||||
.selected_text(shape_kind)
|
.selected_text(shape_kind)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for label in ["Box", "Sphere", "Capsule", "Static Mesh"] {
|
for label in ["Box", "Sphere", "Capsule", "Static Mesh"] {
|
||||||
@ -4251,15 +4302,8 @@ fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if shape_kind != collider_shape_kind(&collider.shape) {
|
if shape_kind != collider_shape_kind(&collider.shape) {
|
||||||
collider.shape = match shape_kind {
|
collider.shape =
|
||||||
"Sphere" => ColliderShapeDesc::Sphere { radius: 0.5 },
|
convert_collider_shape(&collider.shape, shape_kind, renderer_meshes.clone());
|
||||||
"Capsule" => ColliderShapeDesc::Capsule {
|
|
||||||
radius: 0.5,
|
|
||||||
height: 1.0,
|
|
||||||
},
|
|
||||||
"Static Mesh" => ColliderShapeDesc::static_mesh(renderer_meshes.clone()),
|
|
||||||
_ => ColliderShapeDesc::default(),
|
|
||||||
};
|
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -4327,12 +4371,48 @@ fn collider_shape_kind(shape: &ColliderShapeDesc) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_collider_shape(
|
||||||
|
previous: &ColliderShapeDesc,
|
||||||
|
shape_kind: &str,
|
||||||
|
renderer_meshes: Vec<EditorAssetRef>,
|
||||||
|
) -> ColliderShapeDesc {
|
||||||
|
let dimensions = match previous {
|
||||||
|
ColliderShapeDesc::Cuboid {
|
||||||
|
x_length,
|
||||||
|
y_length,
|
||||||
|
z_length,
|
||||||
|
} => Vec3::new(*x_length, *y_length, *z_length),
|
||||||
|
ColliderShapeDesc::Sphere { radius } => Vec3::splat(*radius * 2.0),
|
||||||
|
ColliderShapeDesc::Capsule { radius, height } => {
|
||||||
|
Vec3::new(*radius * 2.0, *height, *radius * 2.0)
|
||||||
|
}
|
||||||
|
ColliderShapeDesc::StaticMesh { .. } => Vec3::ONE,
|
||||||
|
}
|
||||||
|
.max(Vec3::splat(0.001));
|
||||||
|
|
||||||
|
match shape_kind {
|
||||||
|
"Sphere" => ColliderShapeDesc::Sphere {
|
||||||
|
radius: dimensions.max_element() * 0.5,
|
||||||
|
},
|
||||||
|
"Capsule" => ColliderShapeDesc::Capsule {
|
||||||
|
radius: dimensions.x.max(dimensions.z) * 0.5,
|
||||||
|
height: dimensions.y,
|
||||||
|
},
|
||||||
|
"Static Mesh" => ColliderShapeDesc::static_mesh(renderer_meshes),
|
||||||
|
_ => ColliderShapeDesc::Cuboid {
|
||||||
|
x_length: dimensions.x,
|
||||||
|
y_length: dimensions.y,
|
||||||
|
z_length: dimensions.z,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn dimension_drag(ui: &mut egui::Ui, label: &str, value: &mut f32) -> bool {
|
fn dimension_drag(ui: &mut egui::Ui, label: &str, value: &mut f32) -> bool {
|
||||||
property_row(ui, label, |ui| {
|
property_row(ui, label, |ui| {
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
[fit_width(ui, 72.0, 120.0), 20.0],
|
[fit_width(ui, 72.0, 120.0), 20.0],
|
||||||
egui::DragValue::new(value)
|
egui::DragValue::new(value)
|
||||||
.range(0.0..=10_000.0)
|
.range(0.001..=10_000.0)
|
||||||
.speed(0.05)
|
.speed(0.05)
|
||||||
.min_decimals(2)
|
.min_decimals(2)
|
||||||
.max_decimals(3),
|
.max_decimals(3),
|
||||||
@ -4839,3 +4919,30 @@ fn option_string_ui(ui: &mut egui::Ui, label: &str, value: &mut Option<String>)
|
|||||||
};
|
};
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod collider_inspector_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::history::{apply_command_undo, EditorHistory};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shape_switch_preserves_dimensions_and_is_one_undoable_edit() {
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<EditorHistory>();
|
||||||
|
let original = ColliderDesc::static_cuboid(Vec3::new(2.0, 4.0, 6.0));
|
||||||
|
let entity = world.spawn((LevelObject, original.clone())).id();
|
||||||
|
|
||||||
|
let sphere = ColliderDesc {
|
||||||
|
shape: convert_collider_shape(&original.shape, "Sphere", Vec::new()),
|
||||||
|
..original.clone()
|
||||||
|
};
|
||||||
|
assert_eq!(sphere.shape, ColliderShapeDesc::Sphere { radius: 3.0 });
|
||||||
|
|
||||||
|
set_collider_with_history(&mut world, entity, sphere.clone());
|
||||||
|
assert_eq!(world.resource::<EditorHistory>().undo_depth(), 1);
|
||||||
|
assert_eq!(world.get::<ColliderDesc>(entity), Some(&sphere));
|
||||||
|
|
||||||
|
apply_command_undo(&mut world);
|
||||||
|
assert_eq!(world.get::<ColliderDesc>(entity), Some(&original));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssets};
|
|||||||
use crate::camera::EditorCamera;
|
use crate::camera::EditorCamera;
|
||||||
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
|
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
|
||||||
use crate::render_target::ViewportRenderTarget;
|
use crate::render_target::ViewportRenderTarget;
|
||||||
use crate::selection::{ViewportClick, ViewportPickStack};
|
use crate::selection::{SelectedEntity, ViewportClick, ViewportPickStack};
|
||||||
use crate::state::PlayPossession;
|
use crate::state::PlayPossession;
|
||||||
use crate::viewport::actor_icons::ActorIconSettings;
|
use crate::viewport::actor_icons::ActorIconSettings;
|
||||||
use crate::viewport::brush_edit::{BrushEditMode, BrushElementSelection};
|
use crate::viewport::brush_edit::{BrushEditMode, BrushElementSelection};
|
||||||
@ -200,16 +200,54 @@ fn scene_view_render_badge(world: &World, ctx: &egui::Context, scene_rect: egui:
|
|||||||
});
|
});
|
||||||
|
|
||||||
if mode == EditorViewportMode::Collider {
|
if mode == EditorViewportMode::Collider {
|
||||||
|
let health =
|
||||||
|
world.get_resource::<crate::viewport::collider_diagnostics::ColliderDiagnosticsState>();
|
||||||
|
let selected_entry = world
|
||||||
|
.get_resource::<SelectedEntity>()
|
||||||
|
.and_then(|selected| selected.0)
|
||||||
|
.and_then(|entity| health.and_then(|health| health.entry(entity)));
|
||||||
egui::Area::new(egui::Id::new("scene_view_collider_warning"))
|
egui::Area::new(egui::Id::new("scene_view_collider_warning"))
|
||||||
.fixed_pos(scene_rect.left_bottom() + egui::vec2(8.0, -28.0))
|
.fixed_pos(scene_rect.left_bottom() + egui::vec2(8.0, -28.0))
|
||||||
.interactable(false)
|
.interactable(false)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
overlay_chip_frame().show(ui, |ui| {
|
overlay_chip_frame().show(ui, |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("Collider view / meshes hidden")
|
egui::RichText::new("Collider View")
|
||||||
.color(WARNING)
|
.color(WARNING)
|
||||||
|
.strong()
|
||||||
.small(),
|
.small(),
|
||||||
);
|
);
|
||||||
|
if let Some(health) = health {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!(
|
||||||
|
"{} ready {} disabled {} warning {} invalid",
|
||||||
|
health.valid_actors,
|
||||||
|
health.disabled_actors,
|
||||||
|
health.warning_actors,
|
||||||
|
health.error_actors
|
||||||
|
))
|
||||||
|
.color(TEXT_MUTED)
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(entry) = selected_entry {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!(
|
||||||
|
"{} / {} / {}",
|
||||||
|
entry.actor_name,
|
||||||
|
entry.shape_label,
|
||||||
|
if entry.runtime_ready {
|
||||||
|
"hydrated"
|
||||||
|
} else {
|
||||||
|
"not hydrated"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.color(SELECTION)
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
428
crates/editor/src/viewport/collider_diagnostics.rs
Normal file
428
crates/editor/src/viewport/collider_diagnostics.rs
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
//! Shared authored/runtime collider diagnostics for editor workflows.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use avian3d::prelude::{Collider, RigidBody};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use shared::{
|
||||||
|
authoring_component_active, AuthoringComponentStates, ColliderDesc, ColliderShapeDesc,
|
||||||
|
InspectorOrder, LevelObject, PhysicsBody, RigidBodyDesc, COMPONENT_COLLIDER_DESC,
|
||||||
|
COMPONENT_PHYSICS_BODY, COMPONENT_RIGID_BODY_DESC,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::assets::asset_db::{find_asset_by_id, AssetRegistry};
|
||||||
|
use crate::ui::selection_ops::entity_name;
|
||||||
|
|
||||||
|
/// Collider dimensions beyond this world-space extent are usually accidental in a meter-scale
|
||||||
|
/// project and can severely degrade broad-phase performance.
|
||||||
|
pub const OVERSIZED_COLLIDER_EXTENT: f32 = 1_000.0;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum ColliderDiagnosticSeverity {
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ColliderDiagnostic {
|
||||||
|
pub code: &'static str,
|
||||||
|
pub severity: ColliderDiagnosticSeverity,
|
||||||
|
pub message: String,
|
||||||
|
pub repair: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ColliderOverlayStatus {
|
||||||
|
Valid,
|
||||||
|
Trigger,
|
||||||
|
Disabled,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ColliderDiagnosticEntry {
|
||||||
|
pub entity: Entity,
|
||||||
|
pub actor_name: String,
|
||||||
|
pub shape_label: &'static str,
|
||||||
|
pub runtime_ready: bool,
|
||||||
|
pub diagnostics: Vec<ColliderDiagnostic>,
|
||||||
|
pub overlay_status: ColliderOverlayStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColliderDiagnosticEntry {
|
||||||
|
pub fn highest_severity(&self) -> Option<ColliderDiagnosticSeverity> {
|
||||||
|
self.diagnostics
|
||||||
|
.iter()
|
||||||
|
.map(|diagnostic| diagnostic.severity)
|
||||||
|
.max()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blocking_messages(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.diagnostics
|
||||||
|
.iter()
|
||||||
|
.filter(|diagnostic| diagnostic.severity == ColliderDiagnosticSeverity::Error)
|
||||||
|
.map(|diagnostic| diagnostic.message.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Default)]
|
||||||
|
pub struct ColliderDiagnosticsState {
|
||||||
|
pub entries: HashMap<Entity, ColliderDiagnosticEntry>,
|
||||||
|
pub valid_actors: usize,
|
||||||
|
pub disabled_actors: usize,
|
||||||
|
pub warning_actors: usize,
|
||||||
|
pub error_actors: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColliderDiagnosticsState {
|
||||||
|
pub fn entry(&self, entity: Entity) -> Option<&ColliderDiagnosticEntry> {
|
||||||
|
self.entries.get(&entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_collider_diagnostics(world: &mut World) {
|
||||||
|
let entities = world
|
||||||
|
.iter_entities()
|
||||||
|
.filter(|entity| entity.contains::<LevelObject>())
|
||||||
|
.map(|entity| entity.id())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut state = ColliderDiagnosticsState::default();
|
||||||
|
for entity in entities {
|
||||||
|
let Some(entry) = diagnose_collider(world, entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match entry.overlay_status {
|
||||||
|
ColliderOverlayStatus::Error => state.error_actors += 1,
|
||||||
|
ColliderOverlayStatus::Warning => state.warning_actors += 1,
|
||||||
|
ColliderOverlayStatus::Disabled => state.disabled_actors += 1,
|
||||||
|
_ => state.valid_actors += 1,
|
||||||
|
}
|
||||||
|
state.entries.insert(entity, entry);
|
||||||
|
}
|
||||||
|
world.insert_resource(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diagnose_collider(world: &World, entity: Entity) -> Option<ColliderDiagnosticEntry> {
|
||||||
|
let states = world.get::<AuthoringComponentStates>(entity);
|
||||||
|
let order = world.get::<InspectorOrder>(entity);
|
||||||
|
let rigid_body_active = world.get::<RigidBodyDesc>(entity).is_some()
|
||||||
|
&& authoring_component_active(states, order, COMPONENT_RIGID_BODY_DESC);
|
||||||
|
let legacy_active = world.get::<PhysicsBody>(entity).is_some()
|
||||||
|
&& authoring_component_active(states, order, COMPONENT_PHYSICS_BODY);
|
||||||
|
let collider = world.get::<ColliderDesc>(entity);
|
||||||
|
let collider_active =
|
||||||
|
collider.is_some() && authoring_component_active(states, order, COMPONENT_COLLIDER_DESC);
|
||||||
|
|
||||||
|
if !rigid_body_active && !legacy_active && collider.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
if rigid_body_active && !legacy_active && !collider_active {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.missing",
|
||||||
|
severity: ColliderDiagnosticSeverity::Error,
|
||||||
|
message: "Rigid body has no active Collider component.".into(),
|
||||||
|
repair: "Add or enable a Collider component on this actor.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut shape_label = if legacy_active { "Legacy" } else { "Missing" };
|
||||||
|
let mut enabled = legacy_active;
|
||||||
|
let mut trigger = false;
|
||||||
|
let mut authoring_valid = true;
|
||||||
|
if let Some(collider) = collider {
|
||||||
|
shape_label = collider_shape_label(&collider.shape);
|
||||||
|
enabled = collider_active && collider.enabled;
|
||||||
|
trigger = collider.is_trigger;
|
||||||
|
if !collider_active {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.inactive",
|
||||||
|
severity: ColliderDiagnosticSeverity::Info,
|
||||||
|
message: "Collider component is inactive.".into(),
|
||||||
|
repair: "Enable the component when this actor should participate in collision."
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
} else if !collider.enabled {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.disabled",
|
||||||
|
severity: ColliderDiagnosticSeverity::Info,
|
||||||
|
message: "Collider is disabled.".into(),
|
||||||
|
repair: "Enable the collider when collision is required.".into(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let shape_diagnostics = validate_shape(world, entity, &collider.shape);
|
||||||
|
authoring_valid = shape_diagnostics
|
||||||
|
.iter()
|
||||||
|
.all(|diagnostic| diagnostic.severity != ColliderDiagnosticSeverity::Error);
|
||||||
|
diagnostics.extend(shape_diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let runtime_ready = has_runtime_collider(world, entity);
|
||||||
|
if enabled && authoring_valid && !runtime_ready {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.runtime_stale",
|
||||||
|
severity: ColliderDiagnosticSeverity::Warning,
|
||||||
|
message: "Authored collider has not hydrated into a runtime shape.".into(),
|
||||||
|
repair: "Check mesh cooking and asset references, then refresh the scene.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let highest = diagnostics
|
||||||
|
.iter()
|
||||||
|
.map(|diagnostic| diagnostic.severity)
|
||||||
|
.max();
|
||||||
|
let overlay_status = match highest {
|
||||||
|
Some(ColliderDiagnosticSeverity::Error) => ColliderOverlayStatus::Error,
|
||||||
|
Some(ColliderDiagnosticSeverity::Warning) => ColliderOverlayStatus::Warning,
|
||||||
|
_ if !enabled => ColliderOverlayStatus::Disabled,
|
||||||
|
_ if trigger => ColliderOverlayStatus::Trigger,
|
||||||
|
_ => ColliderOverlayStatus::Valid,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ColliderDiagnosticEntry {
|
||||||
|
entity,
|
||||||
|
actor_name: entity_name(world, entity),
|
||||||
|
shape_label,
|
||||||
|
runtime_ready,
|
||||||
|
diagnostics,
|
||||||
|
overlay_status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn physics_placement_prerequisite_problems(world: &World, entity: Entity) -> Vec<String> {
|
||||||
|
let name = entity_name(world, entity);
|
||||||
|
let states = world.get::<AuthoringComponentStates>(entity);
|
||||||
|
let order = world.get::<InspectorOrder>(entity);
|
||||||
|
let split_body = world.get::<RigidBodyDesc>(entity).is_some()
|
||||||
|
&& authoring_component_active(states, order, COMPONENT_RIGID_BODY_DESC);
|
||||||
|
let legacy = world.get::<PhysicsBody>(entity).is_some()
|
||||||
|
&& authoring_component_active(states, order, COMPONENT_PHYSICS_BODY);
|
||||||
|
let collider = world.get::<ColliderDesc>(entity).filter(|collider| {
|
||||||
|
collider.enabled
|
||||||
|
&& !collider.is_trigger
|
||||||
|
&& authoring_component_active(states, order, COMPONENT_COLLIDER_DESC)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
if !split_body && !legacy {
|
||||||
|
problems.push(format!("{name}: add and enable a Rigid Body component"));
|
||||||
|
}
|
||||||
|
if collider.is_none() && !legacy {
|
||||||
|
problems.push(format!(
|
||||||
|
"{name}: add and enable a non-trigger Collider component"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(entry) = diagnose_collider(world, entity) {
|
||||||
|
problems.extend(
|
||||||
|
entry
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.filter(|diagnostic| {
|
||||||
|
diagnostic.severity == ColliderDiagnosticSeverity::Error
|
||||||
|
&& diagnostic.code != "collider.missing"
|
||||||
|
})
|
||||||
|
.map(|diagnostic| format!("{name}: {}", diagnostic.message)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if world.get::<RigidBody>(entity).is_none() {
|
||||||
|
problems.push(format!("{name}: rigid body has not hydrated yet"));
|
||||||
|
}
|
||||||
|
if !has_runtime_collider(world, entity) {
|
||||||
|
problems.push(format!(
|
||||||
|
"{name}: collider has not hydrated or finished cooking"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
problems.sort();
|
||||||
|
problems.dedup();
|
||||||
|
problems
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_runtime_collider(world: &World, root: Entity) -> bool {
|
||||||
|
let mut pending = vec![root];
|
||||||
|
while let Some(entity) = pending.pop() {
|
||||||
|
if world.get::<Collider>(entity).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Some(children) = world.get::<Children>(entity) {
|
||||||
|
pending.extend(children.iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collider_shape_label(shape: &ColliderShapeDesc) -> &'static str {
|
||||||
|
match shape {
|
||||||
|
ColliderShapeDesc::Cuboid { .. } => "Box",
|
||||||
|
ColliderShapeDesc::Sphere { .. } => "Sphere",
|
||||||
|
ColliderShapeDesc::Capsule { .. } => "Capsule",
|
||||||
|
ColliderShapeDesc::StaticMesh { .. } => "Static Mesh",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_shape(
|
||||||
|
world: &World,
|
||||||
|
entity: Entity,
|
||||||
|
shape: &ColliderShapeDesc,
|
||||||
|
) -> Vec<ColliderDiagnostic> {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let dimensions = match shape {
|
||||||
|
ColliderShapeDesc::Cuboid {
|
||||||
|
x_length,
|
||||||
|
y_length,
|
||||||
|
z_length,
|
||||||
|
} => Some(Vec3::new(*x_length, *y_length, *z_length)),
|
||||||
|
ColliderShapeDesc::Sphere { radius } => Some(Vec3::splat(*radius * 2.0)),
|
||||||
|
ColliderShapeDesc::Capsule { radius, height } => {
|
||||||
|
Some(Vec3::new(*radius * 2.0, *height, *radius * 2.0))
|
||||||
|
}
|
||||||
|
ColliderShapeDesc::StaticMesh { meshes, .. } => {
|
||||||
|
if meshes.is_empty() {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.mesh_missing",
|
||||||
|
severity: ColliderDiagnosticSeverity::Error,
|
||||||
|
message: "Static mesh collider has no mesh sources.".into(),
|
||||||
|
repair: "Choose a mesh source or use the actor renderer meshes.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for mesh in meshes {
|
||||||
|
if !mesh.is_resolved() {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.mesh_invalid_ref",
|
||||||
|
severity: ColliderDiagnosticSeverity::Error,
|
||||||
|
message: format!(
|
||||||
|
"Static mesh source `{}` has no stable asset or sub-asset ID.",
|
||||||
|
mesh.label
|
||||||
|
),
|
||||||
|
repair: "Choose a valid mesh from the asset browser.".into(),
|
||||||
|
});
|
||||||
|
} else if world
|
||||||
|
.get_resource::<AssetRegistry>()
|
||||||
|
.is_some_and(|registry| find_asset_by_id(registry, &mesh.asset_id).is_none())
|
||||||
|
{
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.mesh_stale_ref",
|
||||||
|
severity: ColliderDiagnosticSeverity::Error,
|
||||||
|
message: format!(
|
||||||
|
"Static mesh source `{}` no longer exists in the asset registry.",
|
||||||
|
mesh.label
|
||||||
|
),
|
||||||
|
repair: "Relink the collider to an existing mesh asset.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(dimensions) = dimensions {
|
||||||
|
if !dimensions.is_finite() || dimensions.min_element() <= 0.0 {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.invalid_dimensions",
|
||||||
|
severity: ColliderDiagnosticSeverity::Error,
|
||||||
|
message: "Collider dimensions must be positive finite values.".into(),
|
||||||
|
repair: "Reset the shape or enter valid dimensions.".into(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let scale = world
|
||||||
|
.get::<GlobalTransform>(entity)
|
||||||
|
.map(|global| global.to_scale_rotation_translation().0.abs())
|
||||||
|
.unwrap_or(Vec3::ONE);
|
||||||
|
let world_extent = (dimensions * scale).max_element();
|
||||||
|
if world_extent > OVERSIZED_COLLIDER_EXTENT {
|
||||||
|
diagnostics.push(ColliderDiagnostic {
|
||||||
|
code: "collider.oversized",
|
||||||
|
severity: ColliderDiagnosticSeverity::Warning,
|
||||||
|
message: format!(
|
||||||
|
"Collider spans {world_extent:.1} m in world space (recommended maximum: {OVERSIZED_COLLIDER_EXTENT:.0} m)."
|
||||||
|
),
|
||||||
|
repair: "Reduce collider dimensions or actor scale; split large collision into smaller actors."
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use shared::AuthoringRigidBody;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_missing_invalid_oversized_and_stale_runtime_colliders() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let missing = world
|
||||||
|
.spawn((LevelObject, RigidBodyDesc::default(), Transform::default()))
|
||||||
|
.id();
|
||||||
|
let invalid = world
|
||||||
|
.spawn((
|
||||||
|
LevelObject,
|
||||||
|
ColliderDesc {
|
||||||
|
enabled: true,
|
||||||
|
is_trigger: false,
|
||||||
|
shape: ColliderShapeDesc::Sphere { radius: 0.0 },
|
||||||
|
},
|
||||||
|
Transform::default(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
let oversized = world
|
||||||
|
.spawn((
|
||||||
|
LevelObject,
|
||||||
|
ColliderDesc::static_cuboid(Vec3::splat(2.0)),
|
||||||
|
Transform::from_scale(Vec3::splat(600.0)),
|
||||||
|
GlobalTransform::from(Transform::from_scale(Vec3::splat(600.0))),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let missing = diagnose_collider(&world, missing).unwrap();
|
||||||
|
assert!(missing
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| diagnostic.code == "collider.missing"));
|
||||||
|
|
||||||
|
let invalid = diagnose_collider(&world, invalid).unwrap();
|
||||||
|
assert!(invalid
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| diagnostic.code == "collider.invalid_dimensions"));
|
||||||
|
|
||||||
|
let oversized = diagnose_collider(&world, oversized).unwrap();
|
||||||
|
assert!(oversized
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| diagnostic.code == "collider.oversized"));
|
||||||
|
assert!(oversized
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| diagnostic.code == "collider.runtime_stale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hydrated_trigger_has_trigger_overlay_status() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let entity = world
|
||||||
|
.spawn((
|
||||||
|
LevelObject,
|
||||||
|
RigidBodyDesc {
|
||||||
|
body: AuthoringRigidBody::Static,
|
||||||
|
},
|
||||||
|
ColliderDesc {
|
||||||
|
enabled: true,
|
||||||
|
is_trigger: true,
|
||||||
|
shape: ColliderShapeDesc::Sphere { radius: 1.0 },
|
||||||
|
},
|
||||||
|
Collider::sphere(1.0),
|
||||||
|
Transform::default(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let entry = diagnose_collider(&world, entity).unwrap();
|
||||||
|
assert_eq!(entry.overlay_status, ColliderOverlayStatus::Trigger);
|
||||||
|
assert!(entry.runtime_ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ pub mod brush_csg;
|
|||||||
pub mod brush_edit;
|
pub mod brush_edit;
|
||||||
pub mod brush_tool;
|
pub mod brush_tool;
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
|
pub mod collider_diagnostics;
|
||||||
pub mod gizmos;
|
pub mod gizmos;
|
||||||
pub mod material_drop;
|
pub mod material_drop;
|
||||||
mod panel;
|
mod panel;
|
||||||
|
|||||||
@ -5,16 +5,12 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use shared::{
|
|
||||||
authoring_component_active, AuthoringComponentStates, ColliderDesc, InspectorOrder,
|
|
||||||
PhysicsBody, RigidBodyDesc, COMPONENT_COLLIDER_DESC, COMPONENT_PHYSICS_BODY,
|
|
||||||
COMPONENT_RIGID_BODY_DESC,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::history::set_transform_group_with_history;
|
use crate::history::set_transform_group_with_history;
|
||||||
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
|
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
|
||||||
use crate::state::EditorMode;
|
use crate::state::EditorMode;
|
||||||
use crate::ui::selection_ops::{entity_name, is_mutable_level_object};
|
use crate::ui::selection_ops::{entity_name, is_mutable_level_object};
|
||||||
|
use crate::viewport::collider_diagnostics::physics_placement_prerequisite_problems;
|
||||||
|
|
||||||
const STEP_HZ: f64 = 120.0;
|
const STEP_HZ: f64 = 120.0;
|
||||||
const STEPS_PER_FRAME: usize = 4;
|
const STEPS_PER_FRAME: usize = 4;
|
||||||
@ -369,33 +365,7 @@ fn validate_selection(world: &mut World, selected: &[Entity]) -> Result<(), Stri
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let states = world.get::<AuthoringComponentStates>(*entity);
|
problems.extend(physics_placement_prerequisite_problems(world, *entity));
|
||||||
let order = world.get::<InspectorOrder>(*entity);
|
|
||||||
let split_body = world.get::<RigidBodyDesc>(*entity).is_some()
|
|
||||||
&& authoring_component_active(states, order, COMPONENT_RIGID_BODY_DESC);
|
|
||||||
let split_collider = world.get::<ColliderDesc>(*entity).filter(|collider| {
|
|
||||||
collider.enabled
|
|
||||||
&& !collider.is_trigger
|
|
||||||
&& authoring_component_active(states, order, COMPONENT_COLLIDER_DESC)
|
|
||||||
});
|
|
||||||
let legacy = world.get::<PhysicsBody>(*entity).is_some()
|
|
||||||
&& authoring_component_active(states, order, COMPONENT_PHYSICS_BODY);
|
|
||||||
if !split_body && !legacy {
|
|
||||||
problems.push(format!("{name}: add and enable a Rigid Body component"));
|
|
||||||
}
|
|
||||||
if split_collider.is_none() && !legacy {
|
|
||||||
problems.push(format!(
|
|
||||||
"{name}: add and enable a non-trigger Collider component"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if world.get::<RigidBody>(*entity).is_none() {
|
|
||||||
problems.push(format!("{name}: rigid body has not hydrated yet"));
|
|
||||||
}
|
|
||||||
if !has_runtime_collider(world, *entity) {
|
|
||||||
problems.push(format!(
|
|
||||||
"{name}: collider has not hydrated or finished cooking"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if problems.is_empty() {
|
if problems.is_empty() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -404,19 +374,6 @@ fn validate_selection(world: &mut World, selected: &[Entity]) -> Result<(), Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_runtime_collider(world: &World, root: Entity) -> bool {
|
|
||||||
let mut pending = vec![root];
|
|
||||||
while let Some(entity) = pending.pop() {
|
|
||||||
if world.get::<Collider>(entity).is_some() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if let Some(children) = world.get::<Children>(entity) {
|
|
||||||
pending.extend(children.iter());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_snapshot(world: &mut World, snapshot: &BodySnapshot, restore_pose: bool) {
|
fn restore_snapshot(world: &mut World, snapshot: &BodySnapshot, restore_pose: bool) {
|
||||||
let Ok(mut entity) = world.get_entity_mut(snapshot.entity) else {
|
let Ok(mut entity) = world.get_entity_mut(snapshot.entity) else {
|
||||||
return;
|
return;
|
||||||
@ -498,7 +455,7 @@ mod tests {
|
|||||||
use crate::history::{apply_command_undo, EditorHistory};
|
use crate::history::{apply_command_undo, EditorHistory};
|
||||||
use bevy::asset::{AssetApp, AssetPlugin};
|
use bevy::asset::{AssetApp, AssetPlugin};
|
||||||
use bevy::state::app::StatesPlugin;
|
use bevy::state::app::StatesPlugin;
|
||||||
use shared::LevelObject;
|
use shared::{ColliderDesc, LevelObject, RigidBodyDesc};
|
||||||
|
|
||||||
fn app_with_physics() -> App {
|
fn app_with_physics() -> App {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
//! Editor-side scene visualizers for authoring/runtime entities that have no mesh.
|
//! Editor-side scene visualizers for authoring/runtime entities that have no mesh.
|
||||||
|
|
||||||
use avian3d::prelude::ColliderConstructor;
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
|
use avian3d::prelude::{ColliderAabb, ColliderConstructor};
|
||||||
use bevy::ecs::system::SystemParam;
|
use bevy::ecs::system::SystemParam;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use shared::{
|
use shared::{
|
||||||
AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, ColliderShapeDesc,
|
AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, ColliderShapeDesc,
|
||||||
LevelObject, LightDesc, ModelRef, NavigationArea, NavigationBounds, NavigationLink,
|
LevelObject, LightDesc, ModelRef, NavigationArea, NavigationBounds, NavigationLink,
|
||||||
NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
|
NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
|
||||||
PrefabRef, ProjectSun, RaytracingExcluded, SkinnedMeshRenderer, TeamSpawn, TriggerVolume,
|
PrefabRef, ProjectSun, RaytracingExcluded, RigidBodyDesc, SkinnedMeshRenderer, TeamSpawn,
|
||||||
WeaponSpawn,
|
TriggerVolume, WeaponSpawn,
|
||||||
};
|
};
|
||||||
use sim::tuning::{EYE_OFFSET_STAND, PLAYER_LENGTH, PLAYER_RADIUS};
|
use sim::tuning::{EYE_OFFSET_STAND, PLAYER_LENGTH, PLAYER_RADIUS};
|
||||||
use sim::{Player, PlayerCamera};
|
use sim::{Player, PlayerCamera};
|
||||||
@ -16,6 +18,10 @@ use sim::{Player, PlayerCamera};
|
|||||||
use crate::infra::EditorOnly;
|
use crate::infra::EditorOnly;
|
||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::state::{scene_tools_active, EditorMode, PlayPossession};
|
use crate::state::{scene_tools_active, EditorMode, PlayPossession};
|
||||||
|
use crate::viewport::collider_diagnostics::{
|
||||||
|
refresh_collider_diagnostics, ColliderDiagnosticEntry, ColliderDiagnosticsState,
|
||||||
|
ColliderOverlayStatus,
|
||||||
|
};
|
||||||
use crate::viewport::{EditorViewportMode, ViewportDisplayMode};
|
use crate::viewport::{EditorViewportMode, ViewportDisplayMode};
|
||||||
|
|
||||||
#[derive(Resource, Debug, Clone)]
|
#[derive(Resource, Debug, Clone)]
|
||||||
@ -102,7 +108,7 @@ struct VisualizerTargetQueries<'w, 's> {
|
|||||||
Entity,
|
Entity,
|
||||||
(
|
(
|
||||||
With<LevelObject>,
|
With<LevelObject>,
|
||||||
Or<(With<PhysicsBody>, With<ColliderDesc>)>,
|
Or<(With<PhysicsBody>, With<ColliderDesc>, With<RigidBodyDesc>)>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
lights: Query<'w, 's, Entity, (With<LevelObject>, With<LightDesc>)>,
|
lights: Query<'w, 's, Entity, (With<LevelObject>, With<LightDesc>)>,
|
||||||
@ -127,7 +133,15 @@ pub struct EditorVisualizersPlugin;
|
|||||||
impl Plugin for EditorVisualizersPlugin {
|
impl Plugin for EditorVisualizersPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<EditorVisualizationSettings>()
|
app.init_resource::<EditorVisualizationSettings>()
|
||||||
|
.init_resource::<ColliderDiagnosticsState>()
|
||||||
.add_systems(Startup, init_visualizer_proxy_assets)
|
.add_systems(Startup, init_visualizer_proxy_assets)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
refresh_collider_diagnostics
|
||||||
|
.before(sync_visualizer_proxies)
|
||||||
|
.before(draw_visualizers)
|
||||||
|
.before(draw_split_collider_visualizers),
|
||||||
|
)
|
||||||
.add_systems(Update, sync_visualizer_proxies)
|
.add_systems(Update, sync_visualizer_proxies)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@ -135,6 +149,7 @@ impl Plugin for EditorVisualizersPlugin {
|
|||||||
draw_visualizers,
|
draw_visualizers,
|
||||||
draw_audio_visualizers,
|
draw_audio_visualizers,
|
||||||
draw_split_collider_visualizers,
|
draw_split_collider_visualizers,
|
||||||
|
draw_missing_collider_visualizers,
|
||||||
draw_post_process_volume_gizmos,
|
draw_post_process_volume_gizmos,
|
||||||
draw_navigation_visualizers,
|
draw_navigation_visualizers,
|
||||||
)
|
)
|
||||||
@ -145,6 +160,52 @@ impl Plugin for EditorVisualizersPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_missing_collider_visualizers(
|
||||||
|
viewport_mode: Res<EditorViewportMode>,
|
||||||
|
settings: Res<EditorVisualizationSettings>,
|
||||||
|
diagnostics: Res<ColliderDiagnosticsState>,
|
||||||
|
selected: Res<SelectedEntity>,
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
missing: Query<
|
||||||
|
(Entity, &GlobalTransform),
|
||||||
|
(
|
||||||
|
With<LevelObject>,
|
||||||
|
With<RigidBodyDesc>,
|
||||||
|
Without<ColliderDesc>,
|
||||||
|
Without<PhysicsBody>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
let collider_mode = *viewport_mode == EditorViewportMode::Collider;
|
||||||
|
if !(collider_mode || settings.enabled && settings.show_colliders) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (entity, global) in &missing {
|
||||||
|
let color = collider_overlay_color(
|
||||||
|
diagnostics.entry(entity),
|
||||||
|
selected.0 == Some(entity),
|
||||||
|
collider_mode,
|
||||||
|
);
|
||||||
|
let center = global.translation();
|
||||||
|
let radius = if selected.0 == Some(entity) {
|
||||||
|
0.48
|
||||||
|
} else {
|
||||||
|
0.34
|
||||||
|
};
|
||||||
|
draw_box(
|
||||||
|
&mut gizmos,
|
||||||
|
center,
|
||||||
|
global.rotation(),
|
||||||
|
Vec3::splat(radius),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
for axis in [Vec3::X, Vec3::Y, Vec3::Z] {
|
||||||
|
let direction = global.rotation() * axis * radius * 1.35;
|
||||||
|
gizmos.line(center - direction, center + direction, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_navigation_visualizers(
|
fn draw_navigation_visualizers(
|
||||||
state: Res<crate::ui::navigation_inspector::NavigationEditorState>,
|
state: Res<crate::ui::navigation_inspector::NavigationEditorState>,
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
@ -556,17 +617,7 @@ fn draw_visualizers(
|
|||||||
|
|
||||||
if settings.show_colliders || collider_mode {
|
if settings.show_colliders || collider_mode {
|
||||||
for (entity, body, global) in &colliders {
|
for (entity, body, global) in &colliders {
|
||||||
let color = if selected.0 == Some(entity) {
|
let color = collider_overlay_color(None, selected.0 == Some(entity), collider_mode);
|
||||||
if collider_mode {
|
|
||||||
Color::srgba(0.65, 1.0, 0.55, 1.0)
|
|
||||||
} else {
|
|
||||||
Color::srgba(0.55, 1.0, 0.45, 0.95)
|
|
||||||
}
|
|
||||||
} else if collider_mode {
|
|
||||||
Color::srgba(0.35, 1.0, 0.45, 0.9)
|
|
||||||
} else {
|
|
||||||
Color::srgba(0.25, 0.9, 0.35, 0.55)
|
|
||||||
};
|
|
||||||
draw_collider(&mut gizmos, global, &body.collider, color);
|
draw_collider(&mut gizmos, global, &body.collider, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -679,11 +730,18 @@ fn draw_visualizers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "collider overlays combine authoring, runtime bounds, selection, and viewport state"
|
||||||
|
)]
|
||||||
fn draw_split_collider_visualizers(
|
fn draw_split_collider_visualizers(
|
||||||
viewport_mode: Res<EditorViewportMode>,
|
viewport_mode: Res<EditorViewportMode>,
|
||||||
settings: Res<EditorVisualizationSettings>,
|
settings: Res<EditorVisualizationSettings>,
|
||||||
|
diagnostics: Res<ColliderDiagnosticsState>,
|
||||||
selected: Res<SelectedEntity>,
|
selected: Res<SelectedEntity>,
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
|
children: Query<&Children>,
|
||||||
|
runtime_aabbs: Query<&ColliderAabb>,
|
||||||
colliders: Query<
|
colliders: Query<
|
||||||
(Entity, &ColliderDesc, &GlobalTransform),
|
(Entity, &ColliderDesc, &GlobalTransform),
|
||||||
(With<LevelObject>, Without<PhysicsBody>),
|
(With<LevelObject>, Without<PhysicsBody>),
|
||||||
@ -697,16 +755,79 @@ fn draw_split_collider_visualizers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (entity, collider, global) in &colliders {
|
for (entity, collider, global) in &colliders {
|
||||||
let color = if selected.0 == Some(entity) {
|
let color = collider_overlay_color(
|
||||||
Color::srgba(0.65, 1.0, 0.55, 1.0)
|
diagnostics.entry(entity),
|
||||||
} else if collider_mode {
|
selected.0 == Some(entity),
|
||||||
Color::srgba(0.35, 1.0, 0.45, 0.9)
|
collider_mode,
|
||||||
} else {
|
);
|
||||||
Color::srgba(0.25, 0.9, 0.35, 0.55)
|
let drew_runtime_mesh = matches!(collider.shape, ColliderShapeDesc::StaticMesh { .. })
|
||||||
};
|
&& draw_runtime_collider_bounds(&mut gizmos, entity, &children, &runtime_aabbs, color);
|
||||||
|
if !drew_runtime_mesh {
|
||||||
draw_collider_desc(&mut gizmos, global, collider, color);
|
draw_collider_desc(&mut gizmos, global, collider, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collider_overlay_color(
|
||||||
|
entry: Option<&ColliderDiagnosticEntry>,
|
||||||
|
selected: bool,
|
||||||
|
collider_mode: bool,
|
||||||
|
) -> Color {
|
||||||
|
let status = entry
|
||||||
|
.map(|entry| entry.overlay_status)
|
||||||
|
.unwrap_or(ColliderOverlayStatus::Valid);
|
||||||
|
let (rgb, base_alpha): (Vec3, f32) = match status {
|
||||||
|
ColliderOverlayStatus::Valid => (Vec3::new(0.25, 0.92, 0.43), 0.72),
|
||||||
|
ColliderOverlayStatus::Trigger => (Vec3::new(0.18, 0.82, 1.0), 0.78),
|
||||||
|
ColliderOverlayStatus::Disabled => (Vec3::new(0.52, 0.56, 0.62), 0.42),
|
||||||
|
ColliderOverlayStatus::Warning => (Vec3::new(1.0, 0.68, 0.18), 0.9),
|
||||||
|
ColliderOverlayStatus::Error => (Vec3::new(1.0, 0.24, 0.3), 0.96),
|
||||||
|
};
|
||||||
|
let alpha = if selected {
|
||||||
|
1.0
|
||||||
|
} else if collider_mode {
|
||||||
|
base_alpha.max(0.82)
|
||||||
|
} else {
|
||||||
|
base_alpha * 0.72
|
||||||
|
};
|
||||||
|
let rgb = if selected {
|
||||||
|
rgb.lerp(Vec3::ONE, 0.18)
|
||||||
|
} else {
|
||||||
|
rgb
|
||||||
|
};
|
||||||
|
Color::srgba(rgb.x, rgb.y, rgb.z, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_runtime_collider_bounds(
|
||||||
|
gizmos: &mut Gizmos,
|
||||||
|
root: Entity,
|
||||||
|
children: &Query<&Children>,
|
||||||
|
aabbs: &Query<&ColliderAabb>,
|
||||||
|
color: Color,
|
||||||
|
) -> bool {
|
||||||
|
let mut pending = vec![root];
|
||||||
|
let mut drew = false;
|
||||||
|
while let Some(entity) = pending.pop() {
|
||||||
|
if let Ok(aabb) = aabbs.get(entity) {
|
||||||
|
let min = aabb.min;
|
||||||
|
let max = aabb.max;
|
||||||
|
if min.is_finite() && max.is_finite() && min.cmplt(max).all() {
|
||||||
|
draw_box(
|
||||||
|
gizmos,
|
||||||
|
(min + max) * 0.5,
|
||||||
|
Quat::IDENTITY,
|
||||||
|
(max - min) * 0.5,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
drew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(entity_children) = children.get(entity) {
|
||||||
|
pending.extend(entity_children.iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drew
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_post_process_volume_gizmos(
|
fn draw_post_process_volume_gizmos(
|
||||||
viewport_mode: Res<EditorViewportMode>,
|
viewport_mode: Res<EditorViewportMode>,
|
||||||
@ -741,6 +862,8 @@ fn draw_collider(
|
|||||||
collider: &ColliderConstructor,
|
collider: &ColliderConstructor,
|
||||||
color: Color,
|
color: Color,
|
||||||
) {
|
) {
|
||||||
|
let (scale, rotation, translation) = global.to_scale_rotation_translation();
|
||||||
|
let scale = scale.abs();
|
||||||
match collider {
|
match collider {
|
||||||
ColliderConstructor::Cuboid {
|
ColliderConstructor::Cuboid {
|
||||||
x_length,
|
x_length,
|
||||||
@ -748,27 +871,21 @@ fn draw_collider(
|
|||||||
z_length,
|
z_length,
|
||||||
} => draw_box(
|
} => draw_box(
|
||||||
gizmos,
|
gizmos,
|
||||||
global.translation(),
|
translation,
|
||||||
global.rotation(),
|
rotation,
|
||||||
Vec3::new(*x_length, *y_length, *z_length) * 0.5,
|
Vec3::new(*x_length, *y_length, *z_length) * scale * 0.5,
|
||||||
color,
|
color,
|
||||||
),
|
),
|
||||||
ColliderConstructor::Sphere { radius } => {
|
ColliderConstructor::Sphere { radius } => {
|
||||||
gizmos
|
draw_ellipsoid(gizmos, translation, rotation, scale * *radius, color);
|
||||||
.sphere(
|
|
||||||
Isometry3d::new(global.translation(), global.rotation()),
|
|
||||||
*radius,
|
|
||||||
color,
|
|
||||||
)
|
|
||||||
.resolution(24);
|
|
||||||
}
|
}
|
||||||
ColliderConstructor::Capsule { radius, height } => {
|
ColliderConstructor::Capsule { radius, height } => {
|
||||||
draw_capsule(
|
draw_capsule(
|
||||||
gizmos,
|
gizmos,
|
||||||
global.translation(),
|
translation,
|
||||||
global.rotation(),
|
rotation,
|
||||||
*radius,
|
*radius * scale.x.max(scale.z),
|
||||||
*height,
|
*height * scale.y,
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -790,9 +907,8 @@ fn draw_collider_desc(
|
|||||||
collider: &ColliderDesc,
|
collider: &ColliderDesc,
|
||||||
color: Color,
|
color: Color,
|
||||||
) {
|
) {
|
||||||
if !collider.enabled {
|
let (scale, rotation, translation) = global.to_scale_rotation_translation();
|
||||||
return;
|
let scale = scale.abs();
|
||||||
}
|
|
||||||
match &collider.shape {
|
match &collider.shape {
|
||||||
ColliderShapeDesc::Cuboid {
|
ColliderShapeDesc::Cuboid {
|
||||||
x_length,
|
x_length,
|
||||||
@ -800,40 +916,57 @@ fn draw_collider_desc(
|
|||||||
z_length,
|
z_length,
|
||||||
} => draw_box(
|
} => draw_box(
|
||||||
gizmos,
|
gizmos,
|
||||||
global.translation(),
|
translation,
|
||||||
global.rotation(),
|
rotation,
|
||||||
Vec3::new(*x_length, *y_length, *z_length) * 0.5,
|
Vec3::new(*x_length, *y_length, *z_length) * scale * 0.5,
|
||||||
color,
|
color,
|
||||||
),
|
),
|
||||||
ColliderShapeDesc::Sphere { radius } => {
|
ColliderShapeDesc::Sphere { radius } => {
|
||||||
gizmos
|
draw_ellipsoid(gizmos, translation, rotation, scale * *radius, color);
|
||||||
.sphere(
|
|
||||||
Isometry3d::new(global.translation(), global.rotation()),
|
|
||||||
*radius,
|
|
||||||
color,
|
|
||||||
)
|
|
||||||
.resolution(24);
|
|
||||||
}
|
}
|
||||||
ColliderShapeDesc::Capsule { radius, height } => {
|
ColliderShapeDesc::Capsule { radius, height } => {
|
||||||
draw_capsule(
|
draw_capsule(
|
||||||
gizmos,
|
gizmos,
|
||||||
global.translation(),
|
translation,
|
||||||
global.rotation(),
|
rotation,
|
||||||
*radius,
|
*radius * scale.x.max(scale.z),
|
||||||
*height,
|
*height * scale.y,
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ColliderShapeDesc::StaticMesh { .. } => {
|
ColliderShapeDesc::StaticMesh { .. } => {
|
||||||
|
draw_box(gizmos, translation, rotation, Vec3::splat(0.24), color);
|
||||||
|
gizmos.line(
|
||||||
|
translation - rotation * Vec3::splat(0.34),
|
||||||
|
translation + rotation * Vec3::splat(0.34),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_ellipsoid(gizmos: &mut Gizmos, center: Vec3, rotation: Quat, radii: Vec3, color: Color) {
|
||||||
gizmos
|
gizmos
|
||||||
.sphere(
|
.ellipse(
|
||||||
Isometry3d::new(global.translation(), global.rotation()),
|
Isometry3d::new(center, rotation),
|
||||||
0.35,
|
Vec2::new(radii.x, radii.z),
|
||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
.resolution(16);
|
.resolution(32);
|
||||||
}
|
gizmos
|
||||||
}
|
.ellipse(
|
||||||
|
Isometry3d::new(center, rotation * Quat::from_rotation_x(FRAC_PI_2)),
|
||||||
|
Vec2::new(radii.x, radii.y),
|
||||||
|
color,
|
||||||
|
)
|
||||||
|
.resolution(32);
|
||||||
|
gizmos
|
||||||
|
.ellipse(
|
||||||
|
Isometry3d::new(center, rotation * Quat::from_rotation_z(FRAC_PI_2)),
|
||||||
|
Vec2::new(radii.y, radii.z),
|
||||||
|
color,
|
||||||
|
)
|
||||||
|
.resolution(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_light(
|
fn draw_light(
|
||||||
|
|||||||
@ -84,10 +84,12 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [editor/native-dialogs.md](editor/native-dialogs.md) | Non-blocking native dialog acquisition and main-thread result application |
|
| [editor/native-dialogs.md](editor/native-dialogs.md) | Non-blocking native dialog acquisition and main-thread result application |
|
||||||
| [editor/terrain.md](editor/terrain.md) | Terrain schema, inspector workflow, chunk hydration, collision, and follow-on boundaries |
|
| [editor/terrain.md](editor/terrain.md) | Terrain schema, inspector workflow, chunk hydration, collision, and follow-on boundaries |
|
||||||
| [editor/physics-placement.md](editor/physics-placement.md) | Transactional gravity placement, diagnostics, isolation, and undo workflow |
|
| [editor/physics-placement.md](editor/physics-placement.md) | Transactional gravity placement, diagnostics, isolation, and undo workflow |
|
||||||
|
| [editor/collider-authoring.md](editor/collider-authoring.md) | Collider shape authoring, hydration diagnostics, semantic overlays, and placement prerequisites |
|
||||||
| [editor/evaluations/material-renderer-foundation/](editor/evaluations/material-renderer-foundation/) | Live screenshots and acceptance results for the renderer/material/component foundation |
|
| [editor/evaluations/material-renderer-foundation/](editor/evaluations/material-renderer-foundation/) | Live screenshots and acceptance results for the renderer/material/component foundation |
|
||||||
| [editor/evaluations/terrain-sculpt-tools/](editor/evaluations/terrain-sculpt-tools/) | Live screenshot and acceptance results for modal terrain sculpt tools |
|
| [editor/evaluations/terrain-sculpt-tools/](editor/evaluations/terrain-sculpt-tools/) | Live screenshot and acceptance results for modal terrain sculpt tools |
|
||||||
| [editor/evaluations/terrain-material-layers/](editor/evaluations/terrain-material-layers/) | Live screenshot and acceptance results for terrain material assignment, blending, painting, and history |
|
| [editor/evaluations/terrain-material-layers/](editor/evaluations/terrain-material-layers/) | Live screenshot and acceptance results for terrain material assignment, blending, painting, and history |
|
||||||
| [editor/evaluations/physics-placement/](editor/evaluations/physics-placement/) | Live screenshot and acceptance results for physics settling, cancel, commit, undo, and redo |
|
| [editor/evaluations/physics-placement/](editor/evaluations/physics-placement/) | Live screenshot and acceptance results for physics settling, cancel, commit, undo, and redo |
|
||||||
|
| [editor/evaluations/collider-diagnostics/](editor/evaluations/collider-diagnostics/) | Live screenshot and acceptance results for collider overlays, diagnostics, shape history, and placement preflight |
|
||||||
| [editor/evaluations/navigation-authoring/](editor/evaluations/navigation-authoring/) | Live screenshots and acceptance results for navigation authoring and runtime parity |
|
| [editor/evaluations/navigation-authoring/](editor/evaluations/navigation-authoring/) | Live screenshots and acceptance results for navigation authoring and runtime parity |
|
||||||
| [editor/evaluations/production-readiness/](editor/evaluations/production-readiness/) | Current versioned production gate, evidence matrix, candidate commands, soak protocol, and independent sign-off requirements |
|
| [editor/evaluations/production-readiness/](editor/evaluations/production-readiness/) | Current versioned production gate, evidence matrix, candidate commands, soak protocol, and independent sign-off requirements |
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| [native-dialogs.md](native-dialogs.md) | Non-blocking file/folder/confirmation acquisition and main-thread result application |
|
| [native-dialogs.md](native-dialogs.md) | Non-blocking file/folder/confirmation acquisition and main-thread result application |
|
||||||
| [terrain.md](terrain.md) | Inline height-grid terrain, chunk hydration, collision, inspector workflow, and fixtures |
|
| [terrain.md](terrain.md) | Inline height-grid terrain, chunk hydration, collision, inspector workflow, and fixtures |
|
||||||
| [physics-placement.md](physics-placement.md) | Transactional gravity placement, prerequisites, isolation, commit/cancel, and undo |
|
| [physics-placement.md](physics-placement.md) | Transactional gravity placement, prerequisites, isolation, commit/cancel, and undo |
|
||||||
|
| [collider-authoring.md](collider-authoring.md) | Collider shape editing, shared health diagnostics, overlays, hydration status, and placement reuse |
|
||||||
| [evaluations/](evaluations/) | Acceptance evidence records and native Gitea attachment publishing policy |
|
| [evaluations/](evaluations/) | Acceptance evidence records and native Gitea attachment publishing policy |
|
||||||
| [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation |
|
| [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation |
|
||||||
| [evaluations/material-library-targeted-drop/](evaluations/material-library-targeted-drop/) | Live screenshot and verification record for the docked Material Library and exact reversible surface drops |
|
| [evaluations/material-library-targeted-drop/](evaluations/material-library-targeted-drop/) | Live screenshot and verification record for the docked Material Library and exact reversible surface drops |
|
||||||
@ -36,6 +37,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| [evaluations/terrain-sculpt-tools/](evaluations/terrain-sculpt-tools/) | Live screenshot and verification record for modal sculpt controls, footprint, and stroke history |
|
| [evaluations/terrain-sculpt-tools/](evaluations/terrain-sculpt-tools/) | Live screenshot and verification record for modal sculpt controls, footprint, and stroke history |
|
||||||
| [evaluations/terrain-material-layers/](evaluations/terrain-material-layers/) | Live screenshot and verification record for blended layers, assignment, painting, and history |
|
| [evaluations/terrain-material-layers/](evaluations/terrain-material-layers/) | Live screenshot and verification record for blended layers, assignment, painting, and history |
|
||||||
| [evaluations/physics-placement/](evaluations/physics-placement/) | Live screenshot and verification record for multi-prop settling, cancel, commit, undo, and redo |
|
| [evaluations/physics-placement/](evaluations/physics-placement/) | Live screenshot and verification record for multi-prop settling, cancel, commit, undo, and redo |
|
||||||
|
| [evaluations/collider-diagnostics/](evaluations/collider-diagnostics/) | Live screenshot and verification record for semantic collider overlays, health linking, shape switching, and placement reuse |
|
||||||
| [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity |
|
| [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity |
|
||||||
| [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence |
|
| [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence |
|
||||||
|
|
||||||
@ -47,6 +49,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| `shared::components::BrushDesc` / `shared::hydration::brushes` | Persisted brush authoring data and generated runtime mesh hydration | brushes.md, ADR 0021 |
|
| `shared::components::BrushDesc` / `shared::hydration::brushes` | Persisted brush authoring data and generated runtime mesh hydration | brushes.md, ADR 0021 |
|
||||||
| `shared::components::TerrainDesc` / `shared::hydration::terrain` / `blacksite_surface` | Persisted height/layer grids, generated chunks, and raster blending | terrain.md, ADR 0039, ADR 0040 |
|
| `shared::components::TerrainDesc` / `shared::hydration::terrain` / `blacksite_surface` | Persisted height/layer grids, generated chunks, and raster blending | terrain.md, ADR 0039, ADR 0040 |
|
||||||
| `viewport/physics_placement.rs` / `play/state.rs` | Paused Edit physics and transactional gravity placement | physics-placement.md, ADR 0041 |
|
| `viewport/physics_placement.rs` / `play/state.rs` | Paused Edit physics and transactional gravity placement | physics-placement.md, ADR 0041 |
|
||||||
|
| `viewport/collider_diagnostics.rs` / `viewport/visualizers.rs` / `ui/inspector.rs` | Shared collider health, semantic overlays, and undoable shape authoring | collider-authoring.md |
|
||||||
| `scene/` | Level I/O, tabs, composition materialization, schema, viewport render-target setup | architecture.md, multi-scene-composition.md, ADR 0026 |
|
| `scene/` | Level I/O, tabs, composition materialization, schema, viewport render-target setup | architecture.md, multi-scene-composition.md, ADR 0026 |
|
||||||
| `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 |
|
||||||
@ -89,7 +92,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
- **Brush authoring** — `ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector and Window → Brush Diagnostics, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md).
|
- **Brush authoring** — `ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector and Window → Brush Diagnostics, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md).
|
||||||
- **Draw Brush** — `B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter locks the outline for height editing, mouse up/down adjusts height, and Enter/LMB creates additive prism brushes through history. Esc/right-click cancels. Simple concave outlines decompose into convex brush parts; self-intersections remain blocked.
|
- **Draw Brush** — `B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter locks the outline for height editing, mouse up/down adjusts height, and Enter/LMB creates additive prism brushes through history. Esc/right-click cancels. Simple concave outlines decompose into convex brush parts; self-intersections remain blocked.
|
||||||
- **Brush edit modes** — with a brush selected, `1`/`2`/`3`/`4` enter vertex, edge, face, and clip element modes. Element modes show brush handles in the viewport, own LMB picking, support Shift multi-select, show a mode badge, and Esc returns to object mode. Vertex/edge/face selections use the standard `W`/`E`/`R` gizmo at the element pivot and commit undoable `SetBrush` edits. Clip previews a bounds-based half-brush and commits with Enter; command-palette intersect, convex merge, and subtract operations use the same preview/commit lifecycle for conservative cuboid/prism blockout.
|
- **Brush edit modes** — with a brush selected, `1`/`2`/`3`/`4` enter vertex, edge, face, and clip element modes. Element modes show brush handles in the viewport, own LMB picking, support Shift multi-select, show a mode badge, and Esc returns to object mode. Vertex/edge/face selections use the standard `W`/`E`/`R` gizmo at the element pivot and commit undoable `SetBrush` edits. Clip previews a bounds-based half-brush and commits with Enter; command-palette intersect, convex merge, and subtract operations use the same preview/commit lifecycle for conservative cuboid/prism blockout.
|
||||||
- **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 and health** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references. Inspector, viewport, Diagnostics, and physics placement share one authored/runtime health result; see [collider-authoring.md](collider-authoring.md).
|
||||||
- **Material assets and renderer slots** — Material and direct-base Material Instance documents are shared assets. `StaticMeshRenderer` and `SkinnedMeshRenderer` each own stable material slots with imported source defaults and explicit shared assignments; Clear returns to the source default, while reimported missing slots retain overrides as orphans instead of matching by name. Runtime-only property blocks never mutate or persist into the shared asset. Custom Surface evaluators use the same constrained ABI for raster and Solari-eligible non-deformed geometry; skinned/morph geometry is deliberately excluded from Solari until deformed BLAS updates exist. See [material-system.md](material-system.md), [ADR 0035](../adr/0035-shared-material-assets-and-renderer-slots.md), and [ADR 0036](../adr/0036-surface-abi-and-solari-parity.md).
|
- **Material assets and renderer slots** — Material and direct-base Material Instance documents are shared assets. `StaticMeshRenderer` and `SkinnedMeshRenderer` each own stable material slots with imported source defaults and explicit shared assignments; Clear returns to the source default, while reimported missing slots retain overrides as orphans instead of matching by name. Runtime-only property blocks never mutate or persist into the shared asset. Custom Surface evaluators use the same constrained ABI for raster and Solari-eligible non-deformed geometry; skinned/morph geometry is deliberately excluded from Solari until deformed BLAS updates exist. See [material-system.md](material-system.md), [ADR 0035](../adr/0035-shared-material-assets-and-renderer-slots.md), and [ADR 0036](../adr/0036-surface-abi-and-solari-parity.md).
|
||||||
- **Prefab authoring** — shared `PrefabOverrides` targets generated actors by nested anchor chain plus stable `ActorId`, and applies reflected property/component plus same-layer structural operations in editor and game hydration. The editor supports scoped Revert and transactional **Apply to source** with three-way conflict checks, exact-file undo guards, and recoverable changed/broken/conflict states. `HydratedPrefabMember` keeps generated content out of owner serialization; linked-root saves retain local structure as variants. **Unpack Layer** preserves nested links, while **Convert to Local** recursively removes them. See [prefab-authoring.md](prefab-authoring.md) and [ADR 0027](../adr/0027-stable-prefab-ownership-and-variants.md).
|
- **Prefab authoring** — shared `PrefabOverrides` targets generated actors by nested anchor chain plus stable `ActorId`, and applies reflected property/component plus same-layer structural operations in editor and game hydration. The editor supports scoped Revert and transactional **Apply to source** with three-way conflict checks, exact-file undo guards, and recoverable changed/broken/conflict states. `HydratedPrefabMember` keeps generated content out of owner serialization; linked-root saves retain local structure as variants. **Unpack Layer** preserves nested links, while **Convert to Local** recursively removes them. See [prefab-authoring.md](prefab-authoring.md) and [ADR 0027](../adr/0027-stable-prefab-ownership-and-variants.md).
|
||||||
- **Visual language** uses a near-black industrial shell, a compact Blacksite identity mark, restrained amber actions/primary selection, cyan secondary selection, semantic status colors, stable grouped controls, and shared elevated viewport chips. Primary/secondary selection roles remain consistent across hierarchy rows, viewport x-ray shells/corner brackets, the selection HUD, and transform interactions. See [visual-language.md](visual-language.md).
|
- **Visual language** uses a near-black industrial shell, a compact Blacksite identity mark, restrained amber actions/primary selection, cyan secondary selection, semantic status colors, stable grouped controls, and shared elevated viewport chips. Primary/secondary selection roles remain consistent across hierarchy rows, viewport x-ray shells/corner brackets, the selection HUD, and transform interactions. See [visual-language.md](visual-language.md).
|
||||||
|
|||||||
55
docs/editor/collider-authoring.md
Normal file
55
docs/editor/collider-authoring.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Collider Authoring And Diagnostics
|
||||||
|
|
||||||
|
`ColliderDesc` is the saved collision shape. `RigidBodyDesc` independently controls authored body
|
||||||
|
motion, while Avian `Collider` and `RigidBody` components are hydrated runtime state. Collider
|
||||||
|
health compares both sides of that boundary so editor tools do not treat authored-but-unavailable
|
||||||
|
collision as ready.
|
||||||
|
|
||||||
|
## Inspector Workflow
|
||||||
|
|
||||||
|
The Collider component card reports its shape, health, and hydration state. Box, sphere, capsule,
|
||||||
|
and static mesh shapes can be switched from the Shape menu. Primitive conversions retain the
|
||||||
|
previous shape's useful extents; each completed switch uses `SetCollider` history and is one undo
|
||||||
|
operation. Invalid shapes expose a reset action. Static mesh sources can be selected or replaced
|
||||||
|
with the actor's renderer meshes.
|
||||||
|
|
||||||
|
Dimensions are clamped to positive values in normal inspector editing. Project validation still
|
||||||
|
rejects hand-edited or migrated non-finite/zero dimensions and empty mesh source lists.
|
||||||
|
|
||||||
|
## Viewport Contract
|
||||||
|
|
||||||
|
Collider view hides render meshes and emphasizes authored collision using semantic status colors:
|
||||||
|
|
||||||
|
| Color | State |
|
||||||
|
|-------|-------|
|
||||||
|
| Green | Valid hydrated solid collider |
|
||||||
|
| Cyan | Valid hydrated trigger |
|
||||||
|
| Gray | Disabled or inactive collider |
|
||||||
|
| Amber | Stale hydration or oversized collider |
|
||||||
|
| Red | Missing or invalid collider |
|
||||||
|
|
||||||
|
Box, sphere/ellipsoid, and capsule wireframes include actor world scale. Static mesh collision uses
|
||||||
|
the cooked runtime collider bounds when available. A rigid body with no collider receives a red
|
||||||
|
spatial marker, so missing collision remains discoverable with meshes hidden. The lower-left HUD
|
||||||
|
summarizes scene health and identifies the selected collider's shape and hydration state.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
The shared health service reports:
|
||||||
|
|
||||||
|
- missing/inactive/disabled collider authoring;
|
||||||
|
- non-positive or non-finite primitive dimensions;
|
||||||
|
- empty, unresolved, or registry-stale static mesh references;
|
||||||
|
- valid authoring that has not produced a runtime collider;
|
||||||
|
- primitive colliders exceeding 1,000 meters on any world-space axis.
|
||||||
|
|
||||||
|
The 1,000-meter warning is conservative rather than a save blocker. Large worlds should split
|
||||||
|
collision into spatially bounded actors to keep broad-phase behavior predictable. Diagnostics can
|
||||||
|
select the affected actor; static mesh fields retain their asset-browser locate action.
|
||||||
|
|
||||||
|
Physics Placement consumes the same errors plus its stricter non-trigger and hydrated rigid-body
|
||||||
|
requirements. See [physics-placement.md](physics-placement.md).
|
||||||
|
|
||||||
|
`assets/levels/collider_diagnostics_showcase.scn.ron` contains valid box, scaled sphere, capsule,
|
||||||
|
trigger, disabled, and missing-collider states for visual acceptance. Invalid, oversized, and stale
|
||||||
|
asset cases live in unit fixtures so the committed project remains release-valid.
|
||||||
24
docs/editor/evaluations/collider-diagnostics/README.md
Normal file
24
docs/editor/evaluations/collider-diagnostics/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Collider Diagnostics Acceptance
|
||||||
|
|
||||||
|
Issue: Gitea #26 (`BS-JD-305`)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The native Wayland editor loaded `assets/levels/collider_diagnostics_showcase.scn.ron` and entered
|
||||||
|
Collider view. The captured frame shows shape overlays with meshes hidden, the scene health HUD,
|
||||||
|
the selectable Diagnostics window, and the selected trigger's hydrated status in the inspector.
|
||||||
|
|
||||||
|
## Live Results
|
||||||
|
|
||||||
|
- Box, scaled sphere/ellipsoid, and capsule overlays matched authored transforms and dimensions.
|
||||||
|
- Green ready, cyan trigger, gray disabled, and red missing states were visually distinct.
|
||||||
|
- The HUD reported `4 ready / 1 disabled / 0 warning / 1 invalid` and followed selection.
|
||||||
|
- The Diagnostics panel linked the missing finding back to its actor.
|
||||||
|
- Switching the valid box to sphere preserved useful extents and produced `Undo: Set Collider`;
|
||||||
|
undo restored the box.
|
||||||
|
- Physics Placement rejected the missing-collider actor with the shared collider-health messages.
|
||||||
|
- Scene switching and editor shutdown completed without a panic or runtime validation error.
|
||||||
|
|
||||||
|
Packaged verification remains deferred by current project instruction.
|
||||||
BIN
docs/editor/evaluations/collider-diagnostics/collider-diagnostics-live.png
(Stored with Git LFS)
Normal file
BIN
docs/editor/evaluations/collider-diagnostics/collider-diagnostics-live.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -59,6 +59,7 @@ another commit, a dirty worktree, or an older package do not transfer to the can
|
|||||||
| Material catalog and assignment | Material Library and exact targeted drops accepted; `#51` open only on deformed Solari geometry | Not rerun as one candidate |
|
| Material catalog and assignment | Material Library and exact targeted drops accepted; `#51` open only on deformed Solari geometry | Not rerun as one candidate |
|
||||||
| Terrain authoring | `#22`-`#24` open | Blocked |
|
| Terrain authoring | `#22`-`#24` open | Blocked |
|
||||||
| Physics placement | `#25` source/live acceptance complete | Pass; packaged-runtime acceptance owner-deferred |
|
| Physics placement | `#25` source/live acceptance complete | Pass; packaged-runtime acceptance owner-deferred |
|
||||||
|
| Collider authoring and diagnostics | `#26` source/live acceptance complete | Pass; packaged-runtime acceptance owner-deferred |
|
||||||
| Animation | Implemented | Not rerun |
|
| Animation | Implemented | Not rerun |
|
||||||
| Audio | Implemented | Not rerun |
|
| Audio | Implemented | Not rerun |
|
||||||
| Navigation | Implemented | Not rerun |
|
| Navigation | Implemented | Not rerun |
|
||||||
|
|||||||
@ -24,9 +24,10 @@ history. During placement, every non-selected dynamic or kinematic body is tempo
|
|||||||
a static collision surface. The tool restores those bodies exactly when the preview ends. Entering
|
a static collision surface. The tool restores those bodies exactly when the preview ends. Entering
|
||||||
Play mode cancels an active placement preview.
|
Play mode cancels an active placement preview.
|
||||||
|
|
||||||
The start action is non-destructive when prerequisites are missing. The viewport and status strip
|
The start action is non-destructive when prerequisites are missing. It consumes the shared
|
||||||
identify locked/linked selections, missing or disabled rigid bodies, missing/trigger colliders, and
|
[collider health service](collider-authoring.md), so the viewport, inspector, Diagnostics panel,
|
||||||
colliders that are still cooking. A ten-second simulated-time limit stops unstable previews while
|
and placement status agree about missing, invalid, stale, disabled, or trigger collision. A
|
||||||
|
ten-second simulated-time limit stops unstable previews while
|
||||||
leaving explicit commit and cancel available.
|
leaving explicit commit and cancel available.
|
||||||
|
|
||||||
`assets/levels/physics_placement_showcase.scn.ron` is the committed acceptance fixture. Select
|
`assets/levels/physics_placement_showcase.scn.ron` is the committed acceptance fixture. Select
|
||||||
|
|||||||
@ -58,6 +58,7 @@ Phased work for the in-process editor (`crates/editor`). Status reflects the cur
|
|||||||
| Tab cycle overlapping picks | Done | `ViewportPickStack` + Tab in viewport |
|
| Tab cycle overlapping picks | Done | `ViewportPickStack` + Tab in viewport |
|
||||||
| Multi-viewport | Scaffolded | Primary viewport state is separated from render ownership; secondary viewports remain future work |
|
| Multi-viewport | Scaffolded | Primary viewport state is separated from render ownership; secondary viewports remain future work |
|
||||||
| Physics placement | Done | Existing horizontal toolbar action; real Avian settling, prerequisite diagnostics, exact cancel, grouped undo, and isolated non-selected bodies |
|
| Physics placement | Done | Existing horizontal toolbar action; real Avian settling, prerequisite diagnostics, exact cancel, grouped undo, and isolated non-selected bodies |
|
||||||
|
| Collider authoring diagnostics | Done | World-scale shape overlays, semantic health colors, stale/missing/invalid/oversized findings, undoable shape switching, and shared placement preflight |
|
||||||
|
|
||||||
## Phase 2b — Advanced rendering
|
## Phase 2b — Advanced rendering
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user