From f29712d15850940e9c076d2ad9ec177d39d7d421 Mon Sep 17 00:00:00 2001 From: Rbanh Date: Fri, 5 Jun 2026 21:44:45 -0400 Subject: [PATCH] Initial project import --- .cargo/config.toml | 8 + ...mponent_system_refactor_2026-06-05.plan.md | 23 + .../plans/project_roadmap_8a452d43.plan.md | 215 + .../rendering_unification_2026-06-05.plan.md | 27 + ...tic_mesh_asset_refactor_2026-06-05.plan.md | 26 + .cursor/rules/documentation.mdc | 42 + .gitattributes | 34 + .github/workflows/ci.yml | 76 + .gitignore | 26 + .vscode/extensions.json | 9 + .vscode/launch.json | 51 + .vscode/settings.json | 27 + .vscode/tasks.json | 185 + AGENTS.md | 35 + Cargo.lock | 8217 +++++++++++++++++ Cargo.toml | 63 + README.md | 314 + assets/.index/registry.ron | 226 + assets/audio/.gitkeep | 0 assets/levels/.gitkeep | 0 assets/levels/editor_scene 2.scn.ron | 1178 +++ assets/levels/editor_scene 3.scn.ron | 1504 +++ assets/levels/editor_scene.scn.ron | 959 ++ assets/levels/rendering_showcase.scn.ron | 74 + assets/materials/.gitkeep | 0 assets/materials/concrete.ron | 12 + assets/materials/emissive_panel.ron | 15 + ...32d-49da-9424-ae38b3d60648.static_mesh.ron | 82 + ...eee-4ee8-9e16-53aedbc69205.static_mesh.ron | 770 ++ assets/models/.gitkeep | 0 assets/models/Dumpster.fbx | 3 + assets/models/Room3.fbx | 3 + assets/post_fx/chromatic_aberration.ron | 7 + assets/post_fx/chromatic_aberration.wgsl | 20 + assets/post_fx/vignette.ron | 7 + assets/post_fx/vignette.wgsl | 19 + assets/project.ron | 58 + assets/rendering_profiles/cave_dark.ron | 9 + assets/rendering_profiles/outdoor_haze.ron | 9 + assets/shaders/standard_lit.shader.ron | 18 + assets/shaders/unlit.shader.ron | 13 + assets/textures/.gitkeep | 0 crates/editor/Cargo.toml | 45 + crates/editor/assets/icons/01_actor_empty.png | 3 + .../editor/assets/icons/02_actor_generic.png | 3 + .../editor/assets/icons/03_actor_missing.png | 3 + .../editor/assets/icons/04_actor_warning.png | 3 + crates/editor/assets/icons/05_render_mesh.png | 3 + .../editor/assets/icons/06_render_decal.png | 3 + .../assets/icons/07_render_particle.png | 3 + .../assets/icons/08_render_billboard.png | 3 + crates/editor/assets/icons/09_light_point.png | 3 + crates/editor/assets/icons/10_light_spot.png | 3 + .../assets/icons/11_light_directional.png | 3 + crates/editor/assets/icons/12_light_area.png | 3 + crates/editor/assets/icons/13_camera_main.png | 3 + .../editor/assets/icons/14_camera_capture.png | 3 + .../editor/assets/icons/15_audio_source.png | 3 + .../editor/assets/icons/16_audio_listener.png | 3 + .../assets/icons/17_physics_collider.png | 3 + .../assets/icons/18_physics_rigidbody.png | 3 + .../assets/icons/19_physics_trigger.png | 3 + .../editor/assets/icons/20_physics_joint.png | 3 + crates/editor/assets/icons/21_game_spawn.png | 3 + .../editor/assets/icons/22_game_trigger.png | 3 + crates/editor/assets/icons/23_game_marker.png | 3 + .../assets/icons/24_game_interactable.png | 3 + .../editor/assets/icons/25_nav_waypoint.png | 3 + crates/editor/assets/icons/26_nav_link.png | 3 + crates/editor/assets/icons/27_nav_region.png | 3 + .../editor/assets/icons/28_volume_generic.png | 3 + .../assets/icons/29_volume_post_process.png | 3 + .../assets/icons/30_environment_wind.png | 3 + .../assets/icons/31_environment_sky.png | 3 + crates/editor/assets/icons/32_debug_note.png | 3 + .../assets/shaders/selection_outline.wgsl | 28 + crates/editor/src/assets/asset_db.rs | 257 + crates/editor/src/assets/catalog.rs | 711 ++ crates/editor/src/assets/import.rs | 71 + crates/editor/src/assets/materials.rs | 101 + crates/editor/src/assets/mod.rs | 15 + crates/editor/src/assets/prefab_overrides.rs | 390 + crates/editor/src/assets/static_mesh.rs | 648 ++ crates/editor/src/assets/thumbnails/cache.rs | 379 + crates/editor/src/assets/thumbnails/job.rs | 7 + crates/editor/src/assets/thumbnails/mod.rs | 26 + .../src/assets/thumbnails/sources/fbx.rs | 123 + .../src/assets/thumbnails/sources/gltf.rs | 82 + .../src/assets/thumbnails/sources/mod.rs | 33 + crates/editor/src/assets/thumbnails/studio.rs | 544 ++ crates/editor/src/bin/main.rs | 13 + crates/editor/src/ext/brp.rs | 13 + crates/editor/src/ext/command_queue.rs | 15 + crates/editor/src/ext/extensibility.rs | 370 + crates/editor/src/ext/game_inspector.rs | 50 + crates/editor/src/ext/game_panels.rs | 48 + crates/editor/src/ext/hot_reload.rs | 149 + crates/editor/src/ext/mod.rs | 9 + crates/editor/src/history/commands.rs | 151 + crates/editor/src/history/mod.rs | 1385 +++ crates/editor/src/infra.rs | 25 + crates/editor/src/lib.rs | 140 + crates/editor/src/play/mod.rs | 7 + crates/editor/src/play/net_editor.rs | 57 + crates/editor/src/play/session.rs | 117 + crates/editor/src/play/state.rs | 415 + crates/editor/src/project/mod.rs | 5 + crates/editor/src/project/project_io.rs | 162 + crates/editor/src/project/settings_ui.rs | 306 + crates/editor/src/project/workspace.rs | 150 + crates/editor/src/render_target.rs | 35 + crates/editor/src/scene/mod.rs | 5 + crates/editor/src/scene/scene_io.rs | 628 ++ crates/editor/src/scene/scene_schema.rs | 20 + crates/editor/src/scene/scene_view.rs | 78 + crates/editor/src/ui/actor_inspector/mod.rs | 138 + .../src/ui/actor_inspector/transform.rs | 109 + crates/editor/src/ui/asset_browser/mod.rs | 5 + crates/editor/src/ui/asset_browser/panel.rs | 1167 +++ crates/editor/src/ui/asset_browser/state.rs | 62 + crates/editor/src/ui/component_registry.rs | 158 + crates/editor/src/ui/diagnostics.rs | 65 + crates/editor/src/ui/dock_tabs.rs | 158 + crates/editor/src/ui/fonts.rs | 21 + crates/editor/src/ui/helpers.rs | 297 + crates/editor/src/ui/hierarchy.rs | 718 ++ crates/editor/src/ui/hierarchy_ops.rs | 445 + crates/editor/src/ui/hierarchy_state.rs | 142 + crates/editor/src/ui/inspector.rs | 2547 +++++ crates/editor/src/ui/layout.rs | 145 + crates/editor/src/ui/menu.rs | 195 + crates/editor/src/ui/mod.rs | 347 + crates/editor/src/ui/play_controls.rs | 39 + .../editor/src/ui/post_process_volume_ui.rs | 341 + crates/editor/src/ui/reflect_inspector.rs | 16 + crates/editor/src/ui/selection_ops.rs | 60 + crates/editor/src/ui/status_bar.rs | 66 + crates/editor/src/ui/theme.rs | 138 + crates/editor/src/ui/toolbar.rs | 155 + crates/editor/src/ui/viewport_chrome.rs | 530 ++ crates/editor/src/ui/widgets.rs | 117 + crates/editor/src/viewport/actor_icons.rs | 656 ++ crates/editor/src/viewport/camera.rs | 179 + crates/editor/src/viewport/gizmos.rs | 107 + crates/editor/src/viewport/mod.rs | 16 + crates/editor/src/viewport/panel.rs | 282 + crates/editor/src/viewport/render_view.rs | 212 + .../src/viewport/rendering_diagnostics.rs | 485 + crates/editor/src/viewport/selection.rs | 375 + .../editor/src/viewport/selection_outline.rs | 441 + crates/editor/src/viewport/viewport_mode.rs | 231 + crates/editor/src/viewport/visualizers.rs | 800 ++ crates/game/Cargo.toml | 23 + crates/game/src/editor_ext.rs | 30 + crates/game/src/launch.rs | 101 + crates/game/src/lib.rs | 191 + crates/game/src/main.rs | 9 + crates/game_hot/Cargo.toml | 24 + crates/game_hot/src/lib.rs | 53 + crates/game_hot/src/player/camera.rs | 48 + crates/game_hot/src/player/input.rs | 78 + crates/game_hot/src/player/mod.rs | 9 + crates/game_hot/src/player/spawn.rs | 47 + .../game_hot/src/rendering/auto_exposure.rs | 31 + crates/game_hot/src/rendering/camera_fx.rs | 413 + .../src/rendering/fullscreen_effects.rs | 116 + crates/game_hot/src/rendering/mod.rs | 25 + crates/game_hot/src/rendering/solari.rs | 564 ++ .../game_hot/src/rendering/viewport_camera.rs | 244 + crates/game_hot/src/rendering/volumes.rs | 487 + crates/game_hot/src/sim_systems.rs | 159 + crates/game_hot/src/world/level.rs | 115 + crates/game_hot/src/world/lighting.rs | 116 + crates/game_hot/src/world/mod.rs | 27 + crates/protocol/Cargo.toml | 10 + crates/protocol/src/lib.rs | 115 + crates/scene/Cargo.toml | 11 + crates/scene/src/lib.rs | 195 + crates/scene/src/migrate.rs | 232 + crates/settings/Cargo.toml | 14 + crates/settings/src/lib.rs | 264 + crates/settings/src/plugin.rs | 112 + crates/settings/src/rendering.rs | 263 + crates/shared/Cargo.toml | 13 + crates/shared/src/actor.rs | 247 + crates/shared/src/components.rs | 1043 +++ crates/shared/src/hydration/lights.rs | 297 + crates/shared/src/hydration/materials.rs | 92 + crates/shared/src/hydration/mod.rs | 324 + crates/shared/src/hydration/models.rs | 36 + crates/shared/src/hydration/physics.rs | 97 + crates/shared/src/hydration/prefabs.rs | 43 + crates/shared/src/hydration/primitives.rs | 84 + crates/shared/src/hydration/static_meshes.rs | 378 + crates/shared/src/hydration/strip.rs | 41 + crates/shared/src/hydration/visibility.rs | 41 + crates/shared/src/lib.rs | 90 + crates/shared/src/material_asset.rs | 66 + .../shared/src/post_process_effect_asset.rs | 30 + crates/shared/src/rendering_profile_asset.rs | 21 + crates/sim/Cargo.toml | 16 + crates/sim/src/lib.rs | 392 + docs/README.md | 72 + docs/adr/0001-roadmap-architecture.md | 34 + .../0002-bevy-version-and-migration-policy.md | 29 + docs/adr/0003-editor-framework-mission.md | 43 + docs/adr/0004-editor-hot-reload.md | 48 + ...0005-prefab-instance-and-asset-registry.md | 21 + docs/adr/0006-scene-schema-versioning.md | 20 + docs/adr/0007-editor-plugin-extensibility.md | 20 + docs/adr/0008-editor-module-boundaries.md | 41 + docs/adr/0009-authoring-vs-hydrated.md | 22 + docs/adr/0010-actor-kind-and-inspector.md | 22 + docs/adr/0011-scene-sun-policy.md | 21 + docs/adr/0012-zero-tech-debt-editor.md | 24 + ...endering-tiers-and-post-process-volumes.md | 55 + docs/adr/0014-unified-viewport-model.md | 38 + .../0015-viewport-camera-stack-ownership.md | 40 + docs/adr/0016-unified-rendering-contract.md | 49 + .../adr/0017-normalized-static-mesh-assets.md | 32 + ...onentized-actor-inspector-and-materials.md | 57 + .../Bevy_Editor_Component_System_Spec.md | 353 + docs/editor/README.md | 60 + docs/editor/architecture.md | 183 + docs/editor/brp.md | 132 + docs/editor/debt-audit.md | 31 + docs/editor/evaluations/h1-signoff.md | 27 + docs/editor/evaluations/h2-signoff.md | 22 + docs/editor/evaluations/h3-signoff.md | 17 + docs/editor/evaluations/h4-signoff.md | 16 + docs/editor/evaluations/h5-signoff.md | 16 + docs/editor/evaluations/h6-signoff.md | 16 + docs/editor/release-notes.md | 20 + docs/editor/rendering.md | 107 + docs/editor/roadmap.md | 146 + docs/mission.md | 35 + rust-toolchain.toml | 3 + source_assets/blender/.gitkeep | 0 source_assets/references/.gitkeep | 0 source_assets/textures/.gitkeep | 0 source_assets/vendor/.gitkeep | 0 third_party/transform-gizmo-bevy/.cargo-ok | 1 + .../transform-gizmo-bevy/.cargo_vcs_info.json | 6 + third_party/transform-gizmo-bevy/Cargo.lock | 6313 +++++++++++++ third_party/transform-gizmo-bevy/Cargo.toml | 303 + .../transform-gizmo-bevy/Cargo.toml.orig | 50 + third_party/transform-gizmo-bevy/PATCH.md | 13 + third_party/transform-gizmo-bevy/README.md | 44 + .../examples/bevy_minimal.rs | 44 + .../examples/ui_blocked_gizmo.rs | 58 + .../transform-gizmo-bevy/src/gizmo.wgsl | 31 + third_party/transform-gizmo-bevy/src/lib.rs | 669 ++ .../src/mouse_interact.rs | 26 + .../transform-gizmo-bevy/src/picking.rs | 48 + .../transform-gizmo-bevy/src/prelude.rs | 3 + .../transform-gizmo-bevy/src/render.rs | 420 + xtask/Cargo.toml | 17 + xtask/src/clean_target.rs | 410 + xtask/src/validate_levels.rs | 26 + 259 files changed, 52069 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .cursor/plans/component_system_refactor_2026-06-05.plan.md create mode 100644 .cursor/plans/project_roadmap_8a452d43.plan.md create mode 100644 .cursor/plans/rendering_unification_2026-06-05.plan.md create mode 100644 .cursor/plans/static_mesh_asset_refactor_2026-06-05.plan.md create mode 100644 .cursor/rules/documentation.mdc create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 AGENTS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 assets/.index/registry.ron create mode 100644 assets/audio/.gitkeep create mode 100644 assets/levels/.gitkeep create mode 100644 assets/levels/editor_scene 2.scn.ron create mode 100644 assets/levels/editor_scene 3.scn.ron create mode 100644 assets/levels/editor_scene.scn.ron create mode 100644 assets/levels/rendering_showcase.scn.ron create mode 100644 assets/materials/.gitkeep create mode 100644 assets/materials/concrete.ron create mode 100644 assets/materials/emissive_panel.ron create mode 100644 assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron create mode 100644 assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron create mode 100644 assets/models/.gitkeep create mode 100755 assets/models/Dumpster.fbx create mode 100755 assets/models/Room3.fbx create mode 100644 assets/post_fx/chromatic_aberration.ron create mode 100644 assets/post_fx/chromatic_aberration.wgsl create mode 100644 assets/post_fx/vignette.ron create mode 100644 assets/post_fx/vignette.wgsl create mode 100644 assets/project.ron create mode 100644 assets/rendering_profiles/cave_dark.ron create mode 100644 assets/rendering_profiles/outdoor_haze.ron create mode 100644 assets/shaders/standard_lit.shader.ron create mode 100644 assets/shaders/unlit.shader.ron create mode 100644 assets/textures/.gitkeep create mode 100644 crates/editor/Cargo.toml create mode 100644 crates/editor/assets/icons/01_actor_empty.png create mode 100644 crates/editor/assets/icons/02_actor_generic.png create mode 100644 crates/editor/assets/icons/03_actor_missing.png create mode 100644 crates/editor/assets/icons/04_actor_warning.png create mode 100644 crates/editor/assets/icons/05_render_mesh.png create mode 100644 crates/editor/assets/icons/06_render_decal.png create mode 100644 crates/editor/assets/icons/07_render_particle.png create mode 100644 crates/editor/assets/icons/08_render_billboard.png create mode 100644 crates/editor/assets/icons/09_light_point.png create mode 100644 crates/editor/assets/icons/10_light_spot.png create mode 100644 crates/editor/assets/icons/11_light_directional.png create mode 100644 crates/editor/assets/icons/12_light_area.png create mode 100644 crates/editor/assets/icons/13_camera_main.png create mode 100644 crates/editor/assets/icons/14_camera_capture.png create mode 100644 crates/editor/assets/icons/15_audio_source.png create mode 100644 crates/editor/assets/icons/16_audio_listener.png create mode 100644 crates/editor/assets/icons/17_physics_collider.png create mode 100644 crates/editor/assets/icons/18_physics_rigidbody.png create mode 100644 crates/editor/assets/icons/19_physics_trigger.png create mode 100644 crates/editor/assets/icons/20_physics_joint.png create mode 100644 crates/editor/assets/icons/21_game_spawn.png create mode 100644 crates/editor/assets/icons/22_game_trigger.png create mode 100644 crates/editor/assets/icons/23_game_marker.png create mode 100644 crates/editor/assets/icons/24_game_interactable.png create mode 100644 crates/editor/assets/icons/25_nav_waypoint.png create mode 100644 crates/editor/assets/icons/26_nav_link.png create mode 100644 crates/editor/assets/icons/27_nav_region.png create mode 100644 crates/editor/assets/icons/28_volume_generic.png create mode 100644 crates/editor/assets/icons/29_volume_post_process.png create mode 100644 crates/editor/assets/icons/30_environment_wind.png create mode 100644 crates/editor/assets/icons/31_environment_sky.png create mode 100644 crates/editor/assets/icons/32_debug_note.png create mode 100644 crates/editor/assets/shaders/selection_outline.wgsl create mode 100644 crates/editor/src/assets/asset_db.rs create mode 100644 crates/editor/src/assets/catalog.rs create mode 100644 crates/editor/src/assets/import.rs create mode 100644 crates/editor/src/assets/materials.rs create mode 100644 crates/editor/src/assets/mod.rs create mode 100644 crates/editor/src/assets/prefab_overrides.rs create mode 100644 crates/editor/src/assets/static_mesh.rs create mode 100644 crates/editor/src/assets/thumbnails/cache.rs create mode 100644 crates/editor/src/assets/thumbnails/job.rs create mode 100644 crates/editor/src/assets/thumbnails/mod.rs create mode 100644 crates/editor/src/assets/thumbnails/sources/fbx.rs create mode 100644 crates/editor/src/assets/thumbnails/sources/gltf.rs create mode 100644 crates/editor/src/assets/thumbnails/sources/mod.rs create mode 100644 crates/editor/src/assets/thumbnails/studio.rs create mode 100644 crates/editor/src/bin/main.rs create mode 100644 crates/editor/src/ext/brp.rs create mode 100644 crates/editor/src/ext/command_queue.rs create mode 100644 crates/editor/src/ext/extensibility.rs create mode 100644 crates/editor/src/ext/game_inspector.rs create mode 100644 crates/editor/src/ext/game_panels.rs create mode 100644 crates/editor/src/ext/hot_reload.rs create mode 100644 crates/editor/src/ext/mod.rs create mode 100644 crates/editor/src/history/commands.rs create mode 100644 crates/editor/src/history/mod.rs create mode 100644 crates/editor/src/infra.rs create mode 100644 crates/editor/src/lib.rs create mode 100644 crates/editor/src/play/mod.rs create mode 100644 crates/editor/src/play/net_editor.rs create mode 100644 crates/editor/src/play/session.rs create mode 100644 crates/editor/src/play/state.rs create mode 100644 crates/editor/src/project/mod.rs create mode 100644 crates/editor/src/project/project_io.rs create mode 100644 crates/editor/src/project/settings_ui.rs create mode 100644 crates/editor/src/project/workspace.rs create mode 100644 crates/editor/src/render_target.rs create mode 100644 crates/editor/src/scene/mod.rs create mode 100644 crates/editor/src/scene/scene_io.rs create mode 100644 crates/editor/src/scene/scene_schema.rs create mode 100644 crates/editor/src/scene/scene_view.rs create mode 100644 crates/editor/src/ui/actor_inspector/mod.rs create mode 100644 crates/editor/src/ui/actor_inspector/transform.rs create mode 100644 crates/editor/src/ui/asset_browser/mod.rs create mode 100644 crates/editor/src/ui/asset_browser/panel.rs create mode 100644 crates/editor/src/ui/asset_browser/state.rs create mode 100644 crates/editor/src/ui/component_registry.rs create mode 100644 crates/editor/src/ui/diagnostics.rs create mode 100644 crates/editor/src/ui/dock_tabs.rs create mode 100644 crates/editor/src/ui/fonts.rs create mode 100644 crates/editor/src/ui/helpers.rs create mode 100644 crates/editor/src/ui/hierarchy.rs create mode 100644 crates/editor/src/ui/hierarchy_ops.rs create mode 100644 crates/editor/src/ui/hierarchy_state.rs create mode 100644 crates/editor/src/ui/inspector.rs create mode 100644 crates/editor/src/ui/layout.rs create mode 100644 crates/editor/src/ui/menu.rs create mode 100644 crates/editor/src/ui/mod.rs create mode 100644 crates/editor/src/ui/play_controls.rs create mode 100644 crates/editor/src/ui/post_process_volume_ui.rs create mode 100644 crates/editor/src/ui/reflect_inspector.rs create mode 100644 crates/editor/src/ui/selection_ops.rs create mode 100644 crates/editor/src/ui/status_bar.rs create mode 100644 crates/editor/src/ui/theme.rs create mode 100644 crates/editor/src/ui/toolbar.rs create mode 100644 crates/editor/src/ui/viewport_chrome.rs create mode 100644 crates/editor/src/ui/widgets.rs create mode 100644 crates/editor/src/viewport/actor_icons.rs create mode 100644 crates/editor/src/viewport/camera.rs create mode 100644 crates/editor/src/viewport/gizmos.rs create mode 100644 crates/editor/src/viewport/mod.rs create mode 100644 crates/editor/src/viewport/panel.rs create mode 100644 crates/editor/src/viewport/render_view.rs create mode 100644 crates/editor/src/viewport/rendering_diagnostics.rs create mode 100644 crates/editor/src/viewport/selection.rs create mode 100644 crates/editor/src/viewport/selection_outline.rs create mode 100644 crates/editor/src/viewport/viewport_mode.rs create mode 100644 crates/editor/src/viewport/visualizers.rs create mode 100644 crates/game/Cargo.toml create mode 100644 crates/game/src/editor_ext.rs create mode 100644 crates/game/src/launch.rs create mode 100644 crates/game/src/lib.rs create mode 100644 crates/game/src/main.rs create mode 100644 crates/game_hot/Cargo.toml create mode 100644 crates/game_hot/src/lib.rs create mode 100644 crates/game_hot/src/player/camera.rs create mode 100644 crates/game_hot/src/player/input.rs create mode 100644 crates/game_hot/src/player/mod.rs create mode 100644 crates/game_hot/src/player/spawn.rs create mode 100644 crates/game_hot/src/rendering/auto_exposure.rs create mode 100644 crates/game_hot/src/rendering/camera_fx.rs create mode 100644 crates/game_hot/src/rendering/fullscreen_effects.rs create mode 100644 crates/game_hot/src/rendering/mod.rs create mode 100644 crates/game_hot/src/rendering/solari.rs create mode 100644 crates/game_hot/src/rendering/viewport_camera.rs create mode 100644 crates/game_hot/src/rendering/volumes.rs create mode 100644 crates/game_hot/src/sim_systems.rs create mode 100644 crates/game_hot/src/world/level.rs create mode 100644 crates/game_hot/src/world/lighting.rs create mode 100644 crates/game_hot/src/world/mod.rs create mode 100644 crates/protocol/Cargo.toml create mode 100644 crates/protocol/src/lib.rs create mode 100644 crates/scene/Cargo.toml create mode 100644 crates/scene/src/lib.rs create mode 100644 crates/scene/src/migrate.rs create mode 100644 crates/settings/Cargo.toml create mode 100644 crates/settings/src/lib.rs create mode 100644 crates/settings/src/plugin.rs create mode 100644 crates/settings/src/rendering.rs create mode 100644 crates/shared/Cargo.toml create mode 100644 crates/shared/src/actor.rs create mode 100644 crates/shared/src/components.rs create mode 100644 crates/shared/src/hydration/lights.rs create mode 100644 crates/shared/src/hydration/materials.rs create mode 100644 crates/shared/src/hydration/mod.rs create mode 100644 crates/shared/src/hydration/models.rs create mode 100644 crates/shared/src/hydration/physics.rs create mode 100644 crates/shared/src/hydration/prefabs.rs create mode 100644 crates/shared/src/hydration/primitives.rs create mode 100644 crates/shared/src/hydration/static_meshes.rs create mode 100644 crates/shared/src/hydration/strip.rs create mode 100644 crates/shared/src/hydration/visibility.rs create mode 100644 crates/shared/src/lib.rs create mode 100644 crates/shared/src/material_asset.rs create mode 100644 crates/shared/src/post_process_effect_asset.rs create mode 100644 crates/shared/src/rendering_profile_asset.rs create mode 100644 crates/sim/Cargo.toml create mode 100644 crates/sim/src/lib.rs create mode 100644 docs/README.md create mode 100644 docs/adr/0001-roadmap-architecture.md create mode 100644 docs/adr/0002-bevy-version-and-migration-policy.md create mode 100644 docs/adr/0003-editor-framework-mission.md create mode 100644 docs/adr/0004-editor-hot-reload.md create mode 100644 docs/adr/0005-prefab-instance-and-asset-registry.md create mode 100644 docs/adr/0006-scene-schema-versioning.md create mode 100644 docs/adr/0007-editor-plugin-extensibility.md create mode 100644 docs/adr/0008-editor-module-boundaries.md create mode 100644 docs/adr/0009-authoring-vs-hydrated.md create mode 100644 docs/adr/0010-actor-kind-and-inspector.md create mode 100644 docs/adr/0011-scene-sun-policy.md create mode 100644 docs/adr/0012-zero-tech-debt-editor.md create mode 100644 docs/adr/0013-rendering-tiers-and-post-process-volumes.md create mode 100644 docs/adr/0014-unified-viewport-model.md create mode 100644 docs/adr/0015-viewport-camera-stack-ownership.md create mode 100644 docs/adr/0016-unified-rendering-contract.md create mode 100644 docs/adr/0017-normalized-static-mesh-assets.md create mode 100644 docs/adr/0018-componentized-actor-inspector-and-materials.md create mode 100644 docs/editor/Bevy_Editor_Component_System_Spec.md create mode 100644 docs/editor/README.md create mode 100644 docs/editor/architecture.md create mode 100644 docs/editor/brp.md create mode 100644 docs/editor/debt-audit.md create mode 100644 docs/editor/evaluations/h1-signoff.md create mode 100644 docs/editor/evaluations/h2-signoff.md create mode 100644 docs/editor/evaluations/h3-signoff.md create mode 100644 docs/editor/evaluations/h4-signoff.md create mode 100644 docs/editor/evaluations/h5-signoff.md create mode 100644 docs/editor/evaluations/h6-signoff.md create mode 100644 docs/editor/release-notes.md create mode 100644 docs/editor/rendering.md create mode 100644 docs/editor/roadmap.md create mode 100644 docs/mission.md create mode 100644 rust-toolchain.toml create mode 100644 source_assets/blender/.gitkeep create mode 100644 source_assets/references/.gitkeep create mode 100644 source_assets/textures/.gitkeep create mode 100644 source_assets/vendor/.gitkeep create mode 100644 third_party/transform-gizmo-bevy/.cargo-ok create mode 100644 third_party/transform-gizmo-bevy/.cargo_vcs_info.json create mode 100644 third_party/transform-gizmo-bevy/Cargo.lock create mode 100644 third_party/transform-gizmo-bevy/Cargo.toml create mode 100644 third_party/transform-gizmo-bevy/Cargo.toml.orig create mode 100644 third_party/transform-gizmo-bevy/PATCH.md create mode 100644 third_party/transform-gizmo-bevy/README.md create mode 100644 third_party/transform-gizmo-bevy/examples/bevy_minimal.rs create mode 100644 third_party/transform-gizmo-bevy/examples/ui_blocked_gizmo.rs create mode 100644 third_party/transform-gizmo-bevy/src/gizmo.wgsl create mode 100644 third_party/transform-gizmo-bevy/src/lib.rs create mode 100644 third_party/transform-gizmo-bevy/src/mouse_interact.rs create mode 100644 third_party/transform-gizmo-bevy/src/picking.rs create mode 100644 third_party/transform-gizmo-bevy/src/prelude.rs create mode 100644 third_party/transform-gizmo-bevy/src/render.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/clean_target.rs create mode 100644 xtask/src/validate_levels.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a4367a8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,8 @@ +# Use clang + the mold linker to dramatically cut link times during iteration. +# Both `clang` and `mold` are available on this machine. +[target.x86_64-unknown-linux-gnu] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[alias] +clean-target = "run -p xtask --bin clean-target --" diff --git a/.cursor/plans/component_system_refactor_2026-06-05.plan.md b/.cursor/plans/component_system_refactor_2026-06-05.plan.md new file mode 100644 index 0000000..d6e9f3f --- /dev/null +++ b/.cursor/plans/component_system_refactor_2026-06-05.plan.md @@ -0,0 +1,23 @@ +# Component System Refactor Plan - 2026-06-05 + +## Goal + +Align the actor inspector and static mesh authoring model with +`docs/editor/Bevy_Editor_Component_System_Spec.md`. + +## Implemented Scope + +- Stable actor and component item IDs: `ActorId`, `ActorName`, `ComponentInstanceId`. +- Editor-visible component registry powering Add Component metadata. +- `StaticMeshRenderer` slots using imported `EditorAssetRef` mesh/material refs instead of source model paths. +- Static mesh artifact part IDs and resolver fallback for older generated artifacts. +- Separate `RigidBodyDesc` and `ColliderDesc`, including static mesh collider sources. +- Shader-aware `MaterialDesc`, shader schema assets, and per-actor `MaterialOverride`. +- Actor inspector footer and component cards updated for static mesh slots, collider, rigid body, and shader controls. +- Docs/ADR updates for the architecture and workflow changes. + +## Follow-Up + +- Content-browser material asset inspector should become the shared-material editing surface. +- Custom WGSL material rendering should be promoted from schema/data support to a dedicated runtime material pipeline. +- Multi-selection mixed-value editing and prefab override indicators should use the new stable component/slot IDs. diff --git a/.cursor/plans/project_roadmap_8a452d43.plan.md b/.cursor/plans/project_roadmap_8a452d43.plan.md new file mode 100644 index 0000000..95444b0 --- /dev/null +++ b/.cursor/plans/project_roadmap_8a452d43.plan.md @@ -0,0 +1,215 @@ +--- +name: Project roadmap +overview: A long-term, milestone-based roadmap ("plan of plans") for evolving the current game/shared/editor workspace into a flagship multiplayer FPS that doubles as a reusable, modular Bevy engine/editor toolkit, architected for server-authoritative networking and eventual web support, with each milestone spawning its own detailed sub-plan. +todos: + - id: m0-foundations + content: "M0 Foundations: fix window visibility, CI (fmt/clippy/build), test scaffolding, ADRs, documented Bevy version/migration policy." + status: completed + - id: m1-editor + content: "M1 Editor usability (contained sub-plan, see M1 detail section). Exit: an actually usable level editor." + status: completed + - id: m1-infra-tagging + content: M1 - Add EditorOnly marker (crates/editor/src/infra.rs); tag editor + egui cameras and helpers; exclude from hierarchy, picking, and scene save. + status: completed + - id: m1-scene-hierarchy + content: M1 - Custom scene Hierarchy panel in ui/mod.rs listing only LevelObject root+children, with select, F2 rename, and right-click context menu. + status: completed + - id: m1-selection-fixes + content: "M1 - Fix selection.rs: pick only LevelObjects, editor infra non-pickable, click-empty/Esc to deselect, keep gizmo-focus gating." + status: completed + - id: m1-delete-duplicate + content: M1 - Delete/Backspace and Ctrl+D duplicate for the current selection. + status: completed + - id: m1-menu-toolbar + content: M1 - Add top menu bar (File/Edit/View/Play), revamp toolbar, gizmo hotkeys W/E/R + world/local toggle. + status: completed + - id: m1-undo-redo + content: M1 - Add history.rs EditorHistory command stack (Spawn/Despawn/Duplicate/SetTransform/Rename/SetMaterial) wired to ops and gizmo drag-end; Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y. + status: completed + - id: m1-shared-authoring-ext + content: "M1 - Extend shared components+hydration: ModelRef (GLTF), MaterialDesc texture refs, PrefabRef; register new types." + status: completed + - id: m1-asset-browser + content: M1 - Add assets.rs + Asset Browser panel scanning assets/{models,textures,materials,levels} with primitives/lights palette and refresh. + status: completed + - id: m1-drag-drop-materials + content: M1 - Drag-and-drop from browser into viewport with ground-raycast placement; material/texture assignment + basic material editor in Inspector. + status: completed + - id: m1-import-export + content: M1 - rfd import (copy into assets/ + hot reload) and export selection as prefab .scn.ron. + status: completed + - id: m1-play-mode + content: "M1 - Snapshot/restore Play mode in state.rs: snapshot LevelObjects, activate player+FX camera on Play, restore authored scene on Stop." + status: completed + - id: m1-scene-io-polish + content: M1 - New/Open/Save/Save As via rfd with dirty tracking and window-title update in scene_io.rs. + status: completed + - id: m1-deps-verify-docs + content: M1 - Add rfd dep, verify GLTF/texture features, cargo check/clippy workspace, update README controls + checklist. + status: completed + - id: m2-sim-determinism + content: "M2 Sim/authoring split + determinism: extract crates/sim and crates/protocol, move gameplay to FixedUpdate, input-as-intent, determinism test harness." + status: completed + - id: m3-netcode + content: "M3 Networking slice: Lightyear spike + integration, headless crates/server, client prediction/reconciliation + remote interpolation for player movement." + status: pending + - id: m4-fps-core + content: "M4 FPS gameplay core: weapons (hitscan/projectile) with lag compensation, health/damage/respawn, a basic game mode in crates/sim." + status: pending + - id: m5-asset-pipeline + content: "M5 Asset/content pipeline: robust GLTF/material/prefab pipeline, scene schema versioning + migrations, level streaming, import validation." + status: pending + - id: m6-modding-scripting + content: "M6 Modding/scripting: stable plugin API, data-driven definitions, scripting layer (Lua/Rhai or WASM components), BRP external tooling." + status: pending + - id: m7-web-platform + content: "M7 Web/platform: WASM client (WebGPU + WebTransport), build matrix, networked asset loading, perf/input adaptation." + status: pending + - id: m8-production-polish + content: "M8 Production polish: perf budgets/profiling, metrics, settings/save, accessibility, packaging/distribution and update cadence." + status: pending +isProject: false +--- + +## Project Roadmap (Plan of Plans) + +This is the top-level strategy. Each milestone (M0-M8) becomes its own detailed sub-plan when started. Milestone M1 (Editor Build-Out) is fully contained inline in the "M1 detail" section below; later milestones get their detailed plans spun out as they begin. + +### Vision +A flagship, server-authoritative multiplayer FPS built on Bevy, whose engine/editor systems are factored as a reusable, modular toolkit. Desktop-first (Linux/Windows), architected so a WASM/WebGPU client is a later additive step, not a rewrite. + +### Guiding principles (apply from now, retrofit is expensive) +- Deterministic, fixed-timestep simulation. Gameplay logic lives in `FixedUpdate`/`FixedMain`, decoupled from render framerate. +- Input as intent. All world mutation flows through input/command/message events (never ad-hoc mutation from raw input). This is the single highest-leverage thing for cheap multiplayer retrofit and replays. +- Reflection-first, data-driven content. Authoring components + scenes (already in `crates/shared`), with a versioned, migratable schema. +- Strict crate boundaries: reusable engine/toolkit code separate from game-specific content; deterministic sim separate from rendering. +- WASM-friendly core. Native-only deps (rfd, mold, OS dialogs, BRP HTTP) are quarantined behind features or in tooling/editor crates only. +- BRP-aligned tooling so the custom editor can interoperate with the future official Bevy editor. +- Continuous quality: CI, lints, determinism tests, benchmarks, and a per-release Bevy migration discipline. + +### Target architecture (north star) +Evolve the current `crates/{game,shared,editor}` into: + +```mermaid +flowchart TD + subgraph core [Shared deterministic core - wasm-safe] + protocol[protocol: net types, inputs, messages, replication] + sim[sim: deterministic gameplay in FixedUpdate] + scene[scene: authoring data + hydration] + end + server[server: headless authoritative bin] --> sim + server --> protocol + client[game: render, input, audio, ui, glue] --> sim + client --> protocol + client --> scene + editor[editor: egui tooling + BRP] --> scene + editor --> client + netcode[Lightyear] --- server + netcode --- client + xtask[xtask: build, codegen, CI] -.-> core +``` + +- `crates/protocol` (new): networked types, input commands, messages, replication registration. Shared by client + server. +- `crates/sim` (extracted from `crates/game`): deterministic gameplay (movement, combat, rules), no rendering/audio. +- `crates/game` -> client app: rendering/FX (current `rendering/`), input, audio, UI, presentation glue. +- `crates/server` (new): headless authoritative binary. +- `crates/shared` -> `crates/scene`: authoring components + hydration (current files), now also used to seed networked world. +- `crates/editor`: stays the egui toolkit; native-only deps isolated here. +- `xtask` (new): build/codegen/CI helper. + +### Milestone roadmap + +```mermaid +flowchart LR + M0[M0 Foundations] --> M1[M1 Editor usability] + M1 --> M2[M2 Sim split + determinism] + M2 --> M3[M3 Netcode slice] + M3 --> M4[M4 FPS gameplay] + M4 --> M5[M5 Asset pipeline] + M5 --> M6[M6 Modding + scripting] + M6 --> M7[M7 Web + platform] + M7 --> M8[M8 Production polish] +``` + +- M0 - Foundations and hardening: resolve the window-visibility issue, quiet/understand wgpu validation noise, add CI (fmt/clippy/build), test scaffolding, ADRs (architecture decision records), and a documented Bevy version policy. Exit: green CI, reproducible builds, ADR-0001 (architecture) + ADR-0002 (netcode choice spike planned). +- M1 - Editor usability: the existing Editor Build-Out sub-plan (selection/hierarchy fixes, delete/duplicate, undo/redo, asset browser, drag-and-drop, materials/GLTF, save/load, snapshot Play mode). Exit: an actually usable level editor. +- M2 - Simulation/authoring split + determinism: extract `crates/sim`, move gameplay to `FixedUpdate`, introduce `crates/protocol`, convert input to intent/command events, add a determinism test harness (same inputs -> same state hash). Exit: gameplay deterministic in fixed timestep; no ad-hoc world mutation from input. +- M3 - Networking vertical slice: spike then integrate Lightyear (server-authoritative) with client-side prediction + reconciliation for player movement and snapshot interpolation for remotes; stand up `crates/server` (headless). Exit: server + 2 clients, responsive predicted local movement, smooth remote players. +- M4 - FPS gameplay core: weapons (hitscan + projectiles) with lag compensation, health/damage/respawn, a basic game mode, all in `crates/sim`. Exit: playable networked skirmish. +- M5 - Asset and content pipeline maturity: robust GLTF/material/prefab pipeline, scene schema versioning + migrations, level streaming/partitioning, import validation tooling. Exit: author a real level end-to-end from assets in the editor. +- M6 - Modding and scripting extensibility: stable plugin API, data-driven definitions, and a scripting/modding layer (Lua/Rhai or WASM components) plus BRP-based external tooling. Exit: a mod adds a new entity/weapon without recompiling core. +- M7 - Web and platform expansion: WASM client (WebGPU + WebTransport via Lightyear), build matrix, networked asset loading, perf/input adaptation. Exit: a browser client connects to the live server. +- M8 - Production polish and release engineering: performance budgets + profiling, metrics/observability, settings/save systems, accessibility, packaging/distribution and update cadence. Exit: a shippable demo build. + +### M1 detail - Editor Build-Out (contained sub-plan) +Phased; each phase is independently testable and leaves the editor working. Maps directly to the reported issues (cluttered hierarchy, broken selection, no delete, button-spawning, weak save, no undo, no asset browser, no real play mode). + +Architectural decisions: +- Add an `EditorOnly` marker (new `crates/editor/src/infra.rs`) for editor infrastructure (editor camera, egui camera, gizmo/selection helpers). Hierarchy, picking, and scene save all filter off it. +- Replace `bevy-inspector-egui`'s `hierarchy_ui` with a custom scene hierarchy that only walks `LevelObject` roots + children. +- Keep authoring data the source of truth: extend `crates/shared/src/components.rs` so GLTF models and textured materials are reflectable/serializable and round-trip through `.scn.ron` via the existing hydration system. +- Undo/redo via an explicit command stack (`EditorHistory`), except Play mode which uses a scene snapshot for restore. +- Add `rfd` (editor-only dep) for native Open/Save/Import dialogs. + +```mermaid +flowchart LR + Browser[Asset Browser] -->|drag payload| Viewport + Viewport -->|ground raycast place| Spawn[Spawn authoring entity] + Spawn --> History[EditorHistory cmd] + Hierarchy -->|select| Selection + Selection --> Gizmo[Transform gizmo] + Gizmo -->|drag end| History + History -->|undo or redo| World +``` + +M1 Phase 1 - Core usability (the reported bugs): +- New `infra.rs`: `EditorOnly` marker; mark editor camera + egui camera (`crates/editor/src/camera.rs`) and helper entities. +- Custom Hierarchy panel in `crates/editor/src/ui/mod.rs`: only `LevelObject` entities (root + children), click to select, `F2` rename, right-click context menu (Rename / Duplicate / Delete / Focus). +- Selection fixes in `crates/editor/src/selection.rs`: only `LevelObject`s pickable (editor infra opts out via `Pickable::IGNORE`), click empty to deselect, `Esc` to clear, keep gizmo-focus gating. +- Delete (`Delete`/`Backspace`) and Duplicate (`Ctrl+D`). +- Gizmo hotkeys in `crates/editor/src/gizmos.rs` (`W/E/R`) + world/local toggle. + +M1 Phase 2 - Undo / Redo: +- New `crates/editor/src/history.rs`: `EditorHistory` with undo/redo stacks and an `EditorCommand` enum (`Spawn`, `Despawn`, `Duplicate`, `SetTransform`, `Rename`, `SetMaterial`). +- Capture gizmo transform edits on drag-end; structural ops push commands; `Ctrl+Z` / `Ctrl+Shift+Z` (+ `Ctrl+Y`). +- Inspector field-edit undo is out of scope for v1 (README follow-up). + +M1 Phase 3 - Asset browser, authoring extensions, drag-and-drop, materials: +- Extend `crates/shared/src/components.rs` + `hydration.rs`: `ModelRef { path, scene_index }` -> `SceneRoot`/`Mesh3d` from GLTF; `MaterialDesc` optional texture paths (base color / normal / metallic-roughness); `PrefabRef` for dragging in saved `.scn.ron`. +- New `crates/editor/src/assets.rs` + Asset Browser panel: scan `assets/{models,textures,materials,levels}`, grid/tree with labels (thumbnails best-effort), refresh, primitives+lights palette. +- Drag-and-drop: egui drag payload from browser -> drop in Viewport -> place via ground raycast -> spawn matching authoring entity (push to history). Keep click-to-spawn fallback. +- Material assignment from the browser to the selection; basic material editor fields in the Inspector. +- Import via `rfd`: copy external GLTF/GLB/texture into `assets/` and hot-reload; export selection as a prefab `.scn.ron`. + +M1 Phase 4 - Working Play mode + scene IO polish: +- `crates/editor/src/state.rs`: on Edit->Play, snapshot `LevelObject`s to an in-memory `DynamicScene`; activate player + high-fidelity camera, enable game input/physics; on Play->Edit, despawn runtime entities and restore the snapshot. Optional pause/step. +- `crates/editor/src/scene_io.rs`: `New`/`Open`/`Save`/`Save As` with `rfd`, dirty tracking, recent path, window-title update; keep filtering to `LevelObject` + new authoring components. + +M1 Phase 5 - Dependencies, verification, docs: +- Add `rfd` to `crates/editor/Cargo.toml`; confirm `bevy` GLTF/texture features (default). +- `cargo check`/`cargo clippy` across the workspace; fix 0.18 API drift. +- Update `README.md` controls + Implementation Checklist. + +M1 out of scope (later): per-field inspector undo, multi-scene tabs, full PBR material graph, terrain, lightmap baking. (These align with M5/M6 of this roadmap.) + +### Cross-cutting tracks (run continuously, not milestones) +- Rendering fidelity (build on current `crates/game/src/rendering`), UX/UI polish, Tooling/CI, Docs + ADRs, Performance budgets, Security (server authority + anti-cheat posture). + +### Key technology decisions (validated May 2026) +- Bevy: stay on the latest stable (0.18.1 now; adopt 0.19 when it ships, imminent). Migrate every ~3-month release on a scheduled cadence; gate upgrades on ecosystem (esp. netcode) support. Capture as ADR. +- Netcode: Lightyear (server-authoritative; prediction/rollback, interpolation, lag compensation; WASM/WebTransport). Lightyear tracks Bevy by roughly one version, which constrains upgrade timing. Fallback: bevy_replicon if Lightyear complexity outweighs benefit. Decide via M3 spike. +- Physics: keep Avian for static world + spatial queries; keep the player controller custom and deterministic; movement is server-authoritative. Avoid full physics rollback initially (predict player movement only); revisit if needed. Determinism across machines is the top technical risk. +- Editor: continue the custom egui editor; align scene IO and inspection with BRP so we can interoperate with the official Bevy editor later. Quarantine native-only editor deps. +- WASM readiness: keep `protocol`/`sim`/`scene`/`game` wasm-compatible; isolate native-only code behind features/editor. + +### Risks and mitigations +- Cross-platform determinism (floats/physics): fixed timestep + deterministic controller + server authority + state-hash tests; constrain prediction scope. +- Bevy churn (every ~3 months): migration checklist, pinned versions, upgrades gated on Lightyear/ecosystem readiness. +- Scope (game + toolkit + MP): enforce milestone gating and vertical slices; resist premature generality until M5/M6. +- Toolkit/game coupling: keep game content out of reusable crates; reusable code must not depend on `crates/game`. + +### How the plan-of-plans operates +This roadmap is the index, and M1's full detail is contained inline above. Starting a later milestone expands its detailed scope/files/todos the same way (either inline or as a spun-out plan). M0 and M2 are the immediate next planning targets; M3 begins with a time-boxed Lightyear spike before committing. + +### Out of scope for now +- Concrete release dates/staffing (cadence assumptions only), console ports, peer-to-peer topology, and full anti-cheat beyond server authority. \ No newline at end of file diff --git a/.cursor/plans/rendering_unification_2026-06-05.plan.md b/.cursor/plans/rendering_unification_2026-06-05.plan.md new file mode 100644 index 0000000..2a78878 --- /dev/null +++ b/.cursor/plans/rendering_unification_2026-06-05.plan.md @@ -0,0 +1,27 @@ +# Rendering Unification Plan + +## Summary + +Unify the Bevy 0.18 rendering framework around a single effective render-stack contract. Preserve +Hybrid Auto GI: requested Solari provides realtime indirect lighting when ready, while Forward PBR +remains the correctness fallback for unsupported RT hardware, Solari warmup, and local point/spot +shadow lights. + +## Implementation + +- Add `settings::EffectiveRenderStack` and `RenderFallbackReason` as the authoritative GI fallback + decision. +- Route editor camera stack sync and diagnostics through the shared resolver. +- Track requested and effective GI separately in `RenderingCapabilities`. +- Keep requested Solari raytracing mesh tagging alive while the camera temporarily renders Forward. +- Extend material authoring with emissive color, intensity, and texture fields for Solari-compatible + emissive scene lighting. +- Add diagnostics for requested/effective GI, fallback reason, and Solari mesh eligibility counts. + +## Verification + +- `cargo fmt --check` +- `cargo check --workspace` +- `cargo clippy --workspace` +- Focused tests for settings, shared hydration, game rendering, and editor camera stack behavior. +- `cargo run -p xtask -- validate-levels` diff --git a/.cursor/plans/static_mesh_asset_refactor_2026-06-05.plan.md b/.cursor/plans/static_mesh_asset_refactor_2026-06-05.plan.md new file mode 100644 index 0000000..50d9f7b --- /dev/null +++ b/.cursor/plans/static_mesh_asset_refactor_2026-06-05.plan.md @@ -0,0 +1,26 @@ +# Static Mesh Asset Refactor Plan + +Date: 2026-06-05 + +## Scope + +- Normalize glTF/GLB and FBX model placement through one static mesh renderer authoring component. +- Generate project static mesh manifests under `assets/meshes/generated/`. +- Preserve explicit scene-instance placement for full `SceneRoot` behavior. +- Redesign the actor inspector around component cards and add renderer slot controls. + +## Implementation Slices + +1. Add `StaticMeshRenderer` shared authoring data, validation, scene save allowlist, history snapshots, and hydration. +2. Extend the asset registry import settings with placement mode, hierarchy mode, material policy, collider generation, and generated manifest path. +3. Add glTF/FBX static mesh manifest generation and route model drag/drop through renderer placement by default. +4. Expose asset import settings and static mesh renderer controls in editor UI. +5. Update ADR/docs/README and verify with focused checks. + +## Shipped Behavior + +- `StaticAsset` placement creates `ActorKind::StaticMesh + StaticMeshRenderer`. +- `SceneInstance` placement keeps `ActorKind::ImportedModel + ModelRef`. +- `SingleActor` hierarchy mode stores all mesh parts on one renderer. +- `SourceHierarchy` mode creates a root with child static mesh actors. +- Static mesh hydration spawns generated child `Mesh3d` parts with source materials, shadow flags, and optional Avian mesh collider constructors. diff --git a/.cursor/rules/documentation.mdc b/.cursor/rules/documentation.mdc new file mode 100644 index 0000000..e823627 --- /dev/null +++ b/.cursor/rules/documentation.mdc @@ -0,0 +1,42 @@ +--- +description: Keep project documentation current—ADRs, mission, editor docs, README checklist +alwaysApply: true +--- + +# Documentation Maintenance + +When changing behavior, architecture, or user-facing workflows, **update docs in the same task**—do not defer. + +## Doc map (single source of truth: [docs/README.md](../../docs/README.md)) + +| Change type | Update | +|-------------|--------| +| Architecture / crate boundary / sim-network split | New or updated ADR in `docs/adr/` | +| Editor framework intent, principles, non-goals | `docs/mission.md` | +| Editor feature design or phase status | `docs/editor/` (see its README) | +| User controls, run commands, troubleshooting | Root `README.md` | +| Shipped feature completion | Root `README.md` Implementation Checklist | +| Bevy upgrade policy | ADR 0002 + migration notes in `docs/adr/` | +| Milestone scope (pre-implementation) | `.cursor/plans/` plan file | + +## ADRs + +Create `docs/adr/NNNN-short-title.md` when a decision is **hard to reverse** or affects multiple crates (rendering path, scene format, PIE semantics, settings schema, netcode choice). + +Format: Status, Context, Decision, Consequences. Number sequentially. Link from `docs/README.md` and root README. + +## Coherence rules + +- **One home per fact.** Link across docs; do not copy paragraphs between README, mission, and ADRs. +- **Intent vs implementation:** mission/ADRs = *why*; README = *how to use*; crate module docs = *local API*. +- **Plans vs permanent docs:** `.cursor/plans/` holds working roadmaps; promote stable decisions into `docs/` when implemented. +- **Stale docs are bugs.** If code contradicts docs, fix both or fix docs in the same PR/task. +- **New subsystems** get a short entry in `docs/README.md` and, if editor-facing, `docs/editor/README.md`. + +## Agent checklist (end of relevant tasks) + +1. Did behavior or UX change? → README controls / checklist +2. Did architecture or policy change? → ADR or mission +3. Did editor workflow change? → `docs/editor/` +4. Added a new doc file? → index in `docs/README.md` +5. Left a deliberate gap? → "Future work" in README or an ADR Consequences section—not silent omission diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6f9142e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,34 @@ +* text=auto eol=lf + +*.rs text eol=lf +*.toml text eol=lf +*.ron text eol=lf +*.md text eol=lf +*.wgsl text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +*.fbx filter=lfs diff=lfs merge=lfs -text +*.glb filter=lfs diff=lfs merge=lfs -text +*.gltf filter=lfs diff=lfs merge=lfs -text +*.blend filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text +*.tga filter=lfs diff=lfs merge=lfs -text +*.exr filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.flac filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.7z filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.kra filter=lfs diff=lfs merge=lfs -text +*.clip filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ec3dc7b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +env: + CARGO_TERM_COLOR: always + WGPU_VALIDATION: "0" + +jobs: + verify: + name: Format, lint, test, build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + mold \ + pkg-config \ + libasound2-dev \ + libudev-dev \ + libwayland-dev \ + libx11-dev \ + libxkbcommon-dev \ + libxi-dev \ + libxcursor-dev \ + libxrandr-dev \ + libxinerama-dev \ + libgl1-mesa-dev \ + libvulkan-dev + + - name: Install Rust toolchain + run: rustup show + + - name: Check formatting + run: cargo fmt --check + + - name: Check workspace + run: cargo check --workspace + + - name: Clippy + run: cargo clippy --workspace + + - name: Clippy foundation crates + run: cargo clippy -p game -p shared -- -D warnings + + - name: Test foundation helpers + run: cargo test -p game --lib + + - name: Test shared hydration and actor validation + run: cargo test -p shared + + - name: Test sim crate + run: cargo test -p sim + + - name: Validate level scenes + run: cargo run -p xtask --bin validate-levels + + - name: Test scene schema crate + run: cargo test -p scene + + - name: Check editor (dev) + run: cargo check -p editor --features dev + + - name: Build binaries + run: | + cargo build -p game + cargo build -p editor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5af6dee --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Rust build output +/target/ + +# Local automation and assistant state +/.agents/ +/.codex/ + +# Local environment files +.env +.env.* +!.env.example + +# Editor/runtime generated session state +/assets/levels/.pie_session.scn.ron + +# Backup, temporary, and log files +**/*.rs.bk +Cargo.lock.bak +*.bak +*.tmp +*.temp +*.log +*.swp +*.swo +*~ +.DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5a0057c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "vadimcn.vscode-lldb", + "serayuzgur.crates", + "usernamehw.errorlens" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3ba308b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug editor", + "preLaunchTask": "build editor (dev fast-link)", + "program": "${workspaceFolder}/target/debug/editor", + "args": [], + "env": { + "BEVY_FPS_HDR": "0", + "WGPU_VALIDATION": "0", + "CARGO_TARGET_DIR": "${workspaceFolder}/target", + "LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}" + }, + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug game", + "preLaunchTask": "build game (dev fast-link)", + "program": "${workspaceFolder}/target/debug/game", + "args": [], + "env": { + "BEVY_FPS_HDR": "0", + "WGPU_VALIDATION": "0", + "CARGO_TARGET_DIR": "${workspaceFolder}/target", + "LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}" + }, + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Run editor (release)", + "cargo": { + "args": ["build", "--release", "-p", "editor"], + "filter": { "name": "editor", "kind": "bin" } + }, + "args": [], + "env": { + "BEVY_FPS_HDR": "0", + "WGPU_VALIDATION": "0", + "LD_LIBRARY_PATH": "${workspaceFolder}/target/release/deps:${env:LD_LIBRARY_PATH}" + }, + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e4b9308 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + // Use clippy for richer diagnostics on save. + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.allTargets": true, + + // Bevy/wgpu generate a lot of proc-macro code; keep these on for good IntelliSense. + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.procMacro.enable": true, + + // Surface compile errors inline as you type. + "rust-analyzer.diagnostics.enable": true, + + "editor.formatOnSave": true, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true + }, + + // Keep the large /target directory out of search and file watching. + "files.watcherExclude": { + "**/target/**": true + }, + "search.exclude": { + "**/target/**": true, + "**/Cargo.lock": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8370bc7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,185 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "cargo fmt (check)", + "type": "cargo", + "command": "fmt", + "args": ["--check"], + "problemMatcher": [], + "group": "build" + }, + { + "label": "cargo check (workspace)", + "type": "cargo", + "command": "check", + "args": ["--workspace"], + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "cargo clippy (workspace)", + "type": "cargo", + "command": "clippy", + "args": ["--workspace"], + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "cargo clippy (foundation strict)", + "type": "cargo", + "command": "clippy", + "args": ["-p", "game", "-p", "shared", "--", "-D", "warnings"], + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "cargo test (foundation)", + "type": "cargo", + "command": "test", + "args": ["-p", "game", "--lib"], + "problemMatcher": ["$rustc"], + "group": "test" + }, + { + "label": "build game (debug)", + "type": "cargo", + "command": "build", + "args": ["-p", "game"], + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "build editor (debug)", + "type": "cargo", + "command": "build", + "args": ["-p", "editor"], + "problemMatcher": ["$rustc"], + "group": { "kind": "build", "isDefault": true } + }, + { + "label": "build editor (dev fast-link)", + "type": "cargo", + "command": "build", + "args": ["-p", "editor", "--features", "dev"], + "options": { + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "build game (dev fast-link)", + "type": "cargo", + "command": "build", + "args": ["-p", "game", "--features", "dev"], + "options": { + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "clean build editor (dev)", + "type": "shell", + "command": "cargo clean -p editor && cargo build -p editor --features dev", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "target cleanup (dry run)", + "type": "shell", + "command": "cargo clean-target", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": [], + "group": "build" + }, + { + "label": "target cleanup (safe apply)", + "type": "shell", + "command": "cargo clean-target --apply", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": [], + "group": "build" + }, + { + "label": "run editor (dev fast-link)", + "type": "cargo", + "command": "run", + "args": ["-p", "editor", "--features", "dev"], + "options": { + "env": { + "BEVY_FPS_HDR": "0", + "WGPU_VALIDATION": "0", + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "run game (dev fast-link)", + "type": "cargo", + "command": "run", + "args": ["-p", "game", "--features", "dev"], + "options": { + "env": { + "BEVY_FPS_HDR": "0", + "WGPU_VALIDATION": "0", + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build" + }, + { + "label": "watch game_hot (hot reload)", + "type": "shell", + "command": "cargo watch -w crates/game_hot -w crates/sim -x \"build -p game_hot --features dylib\"", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CARGO_TARGET_DIR": "${workspaceFolder}/target" + } + }, + "problemMatcher": ["$rustc"], + "group": "build", + "isBackground": true + }, + { + "label": "verify foundation", + "dependsOrder": "sequence", + "dependsOn": [ + "cargo fmt (check)", + "cargo check (workspace)", + "cargo clippy (workspace)", + "cargo clippy (foundation strict)", + "cargo test (foundation)", + "build game (debug)", + "build editor (debug)" + ], + "problemMatcher": [], + "group": { "kind": "build", "isDefault": true } + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0881f6d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# Agent Instructions + +## Documentation Maintenance + +When changing behavior, architecture, or user-facing workflows, update documentation in the same task. Do not defer documentation updates for implemented work. + +Use `docs/README.md` as the documentation map and keep each fact in one home: + +| Change type | Update | +|-------------|--------| +| Architecture, crate boundaries, or sim/network split | New or updated ADR in `docs/adr/` | +| Editor framework intent, principles, or non-goals | `docs/mission.md` | +| Editor feature design or phase status | `docs/editor/` | +| User controls, run commands, or troubleshooting | Root `README.md` | +| Shipped feature completion | Root `README.md` implementation checklist | +| Bevy upgrade policy | ADR 0002 and migration notes in `docs/adr/` | +| Milestone scope before implementation | `.cursor/plans/` plan file | + +Create an ADR in `docs/adr/NNNN-short-title.md` when a decision is hard to reverse or affects multiple crates, such as rendering path, scene format, PIE semantics, settings schema, or netcode choice. Use this format: Status, Context, Decision, Consequences. Number ADRs sequentially and link new ADRs from `docs/README.md` and the root README when relevant. + +Coherence rules: + +- One home per fact. Link across docs; do not copy paragraphs between README, mission, and ADRs. +- Intent vs implementation: mission/ADRs explain why, README explains how to use, crate module docs explain local API. +- Plans vs permanent docs: `.cursor/plans/` holds working roadmaps; promote stable decisions into `docs/` when implemented. +- Stale docs are bugs. If code contradicts docs, fix both or fix docs in the same PR/task. +- New subsystems get a short entry in `docs/README.md` and, if editor-facing, `docs/editor/README.md`. + +End-of-task checklist for relevant implementation work: + +1. Did behavior or UX change? Update README controls or checklist. +2. Did architecture or policy change? Update or create an ADR or mission note. +3. Did an editor workflow change? Update `docs/editor/`. +4. Added a new doc file? Index it in `docs/README.md`. +5. Left a deliberate gap? Record it as future work in README or an ADR Consequences section. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..caefc33 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,8217 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "accesskit_consumer" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db81010a6895d8707f9072e6ce98070579b43b717193d2614014abd5cb17dd43" +dependencies = [ + "accesskit", + "hashbrown 0.15.5", +] + +[[package]] +name = "accesskit_macos" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0089e5c0ac0ca281e13ea374773898d9354cc28d15af9f0f7394d44a495b575" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_windows" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d63dd5041e49c363d83f5419a896ecb074d309c414036f616dc0b04faca971" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cfabe59d0eaca7412bfb1f70198dd31e3b0496fee7e15b066f9c36a1a140a0" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.1", + "cc", + "jni 0.22.4", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "simd_cesu8", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary-chunks" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad8689a486416c401ea15715a4694de30054248ec627edbf31f49cb64ee4086" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomicow" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301801c08259e328a1c7da556608c0c22687708831b22024dbd3a57ea741e6de" +dependencies = [ + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "avian3d" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0751cde05c2be789fad86cd19219daf8a8907e0c6c2175f2c8144706493f010" +dependencies = [ + "approx", + "avian_derive", + "bevy", + "bevy_heavy", + "bevy_math", + "bevy_transform_interpolation", + "bitflags 2.11.1", + "derive_more", + "disqualified", + "glam_matrix_extras", + "itertools 0.14.0", + "obvhs", + "parry3d", + "parry3d-f64", + "serde", + "smallvec", + "thiserror 2.0.18", + "thread_local", +] + +[[package]] +name = "avian_derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b257f601a1535e0d4a7a7796f535e3a13de62fd422b16dff7c14d27f0d4048" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bevy" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd310426290cec560221f9750c2f4484be4a8eeea7de3483c423329b465c40e" +dependencies = [ + "bevy_dylib", + "bevy_internal", +] + +[[package]] +name = "bevy-inspector-egui" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265c78b2d2b770351e2eb90c5bc997c7036d453a4e2cd62e066561fefeecedec" +dependencies = [ + "bevy-inspector-egui-derive", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_egui", + "bevy_gizmos", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_state", + "bevy_time", + "bevy_utils", + "bevy_window", + "bytemuck", + "disqualified", + "egui", + "fuzzy-matcher", + "image", + "opener", + "smallvec", + "uuid", + "winit", +] + +[[package]] +name = "bevy-inspector-egui-derive" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8405b6aee62eebfc27a95c513e04398869fb7911ea8266ec91675994b11eb749" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_a11y" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e887b25c84f384ffe3278a17cf0e4b405eaa3c8fbc3db24d05d560a11780676d" +dependencies = [ + "accesskit", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_reflect", + "serde", +] + +[[package]] +name = "bevy_android" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c58de772ac1148884112e8a456c4f127a94b95a0e42ab5b160b7a11895a241" +dependencies = [ + "android-activity", +] + +[[package]] +name = "bevy_animation" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5bf5b285f0d3fab983b4505e62e195e06930a29007ffc95bdabde834e163a2" +dependencies = [ + "bevy_animation_macros", + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_utils", + "blake3", + "derive_more", + "downcast-rs 2.0.2", + "either", + "petgraph", + "ron 0.12.1", + "serde", + "smallvec", + "thiserror 2.0.18", + "thread_local", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_animation_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf35516d0e7ac9ec25df533be1bf8cbaa20596a8e65f36838a3f7803a267d6d" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_anti_alias" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726cc494eb7d6a84ce6291c23636fd451fa4846604dc059fa93febca4e60a928" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_utils", + "tracing", +] + +[[package]] +name = "bevy_app" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def9f41aa5bf9b9dec8beda307a332798609cffb9d44f71005e0cfb45164f2f6" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "cfg-if", + "console_error_panic_hook", + "ctrlc", + "downcast-rs 2.0.2", + "log", + "thiserror 2.0.18", + "variadics_please", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "bevy_asset" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f86fed15972b9fb1a3f7b092cf0390e67131caaedab15a2707c043e3a3c886" +dependencies = [ + "async-broadcast", + "async-channel", + "async-fs", + "async-io", + "async-lock", + "atomicow", + "bevy_android", + "bevy_app", + "bevy_asset_macros", + "bevy_diagnostic", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.11.1", + "blake3", + "crossbeam-channel", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "either", + "futures-io", + "futures-lite", + "futures-util", + "js-sys", + "ron 0.12.1", + "serde", + "stackfuture", + "thiserror 2.0.18", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_asset_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb8d948365b06561b43b7d709282e62a6abb756baac5d8e295206d5e156168" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_audio" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d68da32468ce7f4bb2863b71326acfaaa88e9aef8da8306257cd487d40cede4" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_transform", + "coreaudio-sys", + "cpal", + "rodio", + "tracing", +] + +[[package]] +name = "bevy_camera" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ed9eed054e14341852236d06a7244597b1ace39ff9ae023fbd188ffde88619" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "downcast-rs 2.0.2", + "serde", + "smallvec", + "thiserror 2.0.18", + "wgpu-types", +] + +[[package]] +name = "bevy_color" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb41e8310a85811d14a4e75cfc2d6c07ac70661d6a4883509fc960f622970a8" +dependencies = [ + "bevy_math", + "bevy_reflect", + "bytemuck", + "derive_more", + "encase", + "serde", + "thiserror 2.0.18", + "wgpu-types", +] + +[[package]] +name = "bevy_core_pipeline" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0810e85c2436e50c67448d48a83bf0bb1b5849899619ae2c7ea817221e9172" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.11.1", + "nonmax", + "radsort", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "bevy_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318ee0532c3da93749859d18f89a889c638fbc56aabac4d866583df7b951d103" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_dev_tools" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f1464a3f5ef5c23d917987714ee89881f9f791e9ff97ecf6600ee846b9569e" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_state", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_ui", + "bevy_ui_render", + "bevy_window", + "tracing", +] + +[[package]] +name = "bevy_diagnostic" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8543a0f7afd56d3499ba80ffab6ef0bad12f93c2d2ca9aa7b1f1b8816c3980" +dependencies = [ + "atomic-waker", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_tasks", + "bevy_time", + "const-fnv1a-hash", + "log", + "serde", + "sysinfo", +] + +[[package]] +name = "bevy_dylib" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f142f72bbbc1a61338bf6da5dc7cf6dcf223683b9d53bcd635ebff4bfeb5308f" +dependencies = [ + "bevy_internal", +] + +[[package]] +name = "bevy_ecs" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cf7a3ee41342dd7b5a5d82e200d0e8efb933169247fce853b4ad633d51e87d" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.11.1", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.18", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908baf585e2ea16bd53ef0da57b69580478af0059d2dbdb4369991ac9794b618" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_egui" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0a7e4806f3f242326d2c6157531c36d710f3bf320ebc0a1678e44635ed0eac" +dependencies = [ + "arboard", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_time", + "bevy_transform", + "bevy_ui_render", + "bevy_utils", + "bevy_window", + "bevy_winit", + "bytemuck", + "crossbeam-channel", + "egui", + "encase", + "getrandom 0.3.4", + "image", + "itertools 0.14.0", + "js-sys", + "thread_local", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webbrowser", + "wgpu-types", + "winit", +] + +[[package]] +name = "bevy_encase_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fee46eeddcbc00a805ae00ffa973f224671fc5cf0fe1a796963804faeade90" +dependencies = [ + "bevy_macro_utils", + "encase_derive_impl", +] + +[[package]] +name = "bevy_feathers" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb29be8f8443c5cc44e1c4710bbe02877e73703c60228ca043f20529a5496c6" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_text", + "bevy_ui", + "bevy_ui_render", + "bevy_ui_widgets", + "bevy_window", + "smol_str", +] + +[[package]] +name = "bevy_gilrs" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611827ab0ce43b88c0a695e6603901b5f34687efecaf526c861456c9d8e6fedb" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_platform", + "bevy_time", + "gilrs", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "bevy_gizmos" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaff0dd5f405c83d290c5cd591835f1ae8009894947ab19dadcb323062bd7e7" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_gizmos_macros", + "bevy_light", + "bevy_math", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_utils", +] + +[[package]] +name = "bevy_gizmos_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6960ea308d7e94adcac5c712553ff86614bba6b663511f3f3812f6bec028b51e" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_gizmos_render" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a8d18c089102de4c5e9326023ad96ba618a6961029f8102a33640b966883237" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_gizmos", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_render", + "bevy_shader", + "bevy_sprite_render", + "bevy_transform", + "bevy_utils", + "bytemuck", + "tracing", +] + +[[package]] +name = "bevy_gltf" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f37fb52655d0439656ca0a1db027d46926e463c81d893d4b1639668e5d7f1c1" +dependencies = [ + "async-lock", + "base64 0.22.1", + "bevy_animation", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_image", + "bevy_light", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_scene", + "bevy_tasks", + "bevy_transform", + "fixedbitset", + "gltf", + "itertools 0.14.0", + "percent-encoding", + "serde", + "serde_json", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "bevy_heavy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc496d1d43b890896cf561d8ce3dcf7b7b8e4c03c4e837a49a83a530abccbc5e" +dependencies = [ + "bevy_math", + "bevy_reflect", + "glam_matrix_extras", + "serde", +] + +[[package]] +name = "bevy_image" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71daf9b2afdd032c2b1122d1d501f99126218cb3e9983b3604ec381daa35f22" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "bitflags 2.11.1", + "bytemuck", + "futures-lite", + "guillotiere", + "half", + "image", + "ktx2", + "rectangle-pack", + "ruzstd", + "serde", + "thiserror 2.0.18", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_input" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc8ffbd02df34dfc52faf420a5263985973765e228043adf542fd0d790a6b21" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "derive_more", + "log", + "serde", + "smol_str", + "thiserror 2.0.18", +] + +[[package]] +name = "bevy_input_focus" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d48a5bceccb9157549a39ab3de4017f5368b65db6471605e9a3f1c19d91bbc" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_window", + "log", + "thiserror 2.0.18", +] + +[[package]] +name = "bevy_internal" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a11df62e49897def470471551c02f13c6fb488e55dddb5ab7ef098132e07754" +dependencies = [ + "bevy_a11y", + "bevy_android", + "bevy_animation", + "bevy_anti_alias", + "bevy_app", + "bevy_asset", + "bevy_audio", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_dev_tools", + "bevy_diagnostic", + "bevy_ecs", + "bevy_feathers", + "bevy_gilrs", + "bevy_gizmos", + "bevy_gizmos_render", + "bevy_gltf", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_light", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_picking", + "bevy_platform", + "bevy_post_process", + "bevy_ptr", + "bevy_reflect", + "bevy_remote", + "bevy_render", + "bevy_scene", + "bevy_shader", + "bevy_sprite", + "bevy_sprite_render", + "bevy_state", + "bevy_tasks", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_ui", + "bevy_ui_render", + "bevy_utils", + "bevy_window", + "bevy_winit", +] + +[[package]] +name = "bevy_light" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9d2ac64390a9baacb3c0fa0f5456ac1553959d5a387874c102a09aab8b92cc" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "tracing", +] + +[[package]] +name = "bevy_log" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2aac1187f83a1ab2eae887564f7fb14b4abb3fbe8b2267a6426663463923120" +dependencies = [ + "android_log-sys", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_utils", + "tracing", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b147843b81a7ec548876ff97fa7bfdc646ef2567cb465566259237b39664438" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "bevy_math" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e931fa969f89c83498b22c97432383afe90e90fd1a5e04fa07be8da4d3bcac84" +dependencies = [ + "approx", + "arrayvec", + "bevy_reflect", + "derive_more", + "glam 0.30.10", + "itertools 0.14.0", + "libm", + "rand", + "rand_distr", + "serde", + "thiserror 2.0.18", + "variadics_please", +] + +[[package]] +name = "bevy_mesh" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288f590c8173d4cca3cae5f2ba579accd5ed1a35dd3fab338f427eb39d55f05e" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mikktspace", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bitflags 2.11.1", + "bytemuck", + "derive_more", + "hexasphere", + "serde", + "thiserror 2.0.18", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_mikktspace" +version = "0.17.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59" + +[[package]] +name = "bevy_pbr" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab6944ffc6fd71604c0fbca68cc3e2a3654edfcdbfd232f9d8b88e3d20fdc0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bitflags 2.11.1", + "bytemuck", + "derive_more", + "fixedbitset", + "nonmax", + "offset-allocator", + "smallvec", + "static_assertions", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "bevy_picking" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d524dbc8f2c9e73f7ab70c148c8f7886f3c24b8aa8c252a38ba68ed06cbf10" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_window", + "crossbeam-channel", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_platform" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6b36504169b644acd26a5469fd8d371aa6f1d73ee5c01b1b1181ae1cefbf9b" +dependencies = [ + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "bevy_post_process" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f77a4e894aea992e3d6938f1d5898a1cdbb87dba6eebfb95cb4038d0a2600e9" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.11.1", + "nonmax", + "radsort", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "bevy_ptr" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a9329e8dc4e01ced480eeec4902e6d7cb56e56ec37f6fbc4323e5c937290a7" + +[[package]] +name = "bevy_reflect" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1dfeb67a9fe4f59003a84f5f99ba6302141c70e926601cbc6abfd4a1eea9ca9" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "erased-serde", + "foldhash 0.2.0", + "glam 0.30.10", + "indexmap", + "inventory", + "petgraph", + "serde", + "smallvec", + "smol_str", + "thiserror 2.0.18", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475f68c93e9cd5f17e9167635c8533a4f388f12d38245a202359e4c2721d87ba" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_remote" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0129e24bf3e281dd52996a9290c35f8c4821ca09e1ce8b8b222671e1ad9da0c" +dependencies = [ + "anyhow", + "async-channel", + "async-io", + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_log", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "http-body-util", + "hyper", + "serde", + "serde_json", + "smol-hyper", +] + +[[package]] +name = "bevy_render" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243523e33fe5dfcebc4240b1eb2fc16e855c5d4c0ea6a8393910740956770f44" +dependencies = [ + "async-channel", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_encase_derive", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render_macros", + "bevy_shader", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.11.1", + "bytemuck", + "derive_more", + "downcast-rs 2.0.2", + "encase", + "fixedbitset", + "glam 0.30.10", + "image", + "indexmap", + "js-sys", + "naga", + "nonmax", + "offset-allocator", + "send_wrapper", + "smallvec", + "thiserror 2.0.18", + "tracing", + "variadics_please", + "wasm-bindgen", + "web-sys", + "wgpu", +] + +[[package]] +name = "bevy_render_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b6325e9c495a71270446784611e8d7f446f927eac8506c4c099fd10cb4c3ed" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_scene" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cc1047d85ec8048261b63ef675c12f1e6b5782dc0b422fbcee0c140d026bd4" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "derive_more", + "ron 0.12.1", + "serde", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "bevy_shader" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eea95f0273c32be13d6a0b799a93bc256ad7830759ede595c404d5234302da2" +dependencies = [ + "bevy_asset", + "bevy_platform", + "bevy_reflect", + "naga", + "naga_oil", + "serde", + "thiserror 2.0.18", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_solari" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "304f48da01c8f245da7ff73c404fd30883831788c958ea6531881cf0e72ec97e" +dependencies = [ + "bevy_anti_alias", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bytemuck", + "derive_more", + "tracing", +] + +[[package]] +name = "bevy_sprite" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ec5bc0cbdee551b610a46f41d30374bbe42b8951ffc676253c6243ab2b9395" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_picking", + "bevy_reflect", + "bevy_text", + "bevy_transform", + "bevy_window", + "radsort", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_sprite_render" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82cb08905e7ddcea2694a95f757ae7f1fd01e6a7304076bad595d2158e4bfe0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bitflags 2.11.1", + "bytemuck", + "derive_more", + "fixedbitset", + "nonmax", + "tracing", +] + +[[package]] +name = "bevy_state" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae0682968e97d29c1eccc8c6bb6283f2678d362779bc03f1bb990967059473b" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_state_macros", + "bevy_utils", + "log", + "variadics_please", +] + +[[package]] +name = "bevy_state_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73d32f90f9cfcef5a44401db7ce206770daaa1707b0fb95eb7a96a6933f54f1b" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_tasks" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384eb04d80aa38664d69988fd30cbbe03e937ecb65c66aa6abe60ce0bca826aa" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-task", + "atomic-waker", + "bevy_platform", + "concurrent-queue", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless 0.9.3", + "pin-project", +] + +[[package]] +name = "bevy_text" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc5233291dfc22e584de2535f2e37ae9766d37cb5a01652de2133ba202dcb9b" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "cosmic-text", + "serde", + "smallvec", + "sys-locale", + "thiserror 2.0.18", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_time" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ef9af4e523195e561074cf60fbfad0f4cb8d1db504855fee3c4ce8896c7244" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "crossbeam-channel", + "log", + "serde", +] + +[[package]] +name = "bevy_transform" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3bb3de7842fef699344beb03f22bdbff16599d788fe0f47fbb3b1e6fa320eb" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_log", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "derive_more", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "bevy_transform_interpolation" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88545c8b0fa8f3502b9a439c71fa6b596ee9e808bfb16f27a51c8c6f7405a657" +dependencies = [ + "bevy", + "serde", +] + +[[package]] +name = "bevy_ufbx" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31af4df234bae92d017442aa63e5d20e18e53d04c3c149180752569818fc7e" +dependencies = [ + "bevy", + "serde", + "thiserror 2.0.18", + "ufbx", +] + +[[package]] +name = "bevy_ui" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1691a411014085e0d35f8bb8208e5f973edd7ace061a4b1c41c83de21579dc70" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_math", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "serde", + "smallvec", + "taffy", + "thiserror 2.0.18", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_ui_render" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c35402d8a052f512e3fec1f36b26e83eee713fcca57f965c244ee795e1fcb0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_sprite_render", + "bevy_text", + "bevy_transform", + "bevy_ui", + "bevy_utils", + "bytemuck", + "derive_more", + "tracing", +] + +[[package]] +name = "bevy_ui_widgets" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a63cb818b0de41bdb14990e0ce1aaaa347f871750ab280f80c427e83d72712" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_ui", +] + +[[package]] +name = "bevy_utils" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2111910cd7a4b1e6ce07eaaeb6f68a2c0ea0ca609ed0d0d506e3eb161101435b" +dependencies = [ + "bevy_platform", + "disqualified", + "thread_local", +] + +[[package]] +name = "bevy_window" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df06e6993a0896bae2fe7644ae6def29a1a92b45dfb1bcebbd92af782be3638" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "log", + "raw-window-handle", + "serde", +] + +[[package]] +name = "bevy_winit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2de1c13d32ab8528435b58eca7ab874a1068184c6d6f266ee11433ae99d4069" +dependencies = [ + "accesskit", + "accesskit_winit", + "approx", + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_window", + "bytemuck", + "cfg-if", + "js-sys", + "tracing", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winit", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex 1.3.0", + "syn", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "bytemuck", + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-pseudorand" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2097358495d244a0643746f4d13eedba4608137008cf9dec54e53a3b700115a6" +dependencies = [ + "chiapos-chacha8", + "nanorand", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chiapos-chacha8" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f8be573a85f6c2bc1b8e43834c07e32f95e489b914bf856c0549c3c269cd0a" +dependencies = [ + "rayon", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "constgebra" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aaf9b65849a68662ac6c0810c8893a765c960b907dd7cfab9c4a50bf764fbc" +dependencies = [ + "const_soft_float", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cosmic-text" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cadaea21e24c49c0c82116f2b465ae6a49d63c90e428b0f8d9ae1f638ac91f" +dependencies = [ + "bitflags 2.11.1", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 1.1.0", + "self_cell", + "skrifa 0.39.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "duplicate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" +dependencies = [ + "heck", + "proc-macro2", + "proc-macro2-diagnostics", +] + +[[package]] +name = "ecolor" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" +dependencies = [ + "bytemuck", + "emath 0.33.3", + "serde", +] + +[[package]] +name = "ecolor" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e" +dependencies = [ + "emath 0.34.3", +] + +[[package]] +name = "editor" +version = "0.1.0" +dependencies = [ + "avian3d", + "bevy", + "bevy-inspector-egui", + "bevy_egui", + "bevy_solari", + "bevy_ufbx", + "egui_dock", + "egui_phosphor_icons", + "game", + "game_hot", + "gltf", + "hot-lib-reloader", + "notify", + "protocol", + "rfd", + "ron 0.8.1", + "scene", + "serde", + "settings", + "shared", + "sim", + "transform-gizmo-bevy", + "ufbx", + "uuid", + "walkdir", +] + +[[package]] +name = "egui" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.11.1", + "emath 0.33.3", + "epaint 0.33.3", + "log", + "nohash-hasher", + "profiling", + "serde", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui_dock" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0f689738efbd4afb811433b873379c6a8f3af48f2d296d9849fe70ee021d6" +dependencies = [ + "duplicate", + "egui", + "paste", + "serde", +] + +[[package]] +name = "egui_phosphor_icons" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633bbce043c6529ff60011aa7c4f2f1d12b9d2b5a87dc18749b615a7304f05fe" +dependencies = [ + "egui", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "emath" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "emath" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53f0d33a479321da6b0caa71366c9f67e8a2c149762d90bdc0d16e601ee8ecb" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "encase" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3e0ff2ee0b7aa97428308dd9e1e42369cb22f5fb8dc1c55546637443a60f1e" +dependencies = [ + "const_panic", + "encase_derive", + "thiserror 2.0.18", +] + +[[package]] +name = "encase_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d90c5d7d527c6cb8a3b114efd26a6304d9ab772656e73d8f4e32b1f3d601a2" +dependencies = [ + "encase_derive_impl", +] + +[[package]] +name = "encase_derive_impl" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bad72d8308f7a382de2391ec978ddd736e0103846b965d7e2a63a75768af30" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "epaint" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor 0.33.3", + "emath 0.33.3", + "epaint_default_fonts 0.33.3", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "serde", +] + +[[package]] +name = "epaint" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" +dependencies = [ + "ahash", + "ecolor 0.34.3", + "emath 0.34.3", + "epaint_default_fonts 0.34.3", + "font-types 0.11.3", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa 0.40.0", + "smallvec", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" + +[[package]] +name = "epaint_default_fonts" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "game" +version = "0.1.0" +dependencies = [ + "avian3d", + "bevy", + "bevy_ufbx", + "game_hot", + "hot-lib-reloader", + "protocol", + "settings", + "shared", + "sim", +] + +[[package]] +name = "game_hot" +version = "0.1.0" +dependencies = [ + "avian3d", + "bevy", + "bevy_core_pipeline", + "bevy_solari", + "protocol", + "settings", + "shared", + "sim", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gilrs" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa85c2e35dc565c90511917897ea4eae16b77f2773d5223536f7b602536d462" +dependencies = [ + "fnv", + "gilrs-core", + "log", + "uuid", + "vec_map", +] + +[[package]] +name = "gilrs-core" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23f2cc5144060a7f8d9e02d3fce5d06705376568256a509cdbc3c24d47e4f04" +dependencies = [ + "inotify 0.11.1", + "js-sys", + "libc", + "libudev-sys", + "log", + "nix 0.30.1", + "objc2-core-foundation", + "objc2-io-kit", + "uuid", + "vec_map", + "wasm-bindgen", + "web-sys", + "windows 0.62.2", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "approx", + "bytemuck", + "encase", + "libm", + "mint", + "rand", + "serde_core", +] + +[[package]] +name = "glam" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +dependencies = [ + "mint", +] + +[[package]] +name = "glam_matrix_extras" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4664468a60479272b880a8bfc00ad2915229b93d2b2d585556fb33f9ba80e72" +dependencies = [ + "bevy_reflect", + "glam 0.30.10", + "serde", +] + +[[package]] +name = "glamx" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375b4fa374a343fef990a18a6e0413d54bd990c6d7b8c7ada2d3c884275edea3" +dependencies = [ + "approx", + "glam 0.30.10", + "num-traits", + "serde", + "simba", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gltf" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" +dependencies = [ + "base64 0.13.1", + "byteorder", + "gltf-json", + "image", + "lazy_static", + "serde_json", + "urlencoding", +] + +[[package]] +name = "gltf-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" +dependencies = [ + "inflections", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "grid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0caaee032384c10dd597af4579c67dee16650d862a9ccbe1233ff1a379abc07" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "read-fonts 0.36.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hexasphere" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a164ceff4500f2a72b1d21beaa8aa8ad83aec2b641844c659b190cb3ea2e0b" +dependencies = [ + "constgebra", + "glam 0.30.10", + "tinyvec", +] + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hot-lib-reloader" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ab81229a2ea97fe1c9c31f36b2f1a7850304fb3dac5790fa2e9804ea68111a" +dependencies = [ + "crc32fast", + "hot-lib-reloader-macro", + "libloading", + "log", + "notify", + "notify-debouncer-full", + "thiserror 2.0.18", +] + +[[package]] +name = "hot-lib-reloader-macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62680ce3b216830fc95e1fbcc769bfb7ed8f98b92058102b3a731258b0d9d0e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + +[[package]] +name = "ktx2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7f53bdf698e7aa7ec916411bbdc8078135da11b66db5182675b2227f6c0d07" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.8.0", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "pp-rs", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "naga_oil" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310c347db1b30e69581f3b84dc9a5c311ed583f67851b39b77953cb7a066c97f" +dependencies = [ + "codespan-reporting", + "data-encoding", + "indexmap", + "naga", + "regex", + "rustc-hash 1.1.0", + "thiserror 2.0.18", + "tracing", + "unicode-ident", +] + +[[package]] +name = "nanorand" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729eb334247daa1803e0a094d0a5c55711b85571179f5ec6e53eccfdf7008958" + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "normpath" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify 0.9.6", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni 0.21.1", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "obvhs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a714277d5f74beb21a12cb8fb1d4774df5c637c2a0278ff3ebd812f42f7c1e" +dependencies = [ + "bytemuck", + "glam 0.31.1", + "half", + "log", + "rdst", + "static_assertions", +] + +[[package]] +name = "offset-allocator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234d535da3521eb95106f40f0b73483d80bfb3aacf27c40d7e2b72f1a3e00a2" +dependencies = [ + "log", + "nonmax", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opener" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fa337e0cf13357c13ef1dc108df1333eb192f75fc170bea03fcf1fd404c2ee" +dependencies = [ + "bstr", + "normpath", + "windows-sys 0.61.2", +] + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parry3d" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8598fd0fa548d469b6ca4f9e90b446434fb879c12494a91e41c341d8f0f558d8" +dependencies = [ + "approx", + "arrayvec", + "bitflags 2.11.1", + "downcast-rs 2.0.2", + "either", + "ena", + "foldhash 0.2.0", + "glamx", + "hashbrown 0.16.1", + "log", + "num-derive", + "num-traits", + "ordered-float", + "rstar", + "serde", + "serde_arrays", + "simba", + "slab", + "smallvec", + "spade", + "static_assertions", + "thiserror 2.0.18", +] + +[[package]] +name = "parry3d-f64" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f7e4d41f17de279308ca679ae0e30e7eef68e36088dd5a34e7d7923461cf0d" +dependencies = [ + "approx", + "arrayvec", + "bitflags 2.11.1", + "downcast-rs 2.0.2", + "either", + "ena", + "foldhash 0.2.0", + "glamx", + "hashbrown 0.16.1", + "log", + "num-derive", + "num-traits", + "ordered-float", + "rstar", + "serde", + "serde_arrays", + "simba", + "slab", + "smallvec", + "spade", + "thiserror 2.0.18", +] + +[[package]] +name = "partition" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947f833aaa585cf12b8ec7c0476c98784c49f33b861376ffc84ed92adebf2aba" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", + "serde_derive", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "pp-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "bevy", + "serde", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radsort" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rdst" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7970b4e577b76a96d5e56b5f6662b66d1a4e1f5bb026ee118fc31b373c2752" +dependencies = [ + "arbitrary-chunks", + "block-pseudorand", + "criterion", + "partition", + "tikv-jemallocator", + "voracious_radix_sort", +] + +[[package]] +name = "read-fonts" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "percent-encoding", + "pollster", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rodio" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "cpal", + "lewton", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.11.1", + "serde", + "serde_derive", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags 2.11.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scene" +version = "0.1.0" +dependencies = [ + "ron 0.8.1", + "serde", + "shared", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "settings" +version = "0.1.0" +dependencies = [ + "bevy", + "ron 0.8.1", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared" +version = "0.1.0" +dependencies = [ + "avian3d", + "bevy", + "ron 0.8.1", + "serde", + "settings", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sim" +version = "0.1.0" +dependencies = [ + "avian3d", + "bevy", + "protocol", + "settings", + "shared", +] + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "skrifa" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e" +dependencies = [ + "bytemuck", + "read-fonts 0.36.0", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol-hyper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7428a49d323867702cd12b97b08a6b0104f39ec13b49117911f101271321bc1a" +dependencies = [ + "async-executor", + "async-io", + "futures-io", + "hyper", + "pin-project-lite", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spade" +version = "2.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" +dependencies = [ + "hashbrown 0.16.1", + "num-traits", + "robust", + "serde", + "smallvec", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stackfuture" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115beb9c69db2393ff10b75a1b8587a51716e5551d015001e55320ed279d32f9" +dependencies = [ + "const_panic", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +dependencies = [ + "skrifa 0.40.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", +] + +[[package]] +name = "taffy" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "transform-gizmo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52e45b2efe33a248468cabe2d32415f5695d28fd1282381cc000c178cdfc2f7" +dependencies = [ + "ahash", + "ecolor 0.34.3", + "emath 0.34.3", + "enum_dispatch", + "enumset", + "epaint 0.34.3", + "glam 0.32.1", + "mint", +] + +[[package]] +name = "transform-gizmo-bevy" +version = "0.9.0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bytemuck", + "transform-gizmo", + "uuid", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + +[[package]] +name = "ufbx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf2324f07023fcd0507bb122c5406911e73f7b4f1056c903d740b57128bab84" +dependencies = [ + "cc", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +dependencies = [ + "bytemuck", + "fearless_simd", + "hashbrown 0.16.1", + "log", + "peniko", + "skrifa 0.40.0", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "voracious_radix_sort" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e7ffcb6c27a71d05af7e51ef2ee5b71c48424b122a832f2439651e1914899" +dependencies = [ + "rayon", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +dependencies = [ + "core-foundation 0.10.1", + "jni 0.22.4", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.11.1", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.6.0+11769913", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.1", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "scene", + "walkdir", +] + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5efe988 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,63 @@ +[workspace] +members = [ + "crates/game", + "crates/game_hot", + "crates/shared", + "crates/editor", + "crates/protocol", + "crates/sim", + "crates/settings", + "crates/scene", + "xtask", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[workspace.dependencies] +avian3d = { version = "0.6", default-features = false, features = [ + "3d", + "f32", + "parry-f32", + "bevy_scene", + "collider-from-mesh", + "serialize", +] } +bevy = { version = "0.18", features = ["serialize"] } +bevy_core_pipeline = "0.18" +bevy_solari = "0.18" +bevy_ufbx = "0.18" +bevy_egui = "0.39" +bevy-inspector-egui = "0.36" +egui_dock = { version = "0.18", features = ["serde"] } +egui_phosphor_icons = "0.2" +transform-gizmo-bevy = "0.9" +serde = { version = "1", features = ["derive"] } +shared = { path = "crates/shared" } +game = { path = "crates/game" } +game_hot = { path = "crates/game_hot" } +protocol = { path = "crates/protocol" } +sim = { path = "crates/sim" } +settings = { path = "crates/settings" } +scene = { path = "crates/scene" } + +[patch.crates-io] +# Atmosphere-aware mesh view bind groups (WYSIWYG editor + transform gizmos). +transform-gizmo-bevy = { path = "third_party/transform-gizmo-bevy" } + +# Compile our own crate with light optimizations, but fully optimize dependencies +# (the bevy/wgpu stack) so debug builds are not unbearably slow. +[profile.dev] +opt-level = 1 +rpath = true + +[profile.dev.package."*"] +opt-level = 3 + +# A snappy release profile for shipping builds. +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5a8c7f --- /dev/null +++ b/README.md @@ -0,0 +1,314 @@ +# Bevy FPS Foundation + +A modular first-person game foundation and in-process editor built on **Bevy 0.18** and +**Avian 0.6** physics. + +The runtime game provides a high-fidelity PBR stack (HDR, procedural atmosphere + image-based +lighting, cascaded shadows, SSAO, TAA, bloom, fog, ACES tonemapping) plus Hybrid Auto Solari +raytraced indirect lighting where supported, and local input/presentation glue for a fixed-step +kinematic FPS simulation. The editor provides dockable egui panels, a +filtered scene hierarchy, click selection, transform gizmos, undo/redo, asset import/placement, +native Bevy scene save/load, and BRP support for external tooling. + +## Requirements + +- Rust (stable; pinned via `rust-toolchain.toml`) +- Git LFS for binary game/source assets: run `git lfs install` once, then `git lfs pull` after clone. +- A Vulkan-capable GPU + drivers (developed against an NVIDIA RTX 3080 Ti) +- Linux build deps for `winit`/`wgpu` (ALSA, udev, etc.) if not already present + +## Running + +```bash +# Check everything +cargo fmt --check +cargo check --workspace +cargo clippy --workspace +cargo clippy -p game -p shared -p protocol -p sim -- -D warnings +cargo test -p game --lib +cargo test -p sim + +# Runtime game +cargo run -p game +cargo run -p game --features dev + +# In-engine editor +cargo run -p editor +cargo run -p editor --features dev +``` + +### Hot reload (gameplay iteration) + +`--features dev` turns on Bevy **dynamic linking** for faster rebuilds (recommended day-to-day). + +Add `--features hot-reload` for **in-process hot reload** of [`game_hot`](crates/game_hot) (sim movement, player input/camera, world bootstrap). The editor process stays open; only gameplay system bodies swap when the dylib rebuilds. + +**Two-terminal workflow (hot reload):** + +```bash +# Terminal 1 — rebuild the hot dylib on save +cargo watch -w crates/game_hot -w crates/sim -x "build -p game_hot --features dylib" + +# Terminal 2 — run the editor once +cargo run -p editor --features dev,hot-reload +``` + +Or use the VS Code task **watch game_hot (hot reload)** alongside **run editor (dev fast-link)** (enable the `hot-reload` feature on the editor run task when using dylib iteration). + +| Input | Action | +|-------|--------| +| Save `game_hot` / `sim` gameplay code | Dylib rebuilds; status bar shows **building…** then **reloaded** | +| **Ctrl+Shift+R** | Manual refresh (`on_hot_reload` bootstrap: camera FX, ambient light) | + +**Safe to hot reload:** system logic, movement tuning in code, input mapping. + +**Requires full editor restart:** new/changed component fields, new components, plugin schedule changes, editor UI code. + +> Note: the workspace uses the `mold` linker via `clang` (see `.cargo/config.toml`) for fast link times. + +### Target Cache Cleanup + +Cargo/Bevy debug artifacts can grow quickly. Use the workspace cleanup task before reaching for a full `cargo clean`: + +| Command | Effect | +|---------|--------| +| `cargo clean-target` | Dry run: prints reclaimable `target/` cache. | +| `cargo clean-target --apply` | Safe cleanup: removes incremental and rust-analyzer flycheck cache. | +| `cargo clean-target --include-artifacts --days 3 --apply` | Deeper cleanup: also removes stale hashed `deps`, `build`, `.fingerprint`, and example artifacts older than 3 days. Cargo will rebuild anything still needed. | + +VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**. + +### Launch Troubleshooting + +- The native game/editor windows force an opaque Wayland surface and opaque camera clears to avoid compositor alpha issues on mixed HDR/SDR desktops. +- If the window maps but appears transparent on Hyprland or another Wayland compositor, launch with `BEVY_FPS_HDR=0` to force the SDR camera path while debugging monitor/compositor behavior. +- Debug launch configs and run tasks set `WGPU_VALIDATION=0` to quiet the known wgpu/Vulkan validation-layer warning `VUID-StandaloneSpirv-MemorySemantics-10871`. This only disables the Vulkan validation layer for those launches; Rust panics and application errors still surface normally. +- If **CodeLLDB / mold** fails with hundreds of `undefined symbol` linker errors, the incremental `target/` cache is stale. Run the VS Code task **clean build editor (dev)** or `cargo clean -p editor && cargo build -p editor --features dev`, then launch **Debug editor** again. Use `cargo run -p editor --features dev` from the terminal if you need `libbevy_dylib` on `LD_LIBRARY_PATH` automatically. + +## Editor Controls + +| Input | Action | +|-------|--------| +| `F5` | Toggle Edit / Play in the same viewport | +| `F6` | While playing: pause / resume simulation (stay in Play mode) | +| `F8` | While playing: possess player / eject to editor camera | +| `G` | Toggle clean game-view overlay (hide editor widgets/proxies/gizmos) | +| `Ctrl+G` | Toggle viewport grid | +| `F` | Focus editor camera on selection | +| `Ctrl+Shift+1` / `Ctrl+Shift+2` | Save / recall viewport camera bookmark | +| RMB + mouse | Editor camera look | +| RMB + `W` `A` `S` `D` | Editor camera fly | +| RMB + `Q` / `E` | Editor camera down / up | +| Mouse wheel | Dolly editor camera | +| MMB + mouse | Pan editor camera | +| Click authored object, actor root icon, or visualizer in viewport | Select source entity | +| Click empty viewport / `Esc` | Deselect | +| `Delete` / `Backspace` | Delete selection | +| `Ctrl+D` | Duplicate selection | +| `Ctrl+Z` / `Ctrl+Shift+Z` / `Ctrl+Y` | Undo / redo | +| `F2` in Hierarchy | Rename selection | +| `W` / `E` / `R` | Translate / rotate / scale gizmo | +| `X` | Toggle world/local gizmo orientation | +| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) | +| Viewport eye/options | Toggle actor root icon categories, colliders, lights, spawns, prefab/model anchors, and runtime player/camera visualizers | +| `Tab` in viewport | Cycle selection through overlapping objects at last click | +| Click Player visualizer in Edit mode | Select or create the authored `PlayerSpawn` (`Player Start`) | +| Select Project Sun | Inspect project default lighting; create a scene sun override | +| Asset Browser project/file views | Browse `assets/`, search/filter/sort, switch grid/list, inspect import settings; drag assets into the viewport to place | +| Asset Browser texture action | Assign base-color texture to selection | +| `Ctrl+P` | Command palette (`scene.reset_lighting`, `selection.group`, `selection.focus`, play commands) | +| `F7` | While paused in Play: advance one sim tick | +| Shift/Ctrl + click (Hierarchy) | Additive selection | +| Hierarchy context | Reparent to other selection / Unparent | +| File menu | New, Open, Save, Save As, Import Assets, Export Selection, Save Selection As Prefab, Recent Scenes | +| Inspector component card | Collapse with caret, toggle active with status dot, or use triple-dot menu for reset/copy/paste/move/remove actions | +| Inspector footer → Add Component | Pinned search/add control for registered authoring, rendering, physics, and gameplay components with undo | +| Edit → Project Settings… | Edit `assets/project.ron` (rendering, physics, input) | + +### Play Mode + +- Press **F5** (or Play menu) to run the **real game** in-process: same `GamePlugin`, player, + fixed-step `sim`, and rendering stack as the standalone `game` binary. +- The unified **Viewport** stays in place on play enter. The editor fly camera and player camera + render to the same offscreen HDR texture depending on Edit/Play possession state. +- While editing, gameplay simulation and input are disabled (`SimEnabled` / `GameInputEnabled`). + Entering Play enables simulation; **F6** pauses/resumes the sim without leaving Play; **F8** toggles **possess** (player input + camera) vs **eject** + (editor fly camera while sim keeps running). +- Transport controls (Play, Pause, Stop, Eject) live on the **main toolbar** below the menu bar. +- The viewport uses the full project rendering stack from **`assets/project.ron`** + (Edit → Project Settings…). Only one 3D camera carries that stack at a + time (editor fly cam while editing/ejected, player cam while possessed) so GPU bind limits are + respected. +- Add a `PlayerSpawn` marker component (via Inspector) on a level object to choose where Play + starts; otherwise the default game spawn `(0, 1.5, 10)` is used. +- Clean game-view overlay (`G`) hides editor widgets, actor root icons, visualizers, selectable proxies, gizmos, + selection outlines, and grid without changing the active camera. +- **F5** stops Play and restores **player sim state** only; authored scene edits made during PIE + are kept. **Esc** frees the cursor mid-play. + +## Game Controls + +| Input | Action | +|-------|--------| +| `W` `A` `S` `D` | Move | +| Mouse | Look | +| `Space` | Jump (coyote-time + buffered) | +| `Left Shift` | Sprint | +| `Left Ctrl` | Crouch | +| `Esc` | Release / re-grab mouse cursor | + +## Scene Workflow + +- Levels are saved as native Bevy dynamic scenes under `assets/levels/`. +- Editable entities use reflectable authoring components from `crates/shared`. +- The editable player start is an authored `PlayerSpawn`; moving it with the transform gizmo changes + where Play mode boots the runtime player. +- Project settings provide default ambient/sun lighting. A scene-authored directional `LightDesc` + acts as a per-scene sun override and disables the project sun contribution. +- Hydration systems turn authoring data into runtime meshes, GLTF scenes, materials, lights, and + Avian colliders. Persisted `ActorId` / `ComponentInstanceId` values are stable; raw Bevy `Entity` + IDs are runtime-only. +- Inspector component active toggles are saved in `InspectorOrder`. Inactive authoring components + keep their data but do not hydrate runtime meshes, lights, physics, or post-process effects. +- Dragging glTF/GLB/FBX assets creates `StaticMeshRenderer` actors by default from normalized + artifacts in `assets/meshes/generated/`. Renderer slots reference imported content-browser mesh + and material assets, while generated collision is stored separately in `ColliderDesc` plus + `RigidBodyDesc`. Switch a model asset's placement mode to **Scene Instance** in Asset Browser + details when you need full `SceneRoot` loading for animation/skinning/scene data. +- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable. +- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save. +- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored + `LevelObject` edits made during PIE **remain** in the scene (the level may show as dirty). +- Play mode swaps the unified viewport between the player camera (possessed) and editor fly camera + (ejected). +- Gameplay movement runs in a fixed timestep in `crates/sim`. Raw keyboard/mouse input is translated + by `crates/game` into `protocol::PlayerInputIntent` before it mutates simulation state. + +## Troubleshooting + +### Viewport is black or wireframe-only + +1. Check the **mode badge** (bottom-right of the viewport). **Collider** hides meshes — click the **sun** toolbar icon for **Lit** shading. +2. **Window → Rendering → Active Camera**: check **Runtime lights** (directional/point/spot counts), scene vs project sun status, and contributing post-process volumes. Scene directionals override the project sun; use **Scene → Lighting → Use project sun** to restore project defaults. +3. **Solari requested but not visually changing**: check **Window → Rendering → Active Camera** for requested/effective GI, fallback reason, tagged meshes, Solari-compatible mesh assets, render instances, bind-group readiness, and compatible lights. Effective Solari forces an HDR camera target because Bevy Solari writes through a storage texture; imported/custom meshes need TriangleList geometry, POSITION/NORMAL/UV_0/TANGENT attributes, and U32 indices for Bevy 0.18 Solari. +4. **Lighting changes do nothing in Solari**: Bevy 0.18 Solari samples directional lights and emissive meshes, not point/spot lights. Use Directional lights or emissive materials in Solari; switch to Forward PBR for point/spot light authoring. +5. **GiMode Auto shows Forward**: Solari RT wgpu features are unavailable on this GPU. Dev RT override: `BEVY_FPS_FORCE_SOLARI=1`. +6. **Volume overrides ignored**: confirm camera is inside volume AABB; check priority in Rendering → Volumes tab. +7. **Custom post FX**: RON under `assets/post_fx/`; assign path in volume inspector (see [docs/editor/rendering.md](docs/editor/rendering.md)). +8. Scene **Open** / **Recent** runs hydration immediately; **zero runtime lights** after load usually means missing `LightDesc` on light actors (see Rendering panel). + +### Hierarchy sort jumps when clicking + +**Sort: Name** / **Type** now use stable tie-breakers (duplicate names like several `Pillar` rows stay put). Use **Sort: Manual** and drag-and-drop for explicit sibling order. + +### Hierarchy scrolls during drag + +Scroll-to-drag is disabled while dragging actors. Release the mouse to finish or cancel the drag. + +## Cursor / VSCode Setup + +The `.vscode/` folder is preconfigured: + +- `extensions.json` recommends rust-analyzer, Even Better TOML, CodeLLDB, crates, and Error Lens. +- `settings.json` runs `clippy` on save, enables proc-macro/build-script support, formats on save, + and excludes `target/` from search/watch. +- `tasks.json` provides workspace check/clippy plus game/editor build/run tasks. +- `launch.json` provides CodeLLDB launch configs for the editor and game. +- `.github/workflows/ci.yml` mirrors local formatting, check, clippy, test, and binary build verification. + +## Architecture Decisions + +- [Documentation index](docs/README.md) +- [Mission — editor framework & principles](docs/mission.md) +- [Editor framework docs](docs/editor/README.md) +- [ADR 0001: Roadmap Architecture](docs/adr/0001-roadmap-architecture.md) +- [ADR 0002: Bevy Version And Migration Policy](docs/adr/0002-bevy-version-and-migration-policy.md) +- [ADR 0003: Editor Framework Mission](docs/adr/0003-editor-framework-mission.md) +- [ADR 0014: Unified Viewport Model](docs/adr/0014-unified-viewport-model.md) +- [ADR 0016: Unified Rendering Contract](docs/adr/0016-unified-rendering-contract.md) +- [ADR 0017: Normalized Static Mesh Assets](docs/adr/0017-normalized-static-mesh-assets.md) + +## Project Layout + +``` +crates/ + protocol/ Shared tick, input intent, command, and message/event protocol types + sim/ Fixed-step gameplay simulation and determinism test scaffolding + shared/ Reflectable authoring components + hydration systems + game/ Runtime FPS game library + standalone game binary + editor/ In-process egui editor binary +``` + +## Implementation Checklist + +- [x] Workspace: `game`, `shared`, `editor`, `protocol`, `sim` +- [x] Shared authoring components + hydration +- [x] Protocol tick, input intent, command, and basic message/event types +- [x] Fixed-step deterministic sim crate for player movement/controller state +- [x] Runtime game refactored into `GamePlugin` +- [x] Raw game input translated into protocol intent before simulation +- [x] Docked egui editor scaffold +- [x] Editor fly camera +- [x] Mesh picking selection + highlights +- [x] Transform gizmos +- [x] Editor-side scene visualizers for colliders, lights, player spawns, prefab/model anchors, and runtime player/cameras +- [x] Selectable actor root icons in the 3D viewport with per-category visibility controls +- [x] Filtered hierarchy, inspector, viewport, toolbar, asset browser, and status panels +- [x] Delete, duplicate, rename, and structural/material undo-redo +- [x] Native Bevy scene New/Open/Save/Save As with dirty title tracking +- [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] Unified viewport render-to-texture target + Play session bootstrap +- [x] `PlayerSpawn` authoring marker for editor Play start location +- [x] BRP enabled in the editor +- [x] Cursor/VSCode workspace tasks + launch configs +- [x] Opaque Wayland window launch defaults + SDR/HDR runtime toggle +- [x] CI workflow for format, check, clippy, tests, and binary builds +- [x] ADRs for roadmap architecture and Bevy migration policy +- [x] Determinism harness: same inputs over same ticks produce the same state summary/hash +- [x] Verify: `cargo fmt --check` / `cargo check --workspace` / `cargo clippy --workspace` / strict foundation clippy / `cargo test -p sim` +- [x] Stable asset registry with UUIDs + import settings in asset browser details +- [x] Prefab instances (`PrefabInstance`) + save-as-prefab + unpack +- [x] Unsaved-scene confirm on New/Open/Recent +- [x] Hierarchy multi-select, reparent undo, multi-entity gizmo transform +- [x] Typed inspector undo (light, rigid body, collider, primitive, material, static mesh renderer) + registry-driven Add Component footer +- [x] Gameplay authoring markers + visualizers (`WeaponSpawn`, `TriggerVolume`, etc.) +- [x] Command palette execution; PIE sim step (F7); `xtask validate-levels` +- [x] ADRs 0005–0012 (prefab/registry, scene schema, EditorPlugin, editor structure, authoring/hydration, ActorKind, sun policy, zero-debt) +- [x] `ActorInspectorSection` registry + game demo section (`game::editor_ext`) +- [x] Command palette: reset lighting, group selection, focus selection +- [x] CI: `cargo test -p shared`, scene authoring-only check on repo level, `cargo check -p editor --features dev` +- [x] `scene` crate schema stamp/migrate/validate on save/load + CI `validate-levels` +- [x] Project Settings draft + Apply (HDR/swapchain safe); File → Project New/Open +- [x] Editor lib/bin split + `EditorPluginGroup`; game EditorPlugin dogfood panel +- [x] FBX/glTF model import + normalized `StaticMeshRenderer` placement; explicit scene-instance load via `bevy_ufbx` / `ModelRef` +- [x] Asset browser model thumbnails (unified `assets/thumbnails/` pipeline; `ThumbnailState` cache; FBX via `FbxThumbnailSource`) +- [x] Material assets (`assets/materials/*.ron`, inspector picker, drag-drop to selection) +- [x] Prefab instance overrides v2 (transform/material/child visibility apply-revert groups) +- [x] Advanced rendering: `GiMode`, post-process volumes, Rendering panel, requested/effective render stack, Solari integration, emissive materials, post FX assets ([ADR 0013](docs/adr/0013-rendering-tiers-and-post-process-volumes.md), [ADR 0016](docs/adr/0016-unified-rendering-contract.md), [rendering guide](docs/editor/rendering.md)) +- [x] Static mesh renderer component, generated normalized mesh artifacts, source/one-actor hierarchy placement, and inspector renderer slots ([ADR 0017](docs/adr/0017-normalized-static-mesh-assets.md)) +- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware material data, and per-actor material overrides ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md)) + +## Notes / Future Work + +- Crouch lowers the camera and movement speed; the collider stays full-height for stability. +- The editor asset browser is filesystem-backed with folder/tree navigation, grid/list views, + texture and model thumbnails (glTF albedo fast-path; offscreen render studio for FBX and + untextured models), search/filter/sort controls, and a details pane. **File → Import Assets** + accepts glTF/GLB and **FBX** (binary; copies sibling `.fbm` texture folders when present). + Model assets generate normalized static mesh manifests under `assets/meshes/generated/`; drag/drop + uses `StaticMeshRenderer` by default with imported asset refs and optional separate static mesh + collider components. Asset details can switch placement to **Scene Instance** for `ModelRef`/`SceneRoot` + playback. Skeletal animation playback is not supported by the static mesh path yet. +- Prefab instances use stable asset IDs (`PrefabInstance` + registry) with serialized `PrefabOverrides` (transform, material, per-child visibility by name). Inspector **Apply/Revert** per field group; **Unpack** removes the instance link. +- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. Actor inspector edits create per-actor `MaterialOverride` data for static mesh slots instead of mutating shared material assets. +- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path. +- The authoring/hydration layer is intentionally small so richer asset workflows (terrain, + material graphs, lighting profiles) can be added without changing the scene format foundation. +- The M2 sim split keeps camera pitch as local presentation state; body yaw and movement are + fixed-step intent consumers. M3 should map the existing protocol envelopes onto Lightyear and add + server-authoritative snapshots/prediction rather than broadening the local input path ad hoc. +- The editor renders egui on a dedicated full-window `Camera2d` (`PrimaryEguiContext`, + `RenderLayers::none()`, `auto_create_primary_context = false`) so the 3D viewport camera can be + cropped to the viewport panel without cropping egui itself. Attaching egui to a viewport-cropped + 3D camera makes `egui_dock` lay out into a NaN rect and panics in `advance_cursor_after_rect`. diff --git a/assets/.index/registry.ron b/assets/.index/registry.ron new file mode 100644 index 0000000..51827bb --- /dev/null +++ b/assets/.index/registry.ron @@ -0,0 +1,226 @@ +[ + ( + id: ("31963299-d4c1-45dc-8f67-2f469ca1ad6d"), + path: "assets/post_fx/chromatic_aberration.ron", + label: "chromatic_aberration", + kind_tag: "PostProcessEffect", + 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, + ), + dependencies: [], + ), + ( + id: ("e14a9bc3-9a4e-47b0-901f-b31c004e1836"), + path: "assets/post_fx/vignette.ron", + label: "vignette", + kind_tag: "PostProcessEffect", + 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, + ), + dependencies: [], + ), + ( + id: ("b8b43f26-0eee-4ee8-9e16-53aedbc69205"), + path: "assets/models/Room3.fbx", + label: "Room3", + kind_tag: "Model", + import_settings: ( + scale: 1.7, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SingleActor, + material_policy: AuthoringOverride, + static_mesh_manifest_path: Some("assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron"), + ), + dependencies: [], + ), + ( + id: ("582cd326-c32d-49da-9424-ae38b3d60648"), + path: "assets/models/Dumpster.fbx", + label: "Dumpster", + kind_tag: "Model", + import_settings: ( + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SourceHierarchy, + material_policy: AuthoringOverride, + static_mesh_manifest_path: Some("assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron"), + ), + dependencies: [], + ), + ( + id: ("61802b85-fda9-4aff-94d9-fe5f0860c516"), + path: "assets/levels/rendering_showcase.scn.ron", + label: "rendering_showcase.scn", + kind_tag: "Prefab", + 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, + ), + dependencies: [], + ), + ( + id: ("48bcef13-e6a6-4260-bb1b-c29787bd8c70"), + path: "assets/levels/editor_scene 3.scn.ron", + label: "editor_scene 3.scn", + kind_tag: "Prefab", + 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, + ), + dependencies: [], + ), + ( + id: ("5a2307d0-48e5-40d3-a5c9-527c0cfd30f4"), + path: "assets/levels/editor_scene.scn.ron", + label: "editor_scene.scn", + kind_tag: "Prefab", + 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, + ), + dependencies: [], + ), + ( + id: ("dc13ce01-c7ce-43c5-974c-0e659ae49ab9"), + path: "assets/levels/editor_scene 2.scn.ron", + label: "editor_scene 2.scn", + kind_tag: "Prefab", + 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, + ), + dependencies: [], + ), + ( + id: ("802471df-8295-4997-80e2-db4191850fc9"), + path: "assets/rendering_profiles/outdoor_haze.ron", + label: "outdoor_haze", + kind_tag: "RenderingProfile", + 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, + ), + dependencies: [], + ), + ( + id: ("2c4a9866-17b5-4797-9a8f-726b2ffacdb9"), + path: "assets/rendering_profiles/cave_dark.ron", + label: "cave_dark", + kind_tag: "RenderingProfile", + 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, + ), + dependencies: [], + ), + ( + id: ("a57f2891-8536-47b8-b476-01c08b36ac43"), + path: "assets/materials/concrete.ron", + label: "concrete", + kind_tag: "Material", + 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, + ), + dependencies: [], + ), + ( + id: ("4f684592-50b1-47e4-888c-8ba537e4c29d"), + path: "assets/materials/emissive_panel.ron", + label: "emissive_panel", + kind_tag: "Material", + 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, + ), + dependencies: [], + ), + ( + id: ("7b143cfb-a9e9-45d4-8f01-cfaa66fa9811"), + path: "assets/shaders/unlit.shader.ron", + label: "unlit.shader", + kind_tag: "ShaderSchema", + 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, + ), + dependencies: [], + ), + ( + id: ("addf6f7f-8114-45f3-8566-70d1eaa1fec3"), + path: "assets/shaders/standard_lit.shader.ron", + label: "standard_lit.shader", + kind_tag: "ShaderSchema", + 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, + ), + dependencies: [], + ), +] \ No newline at end of file diff --git a/assets/audio/.gitkeep b/assets/audio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/levels/.gitkeep b/assets/levels/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/levels/editor_scene 2.scn.ron b/assets/levels/editor_scene 2.scn.ron new file mode 100644 index 0000000..67270d2 --- /dev/null +++ b/assets/levels/editor_scene 2.scn.ron @@ -0,0 +1,1178 @@ +(schema_version: 2, + resources: {}, + entities: { + 4294967126: ( + components: { + "bevy_ecs::name::Name": "Pillar Copy", + "bevy_transform::components::transform::Transform": ( + translation: (3.7178926, 3.5, -11.932255), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (23), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 4294967158: ( + components: { + "bevy_ecs::name::Name": "Scene Sun", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 5.316672, 0.0), + rotation: (-0.21401538, 0.31161797, 0.67592126, 0.6326311), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": Light, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (0), + "shared::components::LevelObject": (), + "shared::components::LightDesc": ( + kind: Directional, + color: ( + r: 1.0, + g: 0.95, + b: 0.85, + a: 1.0, + ), + intensity: 100000.0, + range: 0.0, + shadows: true, + inner_angle_deg: 25.0, + outer_angle_deg: 35.0, + ), + }, + ), + 4294967159: ( + components: { + "bevy_ecs::name::Name": "Ground", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, -0.5, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (1), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.42, + g: 0.45, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.92, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 200.0, + y_length: 1.0, + z_length: 200.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (200.0, 1.0, 200.0), + ), + }, + ), + 8589934420: ( + components: { + "bevy_ecs::name::Name": "Pillar Copy", + "bevy_transform::components::transform::Transform": ( + translation: (-12.079595, 3.5, -13.098544), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (25), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934456: ( + components: { + "bevy_ecs::name::Name": "Player Start", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 0.9900649, 12.46175), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": PlayerSpawn, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (2), + "shared::components::LevelObject": (), + "shared::components::PlayerSpawn": (), + }, + ), + 8589934457: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-1.3999996, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (3), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.95, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934458: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-3.6, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (4), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.6, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934459: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-5.8, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (5), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934460: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (6), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.05, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934461: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.90000004, 9.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (7), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.8000001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.8000001, 0.5), + ), + }, + ), + 8589934462: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.81000006, 10.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (8), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.6200001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.6200001, 0.5), + ), + }, + ), + 8589934463: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.72, 10.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (9), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.44, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.44, 0.5), + ), + }, + ), + 8589934464: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.63, 11.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (10), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.26, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.26, 0.5), + ), + }, + ), + 8589934465: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.54, 11.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (11), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.08, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.08, 0.5), + ), + }, + ), + 8589934466: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.45000002, 12.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (12), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.90000004, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.90000004, 0.5), + ), + }, + ), + 8589934467: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.36, 12.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (13), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.72, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.72, 0.5), + ), + }, + ), + 8589934468: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.27, 13.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (14), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.54, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.54, 0.5), + ), + }, + ), + 8589934469: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.18, 13.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (15), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.36, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.36, 0.5), + ), + }, + ), + 8589934470: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.09, 14.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (16), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.18, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.18, 0.5), + ), + }, + ), + 8589934471: ( + components: { + "bevy_ecs::name::Name": "Ramp", + "bevy_transform::components::transform::Transform": ( + translation: (-14.0, 1.6, 0.0), + rotation: (-0.17364821, 0.0, 0.0, 0.9848078), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (17), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.3, + g: 0.45, + b: 0.7, + a: 1.0, + ), + metallic: 0.1, + roughness: 0.55, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.4, + z_length: 9.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.4, 9.0), + ), + }, + ), + 8589934472: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (5.4, 0.5, 2.4), + rotation: (0.0, 0.94948566, 0.0, -0.31381062), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (18), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934473: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (4.0, 0.75, 2.0), + rotation: (0.0, 0.9854498, 0.0, 0.16996713), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (19), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.5, + y_length: 1.5, + z_length: 1.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.5, 1.5, 1.5), + ), + }, + ), + 8589934474: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-2.3, 0.5, -3.1), + rotation: (0.0, 0.72083056, 0.0, -0.6931113), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (20), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934475: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-1.6, 0.5, -3.2), + rotation: (0.0, -0.53118616, 0.0, 0.8472551), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (21), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934476: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-3.0, 0.5, -3.0), + rotation: (0.0, 0.86742324, 0.0, -0.49757105), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (22), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934477: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (23), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934478: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (24), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934479: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (25), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934480: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (26), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + }, +) \ No newline at end of file diff --git a/assets/levels/editor_scene 3.scn.ron b/assets/levels/editor_scene 3.scn.ron new file mode 100644 index 0000000..a467119 --- /dev/null +++ b/assets/levels/editor_scene 3.scn.ron @@ -0,0 +1,1504 @@ +(schema_version: 2, + resources: {}, + entities: { + 4294967124: ( + components: { + "bevy_ecs::name::Name": "Cube", + "bevy_transform::components::transform::Transform": ( + translation: (-14.0, 0.66941494, -1.9732417), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (29), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.8, + g: 0.8, + b: 0.8, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.65, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 4294967126: ( + components: { + "bevy_ecs::name::Name": "Cube", + "bevy_transform::components::transform::Transform": ( + translation: (-13.0, 0.5, 2.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (28), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.8, + g: 0.8, + b: 0.8, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.65, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 4294967128: ( + components: { + "bevy_ecs::name::Name": "Point Light", + "bevy_transform::components::transform::Transform": ( + translation: (-10.83656, 1.680895, -1.0713063), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (2.7062726, 1.0, 1.0), + ), + "shared::components::ActorKind": Light, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (27), + "shared::components::LevelObject": (), + "shared::components::LightDesc": ( + kind: Point, + color: ( + r: 1.0, + g: 0.95, + b: 0.85, + a: 1.0, + ), + intensity: 2000000.0, + range: 40.0, + shadows: true, + inner_angle_deg: 25.0, + outer_angle_deg: 35.0, + ), + }, + ), + 4294967158: ( + components: { + "bevy_ecs::name::Name": "Scene Sun", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 5.316672, 0.0), + rotation: (0.11260023, -0.04732944, 0.7427888, 0.65829015), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": Light, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (0), + "shared::components::LevelObject": (), + "shared::components::LightDesc": ( + kind: Directional, + color: ( + r: 1.0, + g: 0.95, + b: 0.85, + a: 1.0, + ), + intensity: 100000.0, + range: 0.0, + shadows: true, + inner_angle_deg: 25.0, + outer_angle_deg: 35.0, + ), + }, + ), + 4294967159: ( + components: { + "bevy_ecs::name::Name": "Ground", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, -0.5, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (1), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.42, + g: 0.45, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.92, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 200.0, + y_length: 1.0, + z_length: 200.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (200.0, 1.0, 200.0), + ), + }, + ), + 8589934417: ( + components: { + "bevy_ecs::name::Name": "Spot Light", + "bevy_transform::components::transform::Transform": ( + translation: (-13.387727, 0.9319196, -5.454666), + rotation: (-0.1494833, 0.0, 0.0, 0.9887642), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": Light, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (30), + "shared::components::LevelObject": (), + "shared::components::LightDesc": ( + kind: Spot, + color: ( + r: 1.0, + g: 0.95, + b: 0.85, + a: 1.0, + ), + intensity: 2000000.0, + range: 40.0, + shadows: true, + inner_angle_deg: 25.0, + outer_angle_deg: 35.0, + ), + }, + ), + 8589934456: ( + components: { + "bevy_ecs::name::Name": "Player Start", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 0.9900649, 12.46175), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": PlayerSpawn, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (2), + "shared::components::LevelObject": (), + "shared::components::PlayerSpawn": (), + }, + ), + 8589934457: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-1.3999996, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (3), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.95, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934458: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-3.6, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (4), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.6, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934459: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-5.8, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (5), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.3, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934460: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (6), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.05, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + }, + ), + 8589934461: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.90000004, 9.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (7), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.8000001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.8000001, 0.5), + ), + }, + ), + 8589934462: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.81000006, 10.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (8), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.6200001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.6200001, 0.5), + ), + }, + ), + 8589934463: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.72, 10.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (9), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.44, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.44, 0.5), + ), + }, + ), + 8589934464: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.63, 11.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (10), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.26, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.26, 0.5), + ), + }, + ), + 8589934465: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.54, 11.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (11), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.08, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.08, 0.5), + ), + }, + ), + 8589934466: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.45000002, 12.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (12), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.90000004, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.90000004, 0.5), + ), + }, + ), + 8589934467: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.36, 12.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (13), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.72, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.72, 0.5), + ), + }, + ), + 8589934468: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.27, 13.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (14), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.54, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.54, 0.5), + ), + }, + ), + 8589934469: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.18, 13.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (15), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.36, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.36, 0.5), + ), + }, + ), + 8589934470: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.09, 14.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (16), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.18, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.18, 0.5), + ), + }, + ), + 8589934471: ( + components: { + "bevy_ecs::name::Name": "Ramp", + "bevy_transform::components::transform::Transform": ( + translation: (-14.0, 2.292737, 0.0), + rotation: (-0.17364821, 0.0, 0.0, 0.9848078), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (17), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.3, + g: 0.45, + b: 0.7, + a: 1.0, + ), + metallic: 0.1, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.4, + z_length: 9.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.4, 9.0), + ), + }, + ), + 8589934472: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (5.4, 0.5, 2.4), + rotation: (0.0, 0.94948566, 0.0, -0.31381062), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (18), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934473: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (4.0, 0.75, 2.0), + rotation: (0.0, 0.9854498, 0.0, 0.16996713), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (19), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.5, + y_length: 1.5, + z_length: 1.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.5, 1.5, 1.5), + ), + }, + ), + 8589934474: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-2.3, 0.5, -3.1), + rotation: (0.0, 0.72083056, 0.0, -0.6931113), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (20), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934475: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-1.6, 0.5, -3.2), + rotation: (0.0, -0.53118616, 0.0, 0.8472551), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (21), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934476: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-3.0, 0.5, -3.0), + rotation: (0.0, 0.86742324, 0.0, -0.49757105), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (22), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + 8589934477: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (23), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934478: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (24), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934479: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (25), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 8589934480: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (26), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + 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, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + }, + ), + 12884901711: ( + components: { + "bevy_ecs::name::Name": "Cube", + "bevy_transform::components::transform::Transform": ( + translation: (-13.391208, 0.5, -6.673126), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + "shared::components::EditorVisibility": ( + visible: true, + ), + "shared::components::HierarchySiblingIndex": (31), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.7322267, + g: 0.0, + b: 0.0, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + 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: Some("assets/materials/concrete.ron"), + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + }, + ), + }, +) \ No newline at end of file diff --git a/assets/levels/editor_scene.scn.ron b/assets/levels/editor_scene.scn.ron new file mode 100644 index 0000000..f73cf85 --- /dev/null +++ b/assets/levels/editor_scene.scn.ron @@ -0,0 +1,959 @@ +(schema_version: 2, + resources: {}, + entities: { + 4294967133: ( + components: { + "bevy_ecs::name::Name": "Player Start", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 0.9900649, 12.46175), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::PlayerSpawn": (), + "shared::components::ActorKind": PlayerSpawn, + }, + ), + 4294967162: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-1.3999996, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.95, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967163: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-3.6, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.6, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967164: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-5.8, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967165: ( + components: { + "bevy_ecs::name::Name": "ShowcaseSphere", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 0.6, 4.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.9, + g: 0.9, + b: 0.92, + a: 1.0, + ), + metallic: 1.0, + roughness: 0.05, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Sphere( + radius: 0.6, + ), + ), + "shared::components::Primitive": ( + shape: Sphere, + size: (1.2, 1.2, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967166: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.90000004, 9.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.8000001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.8000001, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967167: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.81000006, 10.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.6200001, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.6200001, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967168: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.72, 10.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.44, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.44, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967169: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.63, 11.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.26, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.26, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967170: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.54, 11.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 1.08, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 1.08, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967171: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.45000002, 12.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.90000004, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.90000004, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967172: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.36, 12.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.72, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.72, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967173: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.27, 13.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.54, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.54, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967174: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.18, 13.5), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.36, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.36, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967175: ( + components: { + "bevy_ecs::name::Name": "Stair", + "bevy_transform::components::transform::Transform": ( + translation: (12.0, 0.09, 14.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.55, + g: 0.52, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.8, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.18, + z_length: 0.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.18, 0.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967176: ( + components: { + "bevy_ecs::name::Name": "Ramp", + "bevy_transform::components::transform::Transform": ( + translation: (-14.0, 1.6, 0.0), + rotation: (-0.1736482, 0.0, 0.0, 0.9848078), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.3, + g: 0.45, + b: 0.7, + a: 1.0, + ), + metallic: 0.1, + roughness: 0.55, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 5.0, + y_length: 0.4, + z_length: 9.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (5.0, 0.4, 9.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967177: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (5.4, 0.5, 2.4), + rotation: (0.0, 0.94948566, 0.0, -0.31381062), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967178: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (4.0, 0.75, 2.0), + rotation: (0.0, 0.9854498, 0.0, 0.16996716), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.5, + y_length: 1.5, + z_length: 1.5, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.5, 1.5, 1.5), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967179: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-2.3, 0.5, -3.1), + rotation: (0.0, 0.72083056, 0.0, -0.6931113), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967180: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-1.6, 0.5, -3.2), + rotation: (0.0, -0.53118616, 0.0, 0.84725505), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967181: ( + components: { + "bevy_ecs::name::Name": "Crate", + "bevy_transform::components::transform::Transform": ( + translation: (-3.0, 0.5, -3.0), + rotation: (0.0, 0.86742324, 0.0, -0.4975711), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.72, + g: 0.45, + b: 0.2, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.0, + y_length: 1.0, + z_length: 1.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.0, 1.0, 1.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967182: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967183: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, 6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967184: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967185: ( + components: { + "bevy_ecs::name::Name": "Pillar", + "bevy_transform::components::transform::Transform": ( + translation: (-8.0, 3.5, -6.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.85, + g: 0.85, + b: 0.88, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.3, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 1.2, + y_length: 7.0, + z_length: 1.2, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (1.2, 7.0, 1.2), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 4294967186: ( + components: { + "bevy_ecs::name::Name": "Ground", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, -0.5, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::MaterialDesc": ( + base_color: ( + r: 0.42, + g: 0.45, + b: 0.48, + a: 1.0, + ), + metallic: 0.0, + roughness: 0.92, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + ), + "shared::components::PhysicsBody": ( + body: Static, + collider: Cuboid( + x_length: 200.0, + y_length: 1.0, + z_length: 200.0, + ), + ), + "shared::components::Primitive": ( + shape: Box, + size: (200.0, 1.0, 200.0), + ), + "shared::components::ActorKind": StaticMesh, + }, + ), + 25769803611: ( + components: { + "bevy_ecs::name::Name": "Scene Sun", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 5.316672, 0.0), + rotation: (-0.21401538, 0.31161797, 0.67592126, 0.6326311), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::LightDesc": ( + kind: Directional, + color: ( + r: 1.0, + g: 0.95, + b: 0.85, + a: 1.0, + ), + intensity: 100000.0, + range: 0.0, + shadows: true, + ), + "shared::components::ActorKind": Light, + }, + ), + }, +) \ No newline at end of file diff --git a/assets/levels/rendering_showcase.scn.ron b/assets/levels/rendering_showcase.scn.ron new file mode 100644 index 0000000..4f16031 --- /dev/null +++ b/assets/levels/rendering_showcase.scn.ron @@ -0,0 +1,74 @@ +(schema_version: 2, + resources: {}, + entities: { + 4294968001: ( + components: { + "bevy_ecs::name::Name": "Fog Volume", + "bevy_transform::components::transform::Transform": ( + translation: (0.0, 2.0, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::ActorKind": PostProcessVolume, + "shared::components::PostProcessVolumeDesc": ( + half_extents: (6.0, 3.0, 6.0), + priority: 0, + blend_distance: 2.0, + overrides: ( + fog_density: Some(0.004), + fog_color: Some((r: 0.5, g: 0.55, b: 0.65, a: 1.0)), + ), + fullscreen_effect: None, + profile: None, + label: Some("Foggy courtyard"), + ), + }, + ), + 4294968002: ( + components: { + "bevy_ecs::name::Name": "Dark Exposure Volume", + "bevy_transform::components::transform::Transform": ( + translation: (14.0, 2.0, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::ActorKind": PostProcessVolume, + "shared::components::PostProcessVolumeDesc": ( + half_extents: (4.0, 2.5, 4.0), + priority: 5, + blend_distance: 1.5, + overrides: ( + exposure_ev100: Some(9.5), + bloom: Some(false), + ), + fullscreen_effect: None, + profile: Some("assets/rendering_profiles/cave_dark.ron"), + label: Some("Cave mouth"), + ), + }, + ), + 4294968003: ( + components: { + "bevy_ecs::name::Name": "Vignette FX Volume", + "bevy_transform::components::transform::Transform": ( + translation: (-12.0, 2.0, 0.0), + rotation: (0.0, 0.0, 0.0, 1.0), + scale: (1.0, 1.0, 1.0), + ), + "shared::components::LevelObject": (), + "shared::components::ActorKind": PostProcessVolume, + "shared::components::PostProcessVolumeDesc": ( + half_extents: (5.0, 2.0, 5.0), + priority: 2, + blend_distance: 2.0, + overrides: (), + fullscreen_effect: Some("assets/post_fx/vignette.ron"), + profile: None, + label: Some("Vignette demo"), + ), + }, + ), + }, +) diff --git a/assets/materials/.gitkeep b/assets/materials/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/materials/concrete.ron b/assets/materials/concrete.ron new file mode 100644 index 0000000..091f375 --- /dev/null +++ b/assets/materials/concrete.ron @@ -0,0 +1,12 @@ +( + label: "Concrete", + material: ( + base_color: (r: 0.55, g: 0.54, b: 0.52, a: 1.0), + metallic: 0.0, + roughness: 0.85, + base_color_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), +) diff --git a/assets/materials/emissive_panel.ron b/assets/materials/emissive_panel.ron new file mode 100644 index 0000000..449e3f8 --- /dev/null +++ b/assets/materials/emissive_panel.ron @@ -0,0 +1,15 @@ +( + label: "Emissive Panel", + material: ( + base_color: (r: 0.15, g: 0.22, b: 0.28, a: 1.0), + metallic: 0.0, + roughness: 0.35, + emissive_color: (r: 0.55, g: 0.85, b: 1.0, a: 1.0), + emissive_intensity: 2500.0, + base_color_texture: None, + emissive_texture: None, + normal_map_texture: None, + metallic_roughness_texture: None, + material_asset_path: None, + ), +) diff --git a/assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron b/assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron new file mode 100644 index 0000000..e9fc995 --- /dev/null +++ b/assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron @@ -0,0 +1,82 @@ +( + schema_version: 1, + asset_id: "582cd326-c32d-49da-9424-ae38b3d60648", + label: "Dumpster", + source: ( + path: "assets/models/Dumpster.fbx", + format: "fbx", + fingerprint: ( + byte_len: 50940, + modified_unix_secs: 1780338936, + ), + dependencies: [], + ), + import: ( + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SourceHierarchy, + material_policy: AuthoringOverride, + ), + metadata: ( + mesh_count: 3, + material_count: 2, + node_count: 4, + animation_count: 0, + skin_count: 0, + light_count: 0, + camera_count: 0, + ), + parts: [ + ( + id: "mesh:mesh1000", + name: "Dumpster / Material 0", + mesh_label: "Mesh1000", + material_id: Some("material:material0"), + material_slot_name: "Dumpster", + material_label: Some("Material0"), + local_transform: ( + translation: (8.342552, 0.06418517, -6.7325354), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node1"), + source_mesh: Some("Mesh1"), + source_material: Some("Dumpster"), + ), + ( + id: "mesh:mesh2000", + name: "Dumpster Lid / Material 0", + mesh_label: "Mesh2000", + material_id: Some("material:material1"), + material_slot_name: "Dumpster Lid", + material_label: Some("Material1"), + local_transform: ( + translation: (7.362827, 2.5433145, -7.784438), + rotation: (0.70710677, 0.000000000082318055, -0.00000000008231806, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node2"), + source_mesh: Some("Mesh2"), + source_material: Some("Dumpster Lid"), + ), + ( + id: "mesh:mesh3000", + name: "Dumpster Lid_02 / Material 0", + mesh_label: "Mesh3000", + material_id: Some("material:material1"), + material_slot_name: "Dumpster Lid", + material_label: Some("Material1"), + local_transform: ( + translation: (9.351767, 2.5433145, -7.784438), + rotation: (0.70710677, 0.000000000082318055, -0.00000000008231806, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node3"), + source_mesh: Some("Mesh3"), + source_material: Some("Dumpster Lid"), + ), + ], + warnings: [], +) \ No newline at end of file diff --git a/assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron b/assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron new file mode 100644 index 0000000..9d9b032 --- /dev/null +++ b/assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron @@ -0,0 +1,770 @@ +( + schema_version: 1, + asset_id: "b8b43f26-0eee-4ee8-9e16-53aedbc69205", + label: "Room3", + source: ( + path: "assets/models/Room3.fbx", + format: "fbx", + fingerprint: ( + byte_len: 159836, + modified_unix_secs: 1780196194, + ), + dependencies: [], + ), + import: ( + scale: 1.7, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SingleActor, + material_policy: AuthoringOverride, + ), + metadata: ( + mesh_count: 23, + material_count: 11, + node_count: 24, + animation_count: 0, + skin_count: 0, + light_count: 0, + camera_count: 0, + ), + parts: [ + ( + id: "mesh:mesh1000", + name: "Windows Small.000 / Material 0", + mesh_label: "Mesh1000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (2.9808052, 1.9115452, -0.40523553), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248338, 1.0), + ), + source_node: Some("Node1"), + source_mesh: Some("Mesh1"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh10000", + name: "Window Right Up.000 / Material 0", + mesh_label: "Mesh10000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (-0.97121567, 1.8132615, 3.0487669), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node10"), + source_mesh: Some("Mesh10"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh10001", + name: "Window Right Up.000 / Material 1", + mesh_label: "Mesh10001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (-0.97121567, 1.8132615, 3.0487669), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node10"), + source_mesh: Some("Mesh10"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh1001", + name: "Windows Small.000 / Material 1", + mesh_label: "Mesh1001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (2.9808052, 1.9115452, -0.40523553), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248338, 1.0), + ), + source_node: Some("Node1"), + source_mesh: Some("Mesh1"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh11000", + name: "Window Right Low.000 / Material 0", + mesh_label: "Mesh11000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (-0.9734232, 1.4140832, 3.0185344), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node11"), + source_mesh: Some("Mesh11"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh11001", + name: "Window Right Low.000 / Material 1", + mesh_label: "Mesh11001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (-0.9734232, 1.4140832, 3.0185344), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node11"), + source_mesh: Some("Mesh11"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh12000", + name: "Window Left Up.000 / Material 0", + mesh_label: "Mesh12000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (1.583923, 1.8132616, 3.0487669), + rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node12"), + source_mesh: Some("Mesh12"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh12001", + name: "Window Left Up.000 / Material 1", + mesh_label: "Mesh12001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (1.583923, 1.8132616, 3.0487669), + rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node12"), + source_mesh: Some("Mesh12"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh13000", + name: "Window Left Low.000 / Material 0", + mesh_label: "Mesh13000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (1.5816854, 1.414083, 3.0185344), + rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node13"), + source_mesh: Some("Mesh13"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh13001", + name: "Window Left Low.000 / Material 1", + mesh_label: "Mesh13001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (1.5816854, 1.414083, 3.0185344), + rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067), + scale: (1.2387476, 0.9248341, 1.0), + ), + source_node: Some("Node13"), + source_mesh: Some("Mesh13"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh14000", + name: "Window Center Low.000 / Material 0", + mesh_label: "Mesh14000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (0.29577515, 1.4927816, 3.0203688), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248343, 1.0), + ), + source_node: Some("Node14"), + source_mesh: Some("Mesh14"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh14001", + name: "Window Center Low.000 / Material 1", + mesh_label: "Mesh14001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (0.29577515, 1.4927816, 3.0203688), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248343, 1.0), + ), + source_node: Some("Node14"), + source_mesh: Some("Mesh14"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh15000", + name: "Window Cente Up.000 / Material 0", + mesh_label: "Mesh15000", + material_id: Some("material:material0"), + material_slot_name: "Window Frame Metal.002", + material_label: Some("Material0"), + local_transform: ( + translation: (0.2986794, 1.7560294, 3.0472102), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248343, 1.0), + ), + source_node: Some("Node15"), + source_mesh: Some("Mesh15"), + source_material: Some("Window Frame Metal.002"), + ), + ( + id: "mesh:mesh15001", + name: "Window Cente Up.000 / Material 1", + mesh_label: "Mesh15001", + material_id: Some("material:material1"), + material_slot_name: "Window Glass.002", + material_label: Some("Material1"), + local_transform: ( + translation: (0.2986794, 1.7560294, 3.0472102), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248343, 1.0), + ), + source_node: Some("Node15"), + source_mesh: Some("Mesh15"), + source_material: Some("Window Glass.002"), + ), + ( + id: "mesh:mesh16000", + name: "Lights.Potlight.004 / Material 0", + mesh_label: "Mesh16000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (1.9999996, 2.9877658, 1.1368884), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node16"), + source_mesh: Some("Mesh16"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh16002", + name: "Lights.Potlight.004 / Material 2", + mesh_label: "Mesh16002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (1.9999996, 2.9877658, 1.1368884), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node16"), + source_mesh: Some("Mesh16"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh17000", + name: "Lights.Potlight.003 / Material 0", + mesh_label: "Mesh17000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (1.9999996, 2.9877653, -1.8631115), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node17"), + source_mesh: Some("Mesh17"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh17002", + name: "Lights.Potlight.003 / Material 2", + mesh_label: "Mesh17002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (1.9999996, 2.9877653, -1.8631115), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node17"), + source_mesh: Some("Mesh17"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh18000", + name: "Lights.Potlight.002 / Material 0", + mesh_label: "Mesh18000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (-1.8000002, 2.9877653, -1.8631115), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node18"), + source_mesh: Some("Mesh18"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh18002", + name: "Lights.Potlight.002 / Material 2", + mesh_label: "Mesh18002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (-1.8000002, 2.9877653, -1.8631115), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node18"), + source_mesh: Some("Mesh18"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh19000", + name: "Lights.Potlight.001 / Material 0", + mesh_label: "Mesh19000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (-1.8000002, 2.9877658, 1.1368884), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node19"), + source_mesh: Some("Mesh19"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh19002", + name: "Lights.Potlight.001 / Material 2", + mesh_label: "Mesh19002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (-1.8000002, 2.9877658, 1.1368884), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node19"), + source_mesh: Some("Mesh19"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh2000", + name: "Window Right.000 / Material 0", + mesh_label: "Mesh2000", + material_id: Some("material:defaultmaterial"), + material_slot_name: "Default Material", + material_label: Some("DefaultMaterial"), + local_transform: ( + translation: (-0.9659626, 1.613703, 3.0355246), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248338, 1.0), + ), + source_node: Some("Node2"), + source_mesh: Some("Mesh2"), + source_material: Some("Default Material"), + ), + ( + id: "mesh:mesh20000", + name: "Lights.Center.003 / Material 0", + mesh_label: "Mesh20000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (0.24156909, 2.792474, -0.63835967), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node20"), + source_mesh: Some("Mesh20"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh20001", + name: "Lights.Center.003 / Material 1", + mesh_label: "Mesh20001", + material_id: Some("material:material8"), + material_slot_name: "Matte Black.002", + material_label: Some("Material8"), + local_transform: ( + translation: (0.24156909, 2.792474, -0.63835967), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node20"), + source_mesh: Some("Mesh20"), + source_material: Some("Matte Black.002"), + ), + ( + id: "mesh:mesh20002", + name: "Lights.Center.003 / Material 2", + mesh_label: "Mesh20002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (0.24156909, 2.792474, -0.63835967), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node20"), + source_mesh: Some("Mesh20"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh21000", + name: "Lights.Center.002 / Material 0", + mesh_label: "Mesh21000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (0.25059184, 2.7924743, 0.36630905), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node21"), + source_mesh: Some("Mesh21"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh21001", + name: "Lights.Center.002 / Material 1", + mesh_label: "Mesh21001", + material_id: Some("material:material8"), + material_slot_name: "Matte Black.002", + material_label: Some("Material8"), + local_transform: ( + translation: (0.25059184, 2.7924743, 0.36630905), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node21"), + source_mesh: Some("Mesh21"), + source_material: Some("Matte Black.002"), + ), + ( + id: "mesh:mesh21002", + name: "Lights.Center.002 / Material 2", + mesh_label: "Mesh21002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (0.25059184, 2.7924743, 0.36630905), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node21"), + source_mesh: Some("Mesh21"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh22000", + name: "Lights.Center.001 / Material 0", + mesh_label: "Mesh22000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (0.21980214, 2.7924743, -0.15153252), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node22"), + source_mesh: Some("Mesh22"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh22001", + name: "Lights.Center.001 / Material 1", + mesh_label: "Mesh22001", + material_id: Some("material:material8"), + material_slot_name: "Matte Black.002", + material_label: Some("Material8"), + local_transform: ( + translation: (0.21980214, 2.7924743, -0.15153252), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node22"), + source_mesh: Some("Mesh22"), + source_material: Some("Matte Black.002"), + ), + ( + id: "mesh:mesh22002", + name: "Lights.Center.001 / Material 2", + mesh_label: "Mesh22002", + material_id: Some("material:material9"), + material_slot_name: "Light.002", + material_label: Some("Material9"), + local_transform: ( + translation: (0.21980214, 2.7924743, -0.15153252), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node22"), + source_mesh: Some("Mesh22"), + source_material: Some("Light.002"), + ), + ( + id: "mesh:mesh23002", + name: "Door_Knob.000 / Material 2", + mesh_label: "Mesh23002", + material_id: Some("material:material10"), + material_slot_name: "Door Knob Gold.002", + material_label: Some("Material10"), + local_transform: ( + translation: (-3.0024662, 1.0415587, -1.6272027), + rotation: (0.70710677, -0.0000000023899738, 0.00000000007193627, -0.7071067), + scale: (0.07870353, 0.45364702, 0.45364702), + ), + source_node: Some("Node23"), + source_mesh: Some("Mesh23"), + source_material: Some("Door Knob Gold.002"), + ), + ( + id: "mesh:mesh3000", + name: "Window Left.000 / Material 0", + mesh_label: "Mesh3000", + material_id: Some("material:defaultmaterial"), + material_slot_name: "Default Material", + material_label: Some("DefaultMaterial"), + local_transform: ( + translation: (1.5851622, 1.613703, 3.0355246), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248338, 1.0), + ), + source_node: Some("Node3"), + source_mesh: Some("Mesh3"), + source_material: Some("Default Material"), + ), + ( + id: "mesh:mesh4000", + name: "Window Frames.000 / Material 0", + mesh_label: "Mesh4000", + material_id: Some("material:material2"), + material_slot_name: "Painted Wooden Frame.002", + material_label: Some("Material2"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.5000001, -0.49999994, -0.5, -0.50000006), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node4"), + source_mesh: Some("Mesh4"), + source_material: Some("Painted Wooden Frame.002"), + ), + ( + id: "mesh:mesh5000", + name: "Window Center.000 / Material 0", + mesh_label: "Mesh5000", + material_id: Some("material:defaultmaterial"), + material_slot_name: "Default Material", + material_label: Some("DefaultMaterial"), + local_transform: ( + translation: (0.3107944, 1.613703, 3.0355246), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2387476, 0.9248338, 1.0), + ), + source_node: Some("Node5"), + source_mesh: Some("Mesh5"), + source_material: Some("Default Material"), + ), + ( + id: "mesh:mesh6000", + name: "Room.000 / Material 0", + mesh_label: "Mesh6000", + material_id: Some("material:material2"), + material_slot_name: "Painted Wooden Frame.002", + material_label: Some("Material2"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node6"), + source_mesh: Some("Mesh6"), + source_material: Some("Painted Wooden Frame.002"), + ), + ( + id: "mesh:mesh6001", + name: "Room.000 / Material 1", + mesh_label: "Mesh6001", + material_id: Some("material:material3"), + material_slot_name: "Ceiling.002", + material_label: Some("Material3"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node6"), + source_mesh: Some("Mesh6"), + source_material: Some("Ceiling.002"), + ), + ( + id: "mesh:mesh6002", + name: "Room.000 / Material 2", + mesh_label: "Mesh6002", + material_id: Some("material:material4"), + material_slot_name: "Hardwood Floor.002", + material_label: Some("Material4"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node6"), + source_mesh: Some("Mesh6"), + source_material: Some("Hardwood Floor.002"), + ), + ( + id: "mesh:mesh6003", + name: "Room.000 / Material 3", + mesh_label: "Mesh6003", + material_id: Some("material:material5"), + material_slot_name: "Painted Wall.002", + material_label: Some("Material5"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node6"), + source_mesh: Some("Mesh6"), + source_material: Some("Painted Wall.002"), + ), + ( + id: "mesh:mesh6004", + name: "Room.000 / Material 4", + mesh_label: "Mesh6004", + material_id: Some("material:material6"), + material_slot_name: "Brick Accent Wall.002", + material_label: Some("Material6"), + local_transform: ( + translation: (0.0, 0.0027079582, 0.0), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node6"), + source_mesh: Some("Mesh6"), + source_material: Some("Brick Accent Wall.002"), + ), + ( + id: "mesh:mesh7000", + name: "Lights.000 / Material 0", + mesh_label: "Mesh7000", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (0.24510838, 2.9453187, -0.14346208), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.0, 1.0, 1.0), + ), + source_node: Some("Node7"), + source_mesh: Some("Mesh7"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh8000", + name: "Door Frame.000 / Material 0", + mesh_label: "Mesh8000", + material_id: Some("material:material2"), + material_slot_name: "Painted Wooden Frame.002", + material_label: Some("Material2"), + local_transform: ( + translation: (-3.0060542, 0.0027079582, -2.1066966), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (1.2365791, 1.0, 1.0), + ), + source_node: Some("Node8"), + source_mesh: Some("Mesh8"), + source_material: Some("Painted Wooden Frame.002"), + ), + ( + id: "mesh:mesh9000", + name: "Door.000 / Material 0", + mesh_label: "Mesh9000", + material_id: Some("material:material2"), + material_slot_name: "Painted Wooden Frame.002", + material_label: Some("Material2"), + local_transform: ( + translation: (-2.9729986, 0.22508162, -2.6792114), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (0.07870354, 0.45364702, 0.45364702), + ), + source_node: Some("Node9"), + source_mesh: Some("Mesh9"), + source_material: Some("Painted Wooden Frame.002"), + ), + ( + id: "mesh:mesh9001", + name: "Door.000 / Material 1", + mesh_label: "Mesh9001", + material_id: Some("material:material7"), + material_slot_name: "Light Metal.002", + material_label: Some("Material7"), + local_transform: ( + translation: (-2.9729986, 0.22508162, -2.6792114), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (0.07870354, 0.45364702, 0.45364702), + ), + source_node: Some("Node9"), + source_mesh: Some("Mesh9"), + source_material: Some("Light Metal.002"), + ), + ( + id: "mesh:mesh9002", + name: "Door.000 / Material 2", + mesh_label: "Mesh9002", + material_id: Some("material:material10"), + material_slot_name: "Door Knob Gold.002", + material_label: Some("Material10"), + local_transform: ( + translation: (-2.9729986, 0.22508162, -2.6792114), + rotation: (0.70710677, 0.0, 0.0, -0.7071067), + scale: (0.07870354, 0.45364702, 0.45364702), + ), + source_node: Some("Node9"), + source_mesh: Some("Mesh9"), + source_material: Some("Door Knob Gold.002"), + ), + ], + warnings: [], +) \ No newline at end of file diff --git a/assets/models/.gitkeep b/assets/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/models/Dumpster.fbx b/assets/models/Dumpster.fbx new file mode 100755 index 0000000..451b265 --- /dev/null +++ b/assets/models/Dumpster.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf8b0fb7877299eca36b8d7735ee4716395add8215c9c0760ff43bed2fb8800e +size 50940 diff --git a/assets/models/Room3.fbx b/assets/models/Room3.fbx new file mode 100755 index 0000000..b4103e2 --- /dev/null +++ b/assets/models/Room3.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf7fedc052323c383164d79b7efafce890bf3e7436933b554e7baecf19add196 +size 159836 diff --git a/assets/post_fx/chromatic_aberration.ron b/assets/post_fx/chromatic_aberration.ron new file mode 100644 index 0000000..81dd383 --- /dev/null +++ b/assets/post_fx/chromatic_aberration.ron @@ -0,0 +1,7 @@ +( + label: "Chromatic Aberration", + kind: ChromaticAberration, + shader: "post_fx/chromatic_aberration.wgsl", + intensity: 0.8, + radius: 0.5, +) diff --git a/assets/post_fx/chromatic_aberration.wgsl b/assets/post_fx/chromatic_aberration.wgsl new file mode 100644 index 0000000..1cecdaf --- /dev/null +++ b/assets/post_fx/chromatic_aberration.wgsl @@ -0,0 +1,20 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +struct ChromaticSettings { + intensity: f32, + radius: f32, +} + +@group(0) @binding(2) var settings: ChromaticSettings; +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let offset = settings.intensity * 0.004; + let r = textureSample(screen_texture, texture_sampler, in.uv + vec2(offset, 0.0)).r; + let g = textureSample(screen_texture, texture_sampler, in.uv).g; + let b = textureSample(screen_texture, texture_sampler, in.uv - vec2(offset, 0.0)).b; + let a = textureSample(screen_texture, texture_sampler, in.uv).a; + return vec4(r, g, b, a); +} diff --git a/assets/post_fx/vignette.ron b/assets/post_fx/vignette.ron new file mode 100644 index 0000000..1035fb1 --- /dev/null +++ b/assets/post_fx/vignette.ron @@ -0,0 +1,7 @@ +( + label: "Vignette", + kind: Vignette, + shader: "post_fx/vignette.wgsl", + intensity: 0.65, + radius: 0.75, +) diff --git a/assets/post_fx/vignette.wgsl b/assets/post_fx/vignette.wgsl new file mode 100644 index 0000000..ed81047 --- /dev/null +++ b/assets/post_fx/vignette.wgsl @@ -0,0 +1,19 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +struct VignetteSettings { + intensity: f32, + radius: f32, +} + +@group(0) @binding(2) var settings: VignetteSettings; +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let color = textureSample(screen_texture, texture_sampler, in.uv); + let dist = distance(in.uv, vec2(0.5, 0.5)); + let vignette = smoothstep(settings.radius, settings.radius - 0.35, dist); + let strength = mix(1.0, vignette, settings.intensity); + return vec4(color.rgb * strength, color.a); +} diff --git a/assets/project.ron b/assets/project.ron new file mode 100644 index 0000000..d2ba3c8 --- /dev/null +++ b/assets/project.ron @@ -0,0 +1,58 @@ +( + version: 1, + name: "Bevy FPS Foundation", + default_level: "assets/levels/editor_scene.scn.ron", + asset_roots: [ + "assets/models", + "assets/textures", + "assets/materials", + "assets/levels", + ], + rendering: ( + gi_mode: Auto, + hdr: true, + exposure_mode: Manual, + exposure_ev100: 12.5, + auto_exposure_speed_brighten: 3.0, + auto_exposure_speed_darken: 1.0, + tonemapping_aces: true, + bloom: true, + taa: true, + ssao: true, + atmosphere: true, + fog_color: (0.66, 0.75, 0.86), + fog_density: 0.0012, + ambient_color: (0.6, 0.7, 0.85), + ambient_brightness: 60.0, + sun_illuminance: 100000.0, + shadow_cascades: 4, + shadow_first_cascade: 6.0, + shadow_max_distance: 220.0, + ), + physics: ( + player_radius: 0.4, + player_length: 1.0, + eye_offset_stand: 0.6, + eye_offset_crouch: 0.1, + gravity: 22.0, + walk_speed: 6.0, + run_speed: 9.5, + crouch_speed: 3.0, + jump_speed: 7.5, + ground_accel: 70.0, + air_accel: 16.0, + ground_check_dist: 0.2, + skin: 0.02, + max_slide_iter: 4, + ground_normal_y: 0.5, + coyote_time: 0.12, + jump_buffer: 0.12, + ), + input: ( + yaw_sensitivity: 0.003, + pitch_sensitivity: 0.0022, + editor_fly_speed: 14.0, + editor_look_sensitivity: 0.003, + editor_pan_speed: 0.015, + ), +) diff --git a/assets/rendering_profiles/cave_dark.ron b/assets/rendering_profiles/cave_dark.ron new file mode 100644 index 0000000..952bfb2 --- /dev/null +++ b/assets/rendering_profiles/cave_dark.ron @@ -0,0 +1,9 @@ +( + label: "Cave Dark", + overrides: ( + exposure_ev100: Some(9.0), + fog_density: Some(0.0025), + bloom: Some(false), + ssao: Some(true), + ), +) diff --git a/assets/rendering_profiles/outdoor_haze.ron b/assets/rendering_profiles/outdoor_haze.ron new file mode 100644 index 0000000..5cf1ae4 --- /dev/null +++ b/assets/rendering_profiles/outdoor_haze.ron @@ -0,0 +1,9 @@ +( + label: "Outdoor Haze", + overrides: ( + exposure_ev100: Some(14.5), + fog_density: Some(0.003), + atmosphere: Some(true), + bloom: Some(true), + ), +) diff --git a/assets/shaders/standard_lit.shader.ron b/assets/shaders/standard_lit.shader.ron new file mode 100644 index 0000000..37a2c19 --- /dev/null +++ b/assets/shaders/standard_lit.shader.ron @@ -0,0 +1,18 @@ +( + label: "Standard Lit", + kind: StandardLit, + wgsl_path: None, + parameters: [ + (name: "base_color", display_name: "Base Color", group: "Surface", property_type: Color), + (name: "metallic", display_name: "Metallic", group: "Surface", property_type: Float(min: Some(0.0), max: Some(1.0))), + (name: "roughness", display_name: "Roughness", group: "Surface", property_type: Float(min: Some(0.0), max: Some(1.0))), + (name: "emissive_color", display_name: "Emissive Color", group: "Emission", property_type: Color), + (name: "emissive_intensity", display_name: "Emissive Nits", group: "Emission", property_type: Float(min: Some(0.0), max: Some(20000.0))), + (name: "base_color_texture", display_name: "Base Color Texture", group: "Textures", property_type: Texture), + (name: "normal_map_texture", display_name: "Normal Map", group: "Textures", property_type: Texture), + (name: "metallic_roughness_texture", display_name: "Metallic/Roughness Texture", group: "Textures", property_type: Texture), + (name: "emissive_texture", display_name: "Emissive Texture", group: "Textures", property_type: Texture), + ], + default_values: [], + default_textures: [], +) diff --git a/assets/shaders/unlit.shader.ron b/assets/shaders/unlit.shader.ron new file mode 100644 index 0000000..31d2686 --- /dev/null +++ b/assets/shaders/unlit.shader.ron @@ -0,0 +1,13 @@ +( + label: "Unlit", + kind: Unlit, + wgsl_path: None, + parameters: [ + (name: "base_color", display_name: "Base Color", group: "Surface", property_type: Color), + (name: "base_color_texture", display_name: "Base Color Texture", group: "Textures", property_type: Texture), + (name: "emissive_color", display_name: "Emissive Color", group: "Emission", property_type: Color), + (name: "emissive_intensity", display_name: "Emissive Nits", group: "Emission", property_type: Float(min: Some(0.0), max: Some(20000.0))), + ], + default_values: [], + default_textures: [], +) diff --git a/assets/textures/.gitkeep b/assets/textures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml new file mode 100644 index 0000000..6c8935a --- /dev/null +++ b/crates/editor/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "editor" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "In-process level editor for the Bevy FPS foundation" + +[lib] +name = "editor" +path = "src/lib.rs" + +[[bin]] +name = "editor" +path = "src/bin/main.rs" + +[features] +dev = ["bevy/dynamic_linking", "game/dev"] +hot-reload = ["game/hot-reload", "dep:hot-lib-reloader", "dep:notify"] + +[dependencies] +avian3d.workspace = true +bevy = { workspace = true, default-features = true, features = ["bevy_remote"] } +bevy_solari.workspace = true +bevy_egui.workspace = true +bevy-inspector-egui.workspace = true +egui_dock.workspace = true +egui_phosphor_icons.workspace = true +bevy_ufbx.workspace = true +ufbx = "0.9" +game.workspace = true +game_hot.workspace = true +hot-lib-reloader = { version = "0.8.2", optional = true } +notify = { version = "6.1", optional = true } +protocol.workspace = true +shared.workspace = true +sim.workspace = true +settings.workspace = true +scene.workspace = true +serde.workspace = true +transform-gizmo-bevy.workspace = true +rfd = "0.17.2" +ron = "0.8" +uuid = { version = "1", features = ["v4", "serde"] } +walkdir = "2.5" +gltf = "1.4" diff --git a/crates/editor/assets/icons/01_actor_empty.png b/crates/editor/assets/icons/01_actor_empty.png new file mode 100644 index 0000000..70aed0a --- /dev/null +++ b/crates/editor/assets/icons/01_actor_empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a808eb1a4e1606a4aa80d5e6cb5372d121db5752411ea50e699f3254b76b30 +size 14529 diff --git a/crates/editor/assets/icons/02_actor_generic.png b/crates/editor/assets/icons/02_actor_generic.png new file mode 100644 index 0000000..5395b62 --- /dev/null +++ b/crates/editor/assets/icons/02_actor_generic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:709fb3633f9d3543980c5de1d33f7e4abd03c77a60c2e064acb2cf6493fdc612 +size 17207 diff --git a/crates/editor/assets/icons/03_actor_missing.png b/crates/editor/assets/icons/03_actor_missing.png new file mode 100644 index 0000000..7d06368 --- /dev/null +++ b/crates/editor/assets/icons/03_actor_missing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee96aa29acf5b3fb6c7c022d7457e4e9ea2247f024200af086e3d94a1e9269cc +size 24902 diff --git a/crates/editor/assets/icons/04_actor_warning.png b/crates/editor/assets/icons/04_actor_warning.png new file mode 100644 index 0000000..c188743 --- /dev/null +++ b/crates/editor/assets/icons/04_actor_warning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:728ab8263d8034df70c2a16e9f392f9314fb1500c04caf31bc653118a38c3dc2 +size 25254 diff --git a/crates/editor/assets/icons/05_render_mesh.png b/crates/editor/assets/icons/05_render_mesh.png new file mode 100644 index 0000000..0bf964c --- /dev/null +++ b/crates/editor/assets/icons/05_render_mesh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce34d5a792897a4f7d7e01344ae9ccf000771315859a014f2598634de55b4513 +size 28761 diff --git a/crates/editor/assets/icons/06_render_decal.png b/crates/editor/assets/icons/06_render_decal.png new file mode 100644 index 0000000..f2e7f6d --- /dev/null +++ b/crates/editor/assets/icons/06_render_decal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e599597d41de345906552fdc0a961e2555457b426db5a86c935b89edd33b773a +size 26196 diff --git a/crates/editor/assets/icons/07_render_particle.png b/crates/editor/assets/icons/07_render_particle.png new file mode 100644 index 0000000..b51d062 --- /dev/null +++ b/crates/editor/assets/icons/07_render_particle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1102fca126c2ae8a5d5d6cb2de1aa610e6cf85dcda28425b75049551417ade65 +size 29358 diff --git a/crates/editor/assets/icons/08_render_billboard.png b/crates/editor/assets/icons/08_render_billboard.png new file mode 100644 index 0000000..0b68483 --- /dev/null +++ b/crates/editor/assets/icons/08_render_billboard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f02ee1b147cd235886ab3faceecdcd006be0fb0e34a0023f791179856372649 +size 17428 diff --git a/crates/editor/assets/icons/09_light_point.png b/crates/editor/assets/icons/09_light_point.png new file mode 100644 index 0000000..121f1d8 --- /dev/null +++ b/crates/editor/assets/icons/09_light_point.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:713708c8477a2cd1acc59411300653283a9f7e2b30340dc2d5a9153514d72fa0 +size 21537 diff --git a/crates/editor/assets/icons/10_light_spot.png b/crates/editor/assets/icons/10_light_spot.png new file mode 100644 index 0000000..95ad3c6 --- /dev/null +++ b/crates/editor/assets/icons/10_light_spot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a65ca7e726fb190dc7b2c76475b5e5cccf80c8f36c822e4be39111b47bbafc12 +size 23494 diff --git a/crates/editor/assets/icons/11_light_directional.png b/crates/editor/assets/icons/11_light_directional.png new file mode 100644 index 0000000..b727d47 --- /dev/null +++ b/crates/editor/assets/icons/11_light_directional.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3871a94db5f93e72708256edc087000cbd23cc1001eb761b37def90a3035a292 +size 20483 diff --git a/crates/editor/assets/icons/12_light_area.png b/crates/editor/assets/icons/12_light_area.png new file mode 100644 index 0000000..5ff4f77 --- /dev/null +++ b/crates/editor/assets/icons/12_light_area.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38e6a8d4a669b4b91a04afdd28abfaaf0bd06a8d563bbfddf902d72846b18042 +size 19074 diff --git a/crates/editor/assets/icons/13_camera_main.png b/crates/editor/assets/icons/13_camera_main.png new file mode 100644 index 0000000..d368e37 --- /dev/null +++ b/crates/editor/assets/icons/13_camera_main.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:566c39c65a0c03791cdec3ef9c135f4c9cfb7d3b13fb1cca62bcdc262135686a +size 18295 diff --git a/crates/editor/assets/icons/14_camera_capture.png b/crates/editor/assets/icons/14_camera_capture.png new file mode 100644 index 0000000..0cfa77d --- /dev/null +++ b/crates/editor/assets/icons/14_camera_capture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1440ed01e4415ddc1782894aff05eba03b4925297b34b898aee8584ac0ea32fd +size 21378 diff --git a/crates/editor/assets/icons/15_audio_source.png b/crates/editor/assets/icons/15_audio_source.png new file mode 100644 index 0000000..11f59d8 --- /dev/null +++ b/crates/editor/assets/icons/15_audio_source.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69cb4d4435b85ab438b24e2cbb3dc4bbe918368c2db42b3943d24efb38dd09a2 +size 22095 diff --git a/crates/editor/assets/icons/16_audio_listener.png b/crates/editor/assets/icons/16_audio_listener.png new file mode 100644 index 0000000..f324af0 --- /dev/null +++ b/crates/editor/assets/icons/16_audio_listener.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd4201ca54bf3bf21744303361d2033bb707c43c96848ccad63646b54381ff75 +size 24658 diff --git a/crates/editor/assets/icons/17_physics_collider.png b/crates/editor/assets/icons/17_physics_collider.png new file mode 100644 index 0000000..fe4ad63 --- /dev/null +++ b/crates/editor/assets/icons/17_physics_collider.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d61aed35f37291edca627c58d58ee4793dfc1b030713493d16771dfb95110e +size 24226 diff --git a/crates/editor/assets/icons/18_physics_rigidbody.png b/crates/editor/assets/icons/18_physics_rigidbody.png new file mode 100644 index 0000000..2f4b083 --- /dev/null +++ b/crates/editor/assets/icons/18_physics_rigidbody.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3118b8a8f46f0933dbe1b5ff0fe0122ef4d9390750406fb3bf561a31f7be2436 +size 26859 diff --git a/crates/editor/assets/icons/19_physics_trigger.png b/crates/editor/assets/icons/19_physics_trigger.png new file mode 100644 index 0000000..b9049c5 --- /dev/null +++ b/crates/editor/assets/icons/19_physics_trigger.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b70d883cfb28d44ee2c4184d0575dfb4df15bc8eb632208b58eb9507934f46ec +size 27280 diff --git a/crates/editor/assets/icons/20_physics_joint.png b/crates/editor/assets/icons/20_physics_joint.png new file mode 100644 index 0000000..d3d45fc --- /dev/null +++ b/crates/editor/assets/icons/20_physics_joint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43dc03ec50a4e898578f827eb8013bc9f3a0f37fca722ca8525060200d023343 +size 19968 diff --git a/crates/editor/assets/icons/21_game_spawn.png b/crates/editor/assets/icons/21_game_spawn.png new file mode 100644 index 0000000..50434b9 --- /dev/null +++ b/crates/editor/assets/icons/21_game_spawn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f1800e6e51f6f18736bfba92d33312fe234d4437f11b61cbe04a3499726c2d7 +size 26020 diff --git a/crates/editor/assets/icons/22_game_trigger.png b/crates/editor/assets/icons/22_game_trigger.png new file mode 100644 index 0000000..0f7515c --- /dev/null +++ b/crates/editor/assets/icons/22_game_trigger.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9457ad9e47b331b94a8de6328d7a412fb98107764b838d051fa913d8f999e9c2 +size 24520 diff --git a/crates/editor/assets/icons/23_game_marker.png b/crates/editor/assets/icons/23_game_marker.png new file mode 100644 index 0000000..69c36ff --- /dev/null +++ b/crates/editor/assets/icons/23_game_marker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:486ea174d92d687a8d5aad1a75ba7a400031041c8fadb7fab5a475d04ee6c278 +size 21519 diff --git a/crates/editor/assets/icons/24_game_interactable.png b/crates/editor/assets/icons/24_game_interactable.png new file mode 100644 index 0000000..5722174 --- /dev/null +++ b/crates/editor/assets/icons/24_game_interactable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d72fb229721e96d2ded58b82bda4ce7fc40b131e31e2ba40c34a20fac70dacd0 +size 24725 diff --git a/crates/editor/assets/icons/25_nav_waypoint.png b/crates/editor/assets/icons/25_nav_waypoint.png new file mode 100644 index 0000000..99ebf72 --- /dev/null +++ b/crates/editor/assets/icons/25_nav_waypoint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2acd86b27a2aa042ab688986548c2d4b63873f14dd52b031897d9648a806766 +size 23280 diff --git a/crates/editor/assets/icons/26_nav_link.png b/crates/editor/assets/icons/26_nav_link.png new file mode 100644 index 0000000..2de2b2a --- /dev/null +++ b/crates/editor/assets/icons/26_nav_link.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768716209a35916b45ee4a674a2198c9b3afe24b730659b4213ecba4097198cf +size 13906 diff --git a/crates/editor/assets/icons/27_nav_region.png b/crates/editor/assets/icons/27_nav_region.png new file mode 100644 index 0000000..6980310 --- /dev/null +++ b/crates/editor/assets/icons/27_nav_region.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e54d4e9267baae5ca9435ceae1b1f21c5ce2705be52340271bbee4903a2f4c +size 37408 diff --git a/crates/editor/assets/icons/28_volume_generic.png b/crates/editor/assets/icons/28_volume_generic.png new file mode 100644 index 0000000..1dafe18 --- /dev/null +++ b/crates/editor/assets/icons/28_volume_generic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1f1a0449428144c0e8d1db287144c5d9317d91f166132ee02729377b494bba8 +size 25938 diff --git a/crates/editor/assets/icons/29_volume_post_process.png b/crates/editor/assets/icons/29_volume_post_process.png new file mode 100644 index 0000000..fce2e74 --- /dev/null +++ b/crates/editor/assets/icons/29_volume_post_process.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1cf5a3553c53b3b821a707951bcef412a659e0c8409ef73e5eb79d5b5512df2 +size 35406 diff --git a/crates/editor/assets/icons/30_environment_wind.png b/crates/editor/assets/icons/30_environment_wind.png new file mode 100644 index 0000000..d81d0a4 --- /dev/null +++ b/crates/editor/assets/icons/30_environment_wind.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2384245f575198e83c01830fbf9a367adaa4d646b9e13143749215592725af70 +size 20757 diff --git a/crates/editor/assets/icons/31_environment_sky.png b/crates/editor/assets/icons/31_environment_sky.png new file mode 100644 index 0000000..c7f17ce --- /dev/null +++ b/crates/editor/assets/icons/31_environment_sky.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b1462cd863dfa259f249b7cca1d0a79d3f898132a0882d56d95e2ebd0ee9c2 +size 32352 diff --git a/crates/editor/assets/icons/32_debug_note.png b/crates/editor/assets/icons/32_debug_note.png new file mode 100644 index 0000000..58a2adf --- /dev/null +++ b/crates/editor/assets/icons/32_debug_note.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29978300afd7f30fbd11337b92ad07b25472c4ce7b881be79673cc62faa93b99 +size 16544 diff --git a/crates/editor/assets/shaders/selection_outline.wgsl b/crates/editor/assets/shaders/selection_outline.wgsl new file mode 100644 index 0000000..8e16af8 --- /dev/null +++ b/crates/editor/assets/shaders/selection_outline.wgsl @@ -0,0 +1,28 @@ +#import bevy_pbr::{ + forward_io::VertexOutput, + mesh_view_bindings::view, +} + +struct OutlineMaterial { + color: vec4, + rim_power: f32, + rim_mix: f32, + pass_kind: u32, +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) +var material: OutlineMaterial; + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + let world_normal = normalize(in.world_normal); + let view_dir = normalize(view.world_position.xyz - in.world_position.xyz); + let fresnel = pow(1.0 - saturate(dot(world_normal, view_dir)), material.rim_power); + + var alpha = fresnel * material.rim_mix; + if material.pass_kind == 1u { + alpha *= 0.8; + } + + return vec4(material.color.rgb, material.color.a * saturate(alpha)); +} diff --git a/crates/editor/src/assets/asset_db.rs b/crates/editor/src/assets/asset_db.rs new file mode 100644 index 0000000..bd8800b --- /dev/null +++ b/crates/editor/src/assets/asset_db.rs @@ -0,0 +1,257 @@ +//! Project asset registry (stable IDs + import metadata). Phase 5 foundation. + +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use uuid::Uuid; + +/// Stable asset identity for dependency tracking and prefab references. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct AssetId(pub Uuid); + +impl AssetId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } + + pub fn as_string(&self) -> String { + self.0.to_string() + } +} + +impl Default for AssetId { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum ModelPlacementMode { + StaticAsset, + SceneInstance, +} + +impl Default for ModelPlacementMode { + fn default() -> Self { + Self::StaticAsset + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum ModelHierarchyMode { + SingleActor, + SourceHierarchy, +} + +impl Default for ModelHierarchyMode { + fn default() -> Self { + Self::SingleActor + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum MaterialImportPolicy { + SourceMaterials, + AuthoringOverride, +} + +impl Default for MaterialImportPolicy { + fn default() -> Self { + Self::SourceMaterials + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ImportSettings { + pub scale: f32, + pub generate_collider: bool, + pub lod0_only: bool, + #[serde(default)] + pub placement_mode: ModelPlacementMode, + #[serde(default)] + pub hierarchy_mode: ModelHierarchyMode, + #[serde(default)] + pub material_policy: MaterialImportPolicy, + #[serde(default)] + pub static_mesh_manifest_path: Option, +} + +impl Default for ImportSettings { + fn default() -> Self { + Self { + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: ModelPlacementMode::default(), + hierarchy_mode: ModelHierarchyMode::default(), + material_policy: MaterialImportPolicy::default(), + static_mesh_manifest_path: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AssetRecord { + pub id: AssetId, + pub path: String, + pub label: String, + pub kind_tag: String, + #[serde(default)] + pub import_settings: ImportSettings, + #[serde(default)] + pub dependencies: Vec, +} + +#[derive(Resource, Debug, Default)] +pub struct AssetRegistry { + pub records: Vec, + pub index_dirty: bool, +} + +pub struct AssetDbPlugin; + +impl Plugin for AssetDbPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(load_registry()) + .add_systems(Update, sync_registry_from_browser); + } +} + +fn sync_registry_from_browser( + assets: Res, + mut registry: ResMut, +) { + if !assets.is_changed() { + return; + } + + let mut existing: HashMap = registry + .records + .drain(..) + .map(|record| (record.path.clone(), record)) + .collect(); + + let mut next_records = Vec::new(); + let mut seen_paths = HashMap::new(); + + for asset in &assets.assets { + let Some(path) = asset.path.clone() else { + continue; + }; + if path.is_empty() || seen_paths.contains_key(&path) { + continue; + } + seen_paths.insert(path.clone(), ()); + + let kind_tag = format!("{:?}", asset.kind); + let mut record = if let Some(mut prior) = existing.remove(&path) { + prior.label = asset.label.clone(); + prior.kind_tag = kind_tag; + prior + } else { + AssetRecord { + id: AssetId::new(), + path: path.clone(), + label: asset.label.clone(), + kind_tag, + import_settings: ImportSettings::default(), + dependencies: Vec::new(), + } + }; + if record.kind_tag == "Model" { + if let Err(error) = super::static_mesh::refresh_static_mesh_artifact(&mut record) { + warn!( + "Static mesh processor failed for {} (asset id {}): {error}", + record.path, + record.id.as_string() + ); + } + } + next_records.push(record); + } + + let changed = registry.records != next_records; + registry.records = next_records; + if changed { + registry.index_dirty = true; + } + + if registry.index_dirty { + if let Err(error) = save_registry(®istry) { + warn!("Asset registry save failed: {error}"); + } else { + registry.index_dirty = false; + } + } +} + +pub fn find_asset_by_path(registry: &AssetRegistry, path: &str) -> Option { + registry + .records + .iter() + .find(|record| record.path == path) + .cloned() +} + +pub fn find_asset_by_id(registry: &AssetRegistry, id: &str) -> Option { + registry + .records + .iter() + .find(|record| record.id.as_string() == id) + .cloned() +} + +pub fn find_asset_mut_by_path<'a>( + registry: &'a mut AssetRegistry, + path: &str, +) -> Option<&'a mut AssetRecord> { + registry + .records + .iter_mut() + .find(|record| record.path == path) +} + +pub fn update_import_settings( + registry: &mut AssetRegistry, + path: &str, + settings: ImportSettings, +) -> bool { + let Some(record) = registry + .records + .iter_mut() + .find(|record| record.path == path) + else { + return false; + }; + record.import_settings = settings; + registry.index_dirty = true; + true +} + +pub fn registry_index_path() -> &'static str { + "assets/.index/registry.ron" +} + +pub fn save_registry(registry: &AssetRegistry) -> Result<(), String> { + let path = registry_index_path(); + if let Some(parent) = std::path::Path::new(path).parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let text = ron::ser::to_string_pretty(®istry.records, ron::ser::PrettyConfig::default()) + .map_err(|e| e.to_string())?; + std::fs::write(path, text).map_err(|e| e.to_string()) +} + +pub fn load_registry() -> AssetRegistry { + let path = registry_index_path(); + match std::fs::read_to_string(path) { + Ok(text) => { + let records: Vec = ron::from_str(&text).unwrap_or_default(); + AssetRegistry { + records, + index_dirty: false, + } + } + Err(_) => AssetRegistry::default(), + } +} diff --git a/crates/editor/src/assets/catalog.rs b/crates/editor/src/assets/catalog.rs new file mode 100644 index 0000000..3666ed9 --- /dev/null +++ b/crates/editor/src/assets/catalog.rs @@ -0,0 +1,711 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use bevy::prelude::*; +use settings::ProjectSettings; +use shared::{ + ActorKind, AuthoringLightKind, ColliderDesc, ColorDesc, EditorVisibility, LightDesc, + MaterialDesc, ModelRef, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, + PrimitiveShape, RigidBodyDesc, +}; +use walkdir::WalkDir; + +use crate::asset_db::{ + find_asset_by_path, find_asset_mut_by_path, AssetRegistry, ModelPlacementMode, +}; +use crate::assets::static_mesh::{ + load_static_mesh_manifest, refresh_static_mesh_artifact, renderer_from_manifest, +}; +use crate::history::{spawn_with_history, EditorEntitySnapshot}; + +pub const BUILTINS_FOLDER: &str = "__builtins__"; +pub const ASSETS_ROOT: &str = "assets"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AssetSelection { + Builtin(String), + File(String), +} + +impl AssetSelection { + pub fn from_asset(asset: &EditorAsset) -> Self { + match &asset.path { + Some(path) => AssetSelection::File(path.clone()), + None => AssetSelection::Builtin(asset.label.clone()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EditorAssetKind { + Primitive(PrimitiveShape), + Light(AuthoringLightKind), + Model, + Texture, + Material, + Level, + Prefab, + PostProcessVolume, + PostProcessEffect, + RenderingProfile, + ShaderSchema, +} + +#[derive(Debug, Clone)] +pub struct EditorAsset { + pub label: String, + pub path: Option, + pub folder_path: String, + pub kind: EditorAssetKind, +} + +#[derive(Debug, Clone)] +pub struct AssetFolder { + pub path: String, + pub name: String, + pub parent: Option, +} + +#[derive(Resource, Debug)] +pub struct EditorAssets { + pub folders: Vec, + pub assets: Vec, + pub current_folder: String, + pub selected: Option, + pub dragging: Option, + pub status: String, +} + +impl Default for EditorAssets { + fn default() -> Self { + let mut assets = Self { + folders: Vec::new(), + assets: Vec::new(), + current_folder: ASSETS_ROOT.to_string(), + selected: None, + dragging: None, + status: "Assets not scanned yet".to_string(), + }; + assets.refresh(); + assets + } +} + +impl EditorAssets { + pub fn refresh(&mut self) { + self.folders.clear(); + self.assets.clear(); + + self.folders.push(AssetFolder { + path: BUILTINS_FOLDER.to_string(), + name: "Built-ins".to_string(), + parent: None, + }); + + register_folder_tree(&mut self.folders, ASSETS_ROOT); + self.assets.extend(builtin_assets()); + scan_assets_directory(&mut self.assets); + + if !self.folder_exists(&self.current_folder) { + self.current_folder = ASSETS_ROOT.to_string(); + } + + self.selected = self + .selected + .take() + .filter(|selection| self.asset_for_selection(selection).is_some()); + + let folder_count = self.folders.len(); + self.status = format!( + "Scanned {} assets in {} folders", + self.assets.len(), + folder_count + ); + } + + fn folder_exists(&self, path: &str) -> bool { + self.folders.iter().any(|folder| folder.path == path) + } + + pub fn asset_for_selection(&self, selection: &AssetSelection) -> Option<&EditorAsset> { + match selection { + AssetSelection::Builtin(label) => self + .assets + .iter() + .find(|asset| asset.path.is_none() && asset.label == *label), + AssetSelection::File(path) => self + .assets + .iter() + .find(|asset| asset.path.as_deref() == Some(path.as_str())), + } + } + + pub fn selected_asset(&self) -> Option<&EditorAsset> { + self.selected + .as_ref() + .and_then(|selection| self.asset_for_selection(selection)) + } + + pub fn dragging_asset(&self) -> Option<&EditorAsset> { + self.dragging + .as_ref() + .and_then(|selection| self.asset_for_selection(selection)) + } + + pub fn select(&mut self, selection: AssetSelection) { + if let Some(asset) = self.asset_for_selection(&selection).cloned() { + self.selected = Some(selection); + self.status = format!("Selected {}", asset.label); + } + } + + pub fn start_drag(&mut self, selection: AssetSelection) { + if self.asset_for_selection(&selection).is_some() { + self.dragging = Some(selection); + } + } + + pub fn clear_drag(&mut self) { + self.dragging = None; + } +} + +pub struct EditorAssetsPlugin; + +impl Plugin for EditorAssetsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, refresh_assets_on_settings_change); + } +} + +fn refresh_assets_on_settings_change( + settings: Res, + mut assets: ResMut, +) { + if settings.is_changed() { + assets.refresh(); + } +} + +fn builtin_assets() -> Vec { + vec![ + EditorAsset { + label: "Cube".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Primitive(PrimitiveShape::Box), + }, + EditorAsset { + label: "Sphere".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Primitive(PrimitiveShape::Sphere), + }, + EditorAsset { + label: "Ramp".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Primitive(PrimitiveShape::Ramp), + }, + EditorAsset { + label: "Point Light".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Light(AuthoringLightKind::Point), + }, + EditorAsset { + label: "Spot Light".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Light(AuthoringLightKind::Spot), + }, + EditorAsset { + label: "Directional Light".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Light(AuthoringLightKind::Directional), + }, + EditorAsset { + label: "Post Process Volume".to_string(), + path: None, + folder_path: BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::PostProcessVolume, + }, + ] +} + +fn register_folder_tree(folders: &mut Vec, root: &str) { + let normalized = normalize_path(root); + if folders.iter().any(|folder| folder.path == normalized) { + return; + } + + let parent = parent_folder_path(&normalized); + if let Some(ref parent_path) = parent { + if !folders.iter().any(|folder| folder.path == *parent_path) { + register_folder_tree(folders, parent_path); + } + } + + folders.push(AssetFolder { + name: folder_name(&normalized), + path: normalized.clone(), + parent, + }); + + if Path::new(&normalized).is_dir() { + let Ok(read_dir) = fs::read_dir(&normalized) else { + return; + }; + for entry in read_dir.flatten() { + if entry.path().is_dir() { + register_folder_tree(folders, &entry.path().to_string_lossy()); + } + } + } +} + +fn scan_assets_directory(assets: &mut Vec) { + let root_path = Path::new(ASSETS_ROOT); + if !root_path.exists() { + return; + } + + for entry in WalkDir::new(root_path) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + { + if !entry.file_type().is_file() { + continue; + } + let path = entry.path(); + if should_skip_asset_path(path) { + continue; + } + if let Some(asset) = asset_from_file_path(path) { + assets.push(asset); + } + } +} + +fn should_skip_asset_path(path: &Path) -> bool { + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with('.')) + { + return true; + } + + let normalized = normalize_asset_path(path); + normalized == "assets/project.ron" + || normalized.starts_with("assets/.index/") + || normalized.starts_with(crate::assets::static_mesh::STATIC_MESH_ARTIFACT_DIR) +} + +fn asset_from_file_path(path: &Path) -> Option { + let extension = path.extension()?.to_str()?.to_ascii_lowercase(); + let (kinds, extensions) = match extension.as_str() { + "gltf" | "glb" | "fbx" => (vec![EditorAssetKind::Model], vec!["gltf", "glb", "fbx"]), + "png" | "jpg" | "jpeg" | "webp" | "ktx2" => ( + vec![EditorAssetKind::Texture], + vec!["png", "jpg", "jpeg", "webp", "ktx2"], + ), + "ron" | "mat" | "material" => { + if path.to_string_lossy().contains("/materials/") { + ( + vec![EditorAssetKind::Material], + vec!["ron", "mat", "material"], + ) + } else if path.to_string_lossy().contains("/post_fx/") { + (vec![EditorAssetKind::PostProcessEffect], vec!["ron"]) + } else if path.to_string_lossy().contains("/rendering_profiles/") { + (vec![EditorAssetKind::RenderingProfile], vec!["ron"]) + } else if path.to_string_lossy().contains("/shaders/") { + (vec![EditorAssetKind::ShaderSchema], vec!["ron"]) + } else { + ( + vec![EditorAssetKind::Level, EditorAssetKind::Prefab], + vec!["ron"], + ) + } + } + _ => return None, + }; + + if !has_extension(path, &extensions) { + return None; + } + + let path_string = normalize_asset_path(path); + let folder_path = parent_folder_path(&path_string).unwrap_or_else(|| ASSETS_ROOT.to_string()); + let label = path + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or(&path_string) + .to_string(); + let kind = if kinds.len() == 2 && path_string.ends_with(".scn.ron") { + kinds[1].clone() + } else { + kinds[0].clone() + }; + + Some(EditorAsset { + label, + path: Some(path_string), + folder_path, + kind, + }) +} + +fn normalize_path(path: &str) -> String { + path.replace('\\', "/").trim_end_matches('/').to_string() +} + +fn normalize_asset_path(path: &Path) -> String { + let raw = path.to_string_lossy().replace('\\', "/"); + raw.strip_prefix("assets/") + .or_else(|| raw.strip_prefix("./assets/")) + .map(|rest| format!("assets/{rest}")) + .unwrap_or(raw) +} + +/// Path relative to Bevy's `assets/` folder for [`AssetServer::load`]. +pub fn asset_server_path(catalog_path: &str) -> String { + shared::asset_server_path(catalog_path) +} + +fn parent_folder_path(path: &str) -> Option { + let normalized = normalize_path(path); + normalized + .rfind('/') + .map(|index| normalized[..index].to_string()) +} + +fn folder_name(path: &str) -> String { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(path) + .to_string() +} + +fn has_extension(path: &Path, extensions: &[&str]) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| { + extensions + .iter() + .any(|candidate| ext.eq_ignore_ascii_case(candidate)) + }) +} + +pub fn snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> Option { + let mut snapshot = EditorEntitySnapshot { + actor_id: None, + actor_kind: ActorKind::Empty, + actor_name: None, + name: Some(asset.label.clone()), + transform: Transform::from_translation(translation), + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: None, + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: 0, + editor_visibility: EditorVisibility::default(), + children: Vec::new(), + }; + + match &asset.kind { + EditorAssetKind::Primitive(shape) => { + snapshot.actor_kind = ActorKind::StaticMesh; + let size = match shape { + PrimitiveShape::Box => Vec3::ONE, + PrimitiveShape::Sphere => Vec3::splat(1.5), + PrimitiveShape::Ramp => Vec3::new(4.0, 0.4, 5.0), + }; + snapshot.primitive = Some(match shape { + PrimitiveShape::Box => Primitive::cuboid(size), + PrimitiveShape::Sphere => Primitive::sphere(size.x * 0.5), + PrimitiveShape::Ramp => Primitive::ramp(size), + }); + snapshot.material = Some(default_material_for(shape)); + snapshot.rigid_body = Some(RigidBodyDesc::default()); + snapshot.collider = Some(match shape { + PrimitiveShape::Sphere => ColliderDesc::static_sphere(size.x * 0.5), + _ => ColliderDesc::static_cuboid(size), + }); + if matches!(shape, PrimitiveShape::Ramp) { + snapshot.transform.rotation = Quat::from_rotation_x(-std::f32::consts::PI / 9.0); + snapshot.transform.translation.y += 1.0; + } else { + snapshot.transform.translation.y += size.y * 0.5; + } + } + EditorAssetKind::Light(kind) => { + snapshot.actor_kind = ActorKind::Light; + snapshot.light = Some(LightDesc::for_kind(*kind)); + snapshot.transform.translation.y += 3.0; + } + EditorAssetKind::PostProcessVolume => { + snapshot.actor_kind = ActorKind::PostProcessVolume; + snapshot.post_process_volume = Some(PostProcessVolumeDesc { + half_extents: Vec3::new(4.0, 2.0, 4.0), + ..Default::default() + }); + snapshot.transform.translation.y += 2.0; + } + EditorAssetKind::Model => { + snapshot.actor_kind = ActorKind::ImportedModel; + let path = asset.path.as_ref()?; + snapshot.model = Some(ModelRef::new(path.clone())); + snapshot.transform.translation.y += 0.1; + } + EditorAssetKind::Prefab | EditorAssetKind::Level => { + snapshot.actor_kind = ActorKind::PrefabAnchor; + let path = asset.path.as_ref()?; + snapshot.prefab = Some(PrefabRef::new(path.clone())); + } + EditorAssetKind::Texture + | EditorAssetKind::Material + | EditorAssetKind::PostProcessEffect + | EditorAssetKind::RenderingProfile + | EditorAssetKind::ShaderSchema => return None, + } + + Some(snapshot) +} + +pub fn spawn_asset_at(world: &mut World, asset: &EditorAsset, translation: Vec3) -> Option { + let mut snapshot = snapshot_for_asset(asset, translation)?; + if let Some(light) = snapshot.light.as_mut() { + if matches!(light.kind, AuthoringLightKind::Directional) { + light.intensity = world + .resource::() + .rendering + .sun_illuminance; + } + } + if let Some(path) = asset.path.as_ref() { + let record = world + .get_resource::() + .and_then(|registry| find_asset_by_path(registry, path)); + if let Some(record) = record { + match asset.kind { + EditorAssetKind::Model => { + snapshot.transform.scale *= record.import_settings.scale; + if matches!( + record.import_settings.placement_mode, + ModelPlacementMode::StaticAsset + ) { + if let Some(renderer) = static_mesh_renderer_for_asset(world, path) { + let collider = record + .import_settings + .generate_collider + .then(|| static_mesh_collider_for_renderer(&renderer)); + apply_static_mesh_placement_mode( + &mut snapshot, + renderer, + collider, + record.import_settings.hierarchy_mode, + ); + } else { + warn!( + "Static mesh placement fell back to SceneRoot for {} because no renderer manifest was available", + path + ); + } + } + } + EditorAssetKind::Prefab => { + snapshot.prefab_instance = + Some(PrefabInstance::new(record.id.as_string(), path.clone())); + } + _ => {} + } + } + } + Some(spawn_with_history(world, snapshot)) +} + +fn apply_static_mesh_placement_mode( + snapshot: &mut EditorEntitySnapshot, + renderer: shared::StaticMeshRenderer, + collider: Option, + hierarchy_mode: crate::asset_db::ModelHierarchyMode, +) { + snapshot.model = None; + match hierarchy_mode { + crate::asset_db::ModelHierarchyMode::SingleActor => { + snapshot.actor_kind = ActorKind::StaticMesh; + snapshot.static_mesh_renderer = Some(renderer); + if let Some(collider) = collider { + snapshot.rigid_body = Some(RigidBodyDesc::default()); + snapshot.collider = Some(collider); + } + } + crate::asset_db::ModelHierarchyMode::SourceHierarchy => { + snapshot.actor_kind = ActorKind::Empty; + snapshot.static_mesh_renderer = None; + snapshot.children = renderer + .slots + .into_iter() + .enumerate() + .map(|(index, mut entry)| { + let transform = entry.local_transform; + let collider = collider + .as_ref() + .map(|_| ColliderDesc::static_mesh(vec![entry.mesh.clone()])); + entry.local_transform = Transform::default(); + EditorEntitySnapshot { + actor_id: None, + actor_kind: ActorKind::StaticMesh, + actor_name: None, + name: Some(if entry.name.trim().is_empty() { + format!("Mesh Part {index}") + } else { + entry.name.clone() + }), + transform, + primitive: None, + static_mesh_renderer: Some(shared::StaticMeshRenderer::single(entry)), + material: None, + material_override: None, + rigid_body: collider.as_ref().map(|_| RigidBodyDesc::default()), + collider, + physics: None, + light: None, + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: index as i32, + editor_visibility: EditorVisibility::default(), + children: Vec::new(), + } + }) + .collect(); + } + } +} + +fn static_mesh_collider_for_renderer(renderer: &shared::StaticMeshRenderer) -> ColliderDesc { + ColliderDesc::static_mesh( + renderer + .slots + .iter() + .map(|slot| slot.mesh.clone()) + .collect(), + ) +} + +fn static_mesh_renderer_for_asset( + world: &mut World, + path: &str, +) -> Option { + let mut manifest_path = None; + if let Some(mut registry) = world.get_resource_mut::() { + let mut renderer = None; + if let Some(record) = find_asset_mut_by_path(&mut registry, path) { + match refresh_static_mesh_artifact(record) { + Ok(manifest) => { + renderer = Some(renderer_from_manifest(&manifest, &record.import_settings)); + registry.index_dirty = true; + } + Err(error) => { + warn!("Static mesh manifest refresh failed for {path}: {error}"); + manifest_path = record.import_settings.static_mesh_manifest_path.clone(); + } + } + } + if renderer.is_some() { + if let Err(error) = crate::asset_db::save_registry(®istry) { + warn!("Asset registry save failed after static mesh refresh: {error}"); + } else { + registry.index_dirty = false; + } + return renderer; + } + } + + let manifest_path = manifest_path?; + let registry = world.get_resource::()?; + let record = find_asset_by_path(registry, path)?; + let manifest = load_static_mesh_manifest(&manifest_path).ok()?; + Some(renderer_from_manifest(&manifest, &record.import_settings)) +} + +pub fn import_external_assets(paths: &[PathBuf]) -> Result { + let mut copied = 0; + for source in paths { + let Some(file_name) = source.file_name() else { + continue; + }; + let Some(extension) = source.extension().and_then(|ext| ext.to_str()) else { + continue; + }; + let dest_dir = import_dir_for_extension(extension); + if extension.eq_ignore_ascii_case("fbx") { + super::import::copy_fbx_with_sidecar(source, Path::new(dest_dir))?; + copied += 1; + continue; + } + fs::create_dir_all(dest_dir) + .map_err(|err| format!("could not create {dest_dir}: {err}"))?; + let dest = Path::new(dest_dir).join(file_name); + fs::copy(source, &dest).map_err(|err| { + format!( + "could not copy {} to {}: {err}", + source.display(), + dest.display() + ) + })?; + copied += 1; + } + Ok(copied) +} + +fn import_dir_for_extension(extension: &str) -> &'static str { + match extension.to_ascii_lowercase().as_str() { + "gltf" | "glb" | "fbx" => "assets/models", + "png" | "jpg" | "jpeg" | "webp" | "ktx2" => "assets/textures", + "ron" => "assets/levels", + "mat" | "material" => "assets/materials", + _ => "assets", + } +} + +fn default_material_for(shape: &PrimitiveShape) -> MaterialDesc { + match shape { + PrimitiveShape::Box => MaterialDesc::default(), + PrimitiveShape::Sphere => MaterialDesc::new(ColorDesc::srgb(0.9, 0.9, 0.92), 1.0, 0.25), + PrimitiveShape::Ramp => MaterialDesc::new(ColorDesc::srgb(0.30, 0.45, 0.70), 0.05, 0.55), + } +} + +pub fn asset_cache_key(asset: &EditorAsset) -> String { + match &asset.path { + Some(path) => path.clone(), + None => format!("builtin:{}", asset.label), + } +} diff --git a/crates/editor/src/assets/import.rs b/crates/editor/src/assets/import.rs new file mode 100644 index 0000000..bf70cec --- /dev/null +++ b/crates/editor/src/assets/import.rs @@ -0,0 +1,71 @@ +//! External asset import helpers (FBX sidecar folders, etc.). + +use std::fs; +use std::path::{Path, PathBuf}; + +/// Copies an FBX file and its sibling `.fbm` embedded-texture folder when present. +pub fn copy_fbx_with_sidecar(source: &Path, dest_dir: &Path) -> Result<(), String> { + let Some(file_name) = source.file_name() else { + return Err("FBX import path has no file name".into()); + }; + fs::create_dir_all(dest_dir) + .map_err(|err| format!("could not create {}: {err}", dest_dir.display()))?; + let dest = dest_dir.join(file_name); + fs::copy(source, &dest).map_err(|err| { + format!( + "could not copy {} to {}: {err}", + source.display(), + dest.display() + ) + })?; + + if let Some(fbm_dir) = fbm_sidecar_dir(source) { + if fbm_dir.is_dir() { + let dest_fbm = dest_dir.join(fbm_dir.file_name().unwrap_or_default()); + copy_dir_recursive(&fbm_dir, &dest_fbm)?; + } + } + Ok(()) +} + +fn fbm_sidecar_dir(source: &Path) -> Option { + let stem = source.file_stem()?.to_str()?; + let parent = source.parent()?; + Some(parent.join(format!("{stem}.fbm"))) +} + +fn copy_dir_recursive(source: &Path, dest: &Path) -> Result<(), String> { + fs::create_dir_all(dest) + .map_err(|err| format!("could not create {}: {err}", dest.display()))?; + for entry in fs::read_dir(source).map_err(|err| err.to_string())? { + let entry = entry.map_err(|err| err.to_string())?; + let src_path = entry.path(); + let dest_path = dest.join(entry.file_name()); + if src_path.is_dir() { + copy_dir_recursive(&src_path, &dest_path)?; + } else { + fs::copy(&src_path, &dest_path).map_err(|err| { + format!( + "could not copy {} to {}: {err}", + src_path.display(), + dest_path.display() + ) + })?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::fbm_sidecar_dir; + use std::path::Path; + + #[test] + fn fbm_sidecar_path() { + assert_eq!( + fbm_sidecar_dir(Path::new("/tmp/Character.fbx")), + Some(Path::new("/tmp/Character.fbm").to_path_buf()) + ); + } +} diff --git a/crates/editor/src/assets/materials.rs b/crates/editor/src/assets/materials.rs new file mode 100644 index 0000000..b5eefa9 --- /dev/null +++ b/crates/editor/src/assets/materials.rs @@ -0,0 +1,101 @@ +//! Material asset loading and inspector/browser integration. + +use bevy::prelude::*; +use bevy_egui::egui; +use shared::{LevelObject, MaterialAsset, MaterialDesc}; + +use crate::assets::{EditorAsset, EditorAssetKind, EditorAssets}; +use crate::history::set_material_with_history; +use crate::ui::inspector::property_row; + +pub fn load_material_asset(catalog_path: &str) -> Result { + MaterialAsset::load_from_path(catalog_path) +} + +pub fn material_desc_from_asset(catalog_path: &str) -> Result { + let asset = load_material_asset(catalog_path)?; + let mut desc = asset.material; + desc.material_asset_path = Some(catalog_path.to_string()); + Ok(desc) +} + +pub fn apply_material_asset_to_selection( + world: &mut World, + asset: &EditorAsset, + selected: &bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities, +) { + let Some(path) = asset.path.clone() else { + return; + }; + let desc = match material_desc_from_asset(&path) { + Ok(desc) => desc, + Err(err) => { + world.resource_mut::().status = err; + return; + } + }; + for entity in crate::ui::helpers::selected_level_entities(world, selected) { + if world.get::(entity).is_none() { + continue; + } + if world.get::(entity).is_none() { + world.entity_mut(entity).insert(desc.clone()); + } + set_material_with_history(world, entity, desc.clone()); + } + world.resource_mut::().status = + format!("Applied material {}", asset.label); +} + +pub fn material_asset_picker_ui( + world: &mut World, + ui: &mut egui::Ui, + _entity: Entity, + material: &mut MaterialDesc, + _original: &MaterialDesc, +) -> bool { + let assets: Vec<(String, String)> = world + .resource::() + .assets + .iter() + .filter(|a| matches!(a.kind, EditorAssetKind::Material)) + .filter_map(|a| Some((a.label.clone(), a.path.clone()?))) + .collect(); + + let current = material.material_asset_path.clone().unwrap_or_default(); + let mut selected_label = assets + .iter() + .find(|(_, path)| path == ¤t) + .map(|(label, _)| label.clone()) + .unwrap_or_else(|| "(none)".to_string()); + + let mut changed = false; + property_row(ui, "Material asset", |ui| { + egui::ComboBox::from_id_salt("material_asset_picker") + .selected_text(&selected_label) + .show_ui(ui, |ui| { + if ui.selectable_label(current.is_empty(), "(none)").clicked() { + material.material_asset_path = None; + selected_label = "(none)".into(); + changed = true; + } + for (label, path) in &assets { + if ui + .selectable_label(&selected_label == label, label) + .clicked() + { + if let Ok(desc) = material_desc_from_asset(path) { + *material = desc; + selected_label = label.clone(); + changed = true; + } else { + world.resource_mut::().status = + format!("Could not load material {path}"); + } + } + } + }); + }); + + changed +} diff --git a/crates/editor/src/assets/mod.rs b/crates/editor/src/assets/mod.rs new file mode 100644 index 0000000..e91ff9a --- /dev/null +++ b/crates/editor/src/assets/mod.rs @@ -0,0 +1,15 @@ +//! Asset catalog, database, thumbnails, materials, and prefab overrides. + +pub mod asset_db; +mod catalog; +mod import; +pub mod materials; +pub mod prefab_overrides; +pub mod static_mesh; +pub mod thumbnails; + +pub use catalog::*; +pub use thumbnails::{ + draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails, + AssetThumbnailCache, ThumbnailCacheSnapshot, ThumbnailState, ThumbnailsPlugin, +}; diff --git a/crates/editor/src/assets/prefab_overrides.rs b/crates/editor/src/assets/prefab_overrides.rs new file mode 100644 index 0000000..b37a764 --- /dev/null +++ b/crates/editor/src/assets/prefab_overrides.rs @@ -0,0 +1,390 @@ +//! Prefab instance overrides — apply/revert per field group (v2). + +use std::collections::HashMap; +use std::path::Path; + +use bevy::prelude::*; +use bevy_egui::egui; +use egui_phosphor_icons::icons; +use scene::{migrate_scene_text, strip_schema_version, validate_level_text}; +use serde::{Deserialize, Serialize}; +use shared::{EditorVisibility, MaterialDesc, PrefabInstance}; + +use crate::history::{set_material_with_history, set_transform_with_history}; +use crate::ui::inspector::{ + apply_component_card_response, component_card, component_card_context, property_row, + ComponentCardOptions, +}; + +#[derive(Resource, Default)] +pub struct PrefabOverrideClipboard { + pub property_path: String, + pub value_ron: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct PrefabOverrides { + #[serde(default)] + pub transform: Option, + #[serde(default)] + pub material: Option, + #[serde(default)] + pub child_visibility: HashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrefabOverrideField { + Transform, + Material, + ChildVisibility, +} + +pub fn read_overrides(instance: &PrefabInstance) -> PrefabOverrides { + instance + .overrides_ron + .as_ref() + .and_then(|ron| ron::from_str(ron).ok()) + .unwrap_or_default() +} + +pub fn write_overrides(world: &mut World, entity: Entity, overrides: &PrefabOverrides) { + let Some(mut instance) = world.get::(entity).cloned() else { + return; + }; + instance.overrides_ron = if overrides == &PrefabOverrides::default() { + None + } else { + Some(ron::to_string(overrides).unwrap_or_default()) + }; + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(instance); + } +} + +pub fn has_override(overrides: &PrefabOverrides, field: PrefabOverrideField) -> bool { + match field { + PrefabOverrideField::Transform => overrides.transform.is_some(), + PrefabOverrideField::Material => overrides.material.is_some(), + PrefabOverrideField::ChildVisibility => !overrides.child_visibility.is_empty(), + } +} + +pub fn capture_transform_override(world: &mut World, entity: Entity) { + let Some(transform) = world.get::(entity).copied() else { + return; + }; + let Some(instance) = world.get::(entity) else { + return; + }; + let mut overrides = read_overrides(instance); + overrides.transform = Some(transform); + write_overrides(world, entity, &overrides); +} + +pub fn revert_transform_override(world: &mut World, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let Some(old) = world.get::(entity).copied() else { + return; + }; + let Some(source_transform) = prefab_root_transform(&instance.source_path) else { + world.resource_mut::().status = + "Could not read prefab source for revert".into(); + return; + }; + set_transform_with_history(world, entity, old, source_transform); + let mut overrides = read_overrides(&instance); + overrides.transform = None; + write_overrides(world, entity, &overrides); +} + +pub fn apply_material_override(world: &mut World, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let Some(material) = world.get::(entity).cloned() else { + world.resource_mut::().status = + "Prefab instance has no MaterialDesc to capture".into(); + return; + }; + let mut overrides = read_overrides(&instance); + overrides.material = Some(material); + write_overrides(world, entity, &overrides); + world.resource_mut::().status = "Material override applied".into(); +} + +pub fn revert_material_override(world: &mut World, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let Some(source_material) = prefab_root_material(&instance.source_path) else { + world.resource_mut::().status = + "Could not read prefab source material".into(); + return; + }; + let old = world.get::(entity).cloned(); + if old.is_some() { + set_material_with_history(world, entity, source_material.clone()); + } else { + world.entity_mut(entity).insert(source_material.clone()); + } + let mut overrides = read_overrides(&instance); + overrides.material = None; + write_overrides(world, entity, &overrides); +} + +pub fn apply_child_visibility_override(world: &mut World, entity: Entity, child: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let Some(name) = world.get::(child).map(|n| n.to_string()) else { + return; + }; + let visible = world + .get::(child) + .map(|v| v.visible) + .unwrap_or(true); + + let mut overrides = read_overrides(&instance); + overrides.child_visibility.insert(name, visible); + write_overrides(world, entity, &overrides); +} + +pub fn revert_child_visibility_overrides(world: &mut World, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + for child in prefab_instance_children(world, entity) { + if let Ok(mut entity_mut) = world.get_entity_mut(child) { + entity_mut.remove::(); + } + } + let mut overrides = read_overrides(&instance); + overrides.child_visibility.clear(); + write_overrides(world, entity, &overrides); +} + +pub fn apply_stored_overrides(world: &mut World, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let overrides = read_overrides(&instance); + + if let Some(transform) = overrides.transform { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(transform); + } + } + if let Some(material) = overrides.material { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(material); + } + } + for child in prefab_instance_children(world, entity) { + let Some(name) = world.get::(child).map(|n| n.to_string()) else { + continue; + }; + if let Some(&visible) = overrides.child_visibility.get(&name) { + if let Ok(mut entity_mut) = world.get_entity_mut(child) { + entity_mut.insert(EditorVisibility { visible }); + } + } + } +} + +pub fn prefab_instance_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { + let Some(instance) = world.get::(entity).cloned() else { + return; + }; + let overrides = read_overrides(&instance); + + let card = component_card_context( + world, + entity, + ComponentCardOptions::removable( + shared::COMPONENT_PREFAB_INSTANCE, + "Prefab Instance", + icons::PACKAGE, + ), + ); + let card_response = component_card(ui, &card, |ui| { + property_row(ui, "Asset ID", |ui| { + ui.add(egui::Label::new(instance.asset_id.as_str()).truncate()); + }); + property_row(ui, "Source", |ui| { + ui.add(egui::Label::new(instance.source_path.as_str()).truncate()); + }); + + ui.collapsing("Transform override", |ui| { + let active = has_override(&overrides, PrefabOverrideField::Transform); + if active { + ui.label(egui::RichText::new("Override active").weak()); + } + ui.horizontal_wrapped(|ui| { + if ui.button("Apply").clicked() { + capture_transform_override(world, entity); + world.resource_mut::().status = + "Transform override saved".into(); + } + if ui + .add_enabled(active, egui::Button::new("Revert")) + .clicked() + { + revert_transform_override(world, entity); + } + }); + }); + + ui.collapsing("Material override", |ui| { + let active = has_override(&overrides, PrefabOverrideField::Material); + if active { + ui.label(egui::RichText::new("Override active").weak()); + } + ui.horizontal_wrapped(|ui| { + if ui.button("Apply").clicked() { + apply_material_override(world, entity); + } + if ui + .add_enabled(active, egui::Button::new("Revert")) + .clicked() + { + revert_material_override(world, entity); + } + }); + }); + + ui.collapsing("Child visibility overrides", |ui| { + let active = has_override(&overrides, PrefabOverrideField::ChildVisibility); + if active { + ui.label(egui::RichText::new("Overrides stored by child name").weak()); + } + for child in prefab_instance_children(world, entity) { + let name = world + .get::(child) + .map(|n| n.to_string()) + .unwrap_or_else(|| format!("{child:?}")); + ui.horizontal_wrapped(|ui| { + ui.add(egui::Label::new(name).truncate()); + if ui.small_button("Apply").clicked() { + apply_child_visibility_override(world, entity, child); + } + }); + } + ui.horizontal_wrapped(|ui| { + if ui + .add_enabled(active, egui::Button::new("Revert all")) + .clicked() + { + revert_child_visibility_overrides(world, entity); + } + }); + }); + + ui.horizontal_wrapped(|ui| { + if ui.button("Unpack Instance").clicked() { + crate::history::unpack_prefab_instance(world, entity); + } + }); + }); + apply_component_card_response(world, entity, card_response); +} + +fn prefab_instance_children(world: &World, root: Entity) -> Vec { + let mut children = Vec::new(); + let mut stack = Vec::new(); + if let Some(kids) = world.get::(root) { + stack.extend(kids.iter()); + } + while let Some(entity) = stack.pop() { + children.push(entity); + if let Some(kids) = world.get::(entity) { + stack.extend(kids.iter()); + } + } + children +} + +fn prefab_root_transform(source_path: &str) -> Option { + parse_prefab_root_component::(source_path, "Transform") +} + +fn prefab_root_material(source_path: &str) -> Option { + parse_prefab_root_component::(source_path, "MaterialDesc") +} + +fn parse_prefab_root_component( + source_path: &str, + type_name: &str, +) -> Option { + let path = Path::new(source_path); + let text = std::fs::read_to_string(path).ok()?; + validate_level_text(&text).ok()?; + let body = strip_schema_version(&migrate_scene_text(&text).ok()?).ok()?; + let root_tuple = body.trim().trim_start_matches('(').trim_end_matches(')'); + for segment in split_top_level_fields(root_tuple) { + let segment = segment.trim(); + if segment.starts_with("entities:") { + let entities_body = segment.trim_start_matches("entities:").trim(); + return parse_first_entity_component(entities_body, type_name); + } + } + None +} + +fn split_top_level_fields(input: &str) -> Vec { + let mut fields = Vec::new(); + let mut depth: i32 = 0; + let mut start = 0; + for (index, ch) in input.char_indices() { + match ch { + '(' | '{' => depth += 1, + ')' | '}' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + fields.push(input[start..index].to_string()); + start = index + 1; + } + _ => {} + } + } + if start < input.len() { + fields.push(input[start..].to_string()); + } + fields +} + +fn parse_first_entity_component( + entities_body: &str, + type_name: &str, +) -> Option { + let trimmed = entities_body.trim(); + let inner = trimmed.strip_prefix('{')?.strip_suffix('}')?.trim(); + let first_entity = inner.split(',').next()?.trim(); + let components = first_entity + .split(':') + .nth(1)? + .trim() + .strip_prefix('(')? + .strip_suffix(')')?; + for component in split_top_level_fields(components) { + if component.contains(type_name) { + return ron::from_str::(&format!("({component})")).ok(); + } + } + None +} + +pub fn list_prefab_dependencies(path: &str) -> Vec { + vec![path.to_string()] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_overrides_roundtrip() { + let instance = PrefabInstance::new("id", "assets/prefabs/test.scn.ron"); + assert_eq!(read_overrides(&instance), PrefabOverrides::default()); + } +} diff --git a/crates/editor/src/assets/static_mesh.rs b/crates/editor/src/assets/static_mesh.rs new file mode 100644 index 0000000..9ae8962 --- /dev/null +++ b/crates/editor/src/assets/static_mesh.rs @@ -0,0 +1,648 @@ +//! Normalized static mesh artifacts generated from model source files. + +use std::fs; +use std::path::Path; +use std::time::UNIX_EPOCH; + +use bevy::gltf::GltfAssetLabel; +use bevy::prelude::*; +use bevy_ufbx::label::FbxAssetLabel; +use bevy_ufbx::mesh::group_faces_by_material; +use bevy_ufbx::utils::convert_matrix; +use serde::{Deserialize, Serialize}; + +use crate::asset_db::{ + AssetRecord, ImportSettings, MaterialImportPolicy, ModelHierarchyMode, ModelPlacementMode, +}; +use shared::{ComponentInstanceId, EditorAssetRef, StaticMeshRenderer, StaticMeshRendererEntry}; + +pub const STATIC_MESH_MANIFEST_SCHEMA: u32 = 1; +pub const STATIC_MESH_ARTIFACT_DIR: &str = "assets/meshes/generated"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshManifest { + pub schema_version: u32, + pub asset_id: String, + pub label: String, + pub source: StaticMeshSource, + pub import: StaticMeshImportSnapshot, + pub metadata: StaticMeshMetadata, + pub parts: Vec, + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshSource { + pub path: String, + pub format: String, + pub fingerprint: StaticMeshSourceFingerprint, + pub dependencies: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshSourceFingerprint { + pub byte_len: u64, + pub modified_unix_secs: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshImportSnapshot { + pub scale: f32, + pub generate_collider: bool, + pub lod0_only: bool, + pub placement_mode: ModelPlacementMode, + pub hierarchy_mode: ModelHierarchyMode, + pub material_policy: MaterialImportPolicy, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshMetadata { + pub mesh_count: usize, + pub material_count: usize, + pub node_count: usize, + pub animation_count: usize, + pub skin_count: usize, + pub light_count: usize, + pub camera_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StaticMeshPart { + #[serde(default)] + pub id: String, + pub name: String, + pub mesh_label: String, + #[serde(default)] + pub material_id: Option, + pub material_slot_name: String, + pub material_label: Option, + pub local_transform: Transform, + pub source_node: Option, + pub source_mesh: Option, + pub source_material: Option, +} + +pub fn static_mesh_manifest_path(asset_id: &str) -> String { + format!("{STATIC_MESH_ARTIFACT_DIR}/{asset_id}.static_mesh.ron") +} + +pub fn part_id_from_label(label: &str) -> String { + format!("mesh:{}", stable_sub_asset_slug(label)) +} + +pub fn material_id_from_label(label: &str) -> String { + format!("material:{}", stable_sub_asset_slug(label)) +} + +fn stable_sub_asset_slug(label: &str) -> String { + let mut slug = String::new(); + for ch in label.chars() { + if ch.is_ascii_alphanumeric() { + slug.push(ch.to_ascii_lowercase()); + } else if !slug.ends_with('_') { + slug.push('_'); + } + } + slug.trim_matches('_').to_string() +} + +pub fn refresh_static_mesh_artifact( + record: &mut AssetRecord, +) -> Result { + let mut manifest = build_static_mesh_manifest(record)?; + let path = static_mesh_manifest_path(&record.id.as_string()); + record.import_settings.static_mesh_manifest_path = Some(path.clone()); + manifest.source.dependencies.sort(); + manifest.source.dependencies.dedup(); + record.dependencies = manifest.source.dependencies.clone(); + + let text = ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default()) + .map_err(|err| format!("could not serialize static mesh manifest: {err}"))?; + if let Some(parent) = Path::new(&path).parent() { + fs::create_dir_all(parent) + .map_err(|err| format!("could not create {}: {err}", parent.display()))?; + } + let should_write = fs::read_to_string(&path).ok().as_deref() != Some(text.as_str()); + if should_write { + fs::write(&path, text) + .map_err(|err| format!("could not write static mesh manifest {path}: {err}"))?; + info!( + "Static mesh manifest refreshed: source={} artifact={} parts={}", + record.path, + path, + manifest.parts.len() + ); + } + + Ok(manifest) +} + +pub fn load_static_mesh_manifest(path: &str) -> Result { + let text = fs::read_to_string(path).map_err(|err| format!("could not read {path}: {err}"))?; + ron::from_str(&text).map_err(|err| format!("could not parse {path}: {err}")) +} + +pub fn renderer_from_manifest( + manifest: &StaticMeshManifest, + settings: &ImportSettings, +) -> StaticMeshRenderer { + let use_source_materials = matches!( + settings.material_policy, + MaterialImportPolicy::SourceMaterials + ); + + StaticMeshRenderer { + slots: manifest + .parts + .iter() + .map(|part| StaticMeshRendererEntry { + id: ComponentInstanceId::new(part_effective_id(part)), + name: part.name.clone(), + mesh: EditorAssetRef::new( + manifest.asset_id.clone(), + part_effective_id(part), + part.name.clone(), + ), + material: use_source_materials + .then(|| { + part_effective_material_id(part).map(|id| { + EditorAssetRef::new( + manifest.asset_id.clone(), + id, + part.material_slot_name.clone(), + ) + }) + }) + .flatten(), + local_transform: part.local_transform, + visible: true, + cast_shadows: true, + receive_shadows: true, + }) + .collect(), + } +} + +fn part_effective_id(part: &StaticMeshPart) -> String { + if part.id.trim().is_empty() { + part_id_from_label(&part.mesh_label) + } else { + part.id.clone() + } +} + +fn part_effective_material_id(part: &StaticMeshPart) -> Option { + part.material_id.clone().or_else(|| { + part.material_label + .as_ref() + .map(|label| material_id_from_label(label)) + }) +} + +fn build_static_mesh_manifest(record: &AssetRecord) -> Result { + let format = source_format(&record.path)?; + let fingerprint = source_fingerprint(&record.path)?; + let import = StaticMeshImportSnapshot { + scale: record.import_settings.scale, + generate_collider: record.import_settings.generate_collider, + lod0_only: record.import_settings.lod0_only, + placement_mode: record.import_settings.placement_mode.clone(), + hierarchy_mode: record.import_settings.hierarchy_mode.clone(), + material_policy: record.import_settings.material_policy.clone(), + }; + + let mut manifest = match format.as_str() { + "gltf" | "glb" => build_gltf_manifest(record, format, fingerprint, import)?, + "fbx" => build_fbx_manifest(record, format, fingerprint, import)?, + _ => return Err(format!("unsupported static mesh source format `{format}`")), + }; + + if manifest.parts.is_empty() { + manifest + .warnings + .push("No renderable static mesh parts were found.".into()); + } + + Ok(manifest) +} + +fn build_gltf_manifest( + record: &AssetRecord, + format: String, + fingerprint: StaticMeshSourceFingerprint, + import: StaticMeshImportSnapshot, +) -> Result { + let gltf = gltf::Gltf::open(&record.path) + .map_err(|err| format!("could not parse glTF {}: {err}", record.path))?; + let mut parts = Vec::new(); + let mut dependencies = Vec::new(); + let mut warnings = Vec::new(); + + for buffer in gltf.document.buffers() { + if let gltf::buffer::Source::Uri(uri) = buffer.source() { + dependencies.push(resolve_dependency(&record.path, uri)); + } + } + for image in gltf.document.images() { + if let gltf::image::Source::Uri { uri, .. } = image.source() { + dependencies.push(resolve_dependency(&record.path, uri)); + } + } + + if let Some(scene) = gltf + .document + .default_scene() + .or_else(|| gltf.document.scenes().next()) + { + for node in scene.nodes() { + collect_gltf_node_parts(node, Mat4::IDENTITY, &mut parts); + } + } else { + for mesh in gltf.document.meshes() { + collect_gltf_mesh_parts(None, None, mesh, Mat4::IDENTITY, &mut parts); + } + } + + if gltf.document.animations().count() > 0 { + warnings.push( + "Animations are recorded as metadata; use Scene Instance placement for playback." + .into(), + ); + } + if gltf.document.skins().count() > 0 { + warnings.push( + "Skins are recorded as metadata; skinned playback stays on the Scene Instance path." + .into(), + ); + } + + parts.sort_by(|a, b| a.mesh_label.cmp(&b.mesh_label).then(a.name.cmp(&b.name))); + + Ok(StaticMeshManifest { + schema_version: STATIC_MESH_MANIFEST_SCHEMA, + asset_id: record.id.as_string(), + label: record.label.clone(), + source: StaticMeshSource { + path: record.path.clone(), + format, + fingerprint, + dependencies, + }, + import, + metadata: StaticMeshMetadata { + mesh_count: gltf.document.meshes().count(), + material_count: gltf.document.materials().count(), + node_count: gltf.document.nodes().count(), + animation_count: gltf.document.animations().count(), + skin_count: gltf.document.skins().count(), + light_count: 0, + camera_count: gltf.document.cameras().count(), + }, + parts, + warnings, + }) +} + +fn collect_gltf_node_parts( + node: gltf::Node<'_>, + parent_transform: Mat4, + parts: &mut Vec, +) { + let local = Mat4::from_cols_array_2d(&node.transform().matrix()); + let world_transform = parent_transform * local; + if let Some(mesh) = node.mesh() { + collect_gltf_mesh_parts( + node.name().map(str::to_string), + Some(node.index()), + mesh, + world_transform, + parts, + ); + } + for child in node.children() { + collect_gltf_node_parts(child, world_transform, parts); + } +} + +fn collect_gltf_mesh_parts( + node_name: Option, + node_index: Option, + mesh: gltf::Mesh<'_>, + transform: Mat4, + parts: &mut Vec, +) { + let mesh_index = mesh.index(); + let mesh_name = mesh.name().map(str::to_string); + for primitive in mesh.primitives() { + let primitive_index = primitive.index(); + let mesh_label = GltfAssetLabel::Primitive { + mesh: mesh_index, + primitive: primitive_index, + } + .to_string(); + let material = primitive.material(); + let material_label = material + .index() + .map(|index| { + GltfAssetLabel::Material { + index, + is_scale_inverted: false, + } + .to_string() + }) + .or_else(|| Some(GltfAssetLabel::DefaultMaterial.to_string())); + let material_name = material + .name() + .map(str::to_string) + .unwrap_or_else(|| "Default Material".into()); + let name = node_name + .clone() + .or_else(|| mesh_name.clone()) + .unwrap_or_else(|| format!("Mesh {mesh_index}")); + let source_node = node_index.map(|index| format!("Node{index}")); + parts.push(StaticMeshPart { + id: part_id_from_label(&mesh_label), + name: format!("{name} / Primitive {primitive_index}"), + material_id: material_label + .as_ref() + .map(|label| material_id_from_label(label)), + mesh_label, + material_slot_name: material_name.clone(), + material_label, + local_transform: Transform::from_matrix(transform), + source_node, + source_mesh: Some(format!("Mesh{mesh_index}")), + source_material: Some(material_name), + }); + } +} + +fn build_fbx_manifest( + record: &AssetRecord, + format: String, + fingerprint: StaticMeshSourceFingerprint, + import: StaticMeshImportSnapshot, +) -> Result { + let bytes = + fs::read(&record.path).map_err(|err| format!("could not read {}: {err}", record.path))?; + let scene = ufbx::load_memory( + &bytes, + ufbx::LoadOpts { + target_unit_meters: 1.0, + target_axes: ufbx::CoordinateAxes::right_handed_y_up(), + ..Default::default() + }, + ) + .map_err(|err| format!("could not parse FBX {}: {err:?}", record.path))?; + + let mut parts = Vec::new(); + let mut warnings = Vec::new(); + for (node_index, node) in scene.nodes.as_ref().iter().enumerate() { + let Some(mesh_ref) = node.mesh.as_ref() else { + continue; + }; + let mesh = mesh_ref.as_ref(); + if mesh.num_vertices == 0 || mesh.faces.as_ref().is_empty() { + continue; + } + + let mut groups: Vec<(usize, Vec)> = + group_faces_by_material(mesh).into_iter().collect(); + groups.sort_by_key(|(material_index, _)| *material_index); + for (material_index, indices) in groups { + if indices.is_empty() { + continue; + } + let material = mesh.materials.get(material_index).map(|mat| mat.as_ref()); + let material_label = material + .and_then(|mat| fbx_material_label(&scene, mat.element.element_id)) + .or_else(|| Some(FbxAssetLabel::DefaultMaterial.to_string())); + let material_name = material + .map(|mat| mat.element.name.to_string()) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| "Default Material".into()); + let node_name = if node.element.name.is_empty() { + format!("Node {node_index}") + } else { + node.element.name.to_string() + }; + let mesh_label = FbxAssetLabel::Mesh(node_index * 1000 + material_index).to_string(); + parts.push(StaticMeshPart { + id: part_id_from_label(&mesh_label), + name: format!("{node_name} / Material {material_index}"), + material_id: material_label + .as_ref() + .map(|label| material_id_from_label(label)), + mesh_label, + material_slot_name: material_name.clone(), + material_label, + local_transform: Transform::from_matrix(convert_matrix(&node.geometry_to_world)), + source_node: Some(format!("Node{node_index}")), + source_mesh: Some(format!("Mesh{node_index}")), + source_material: Some(material_name), + }); + } + } + + if !scene.anim_stacks.as_ref().is_empty() { + warnings.push( + "Animations are recorded as metadata; use Scene Instance placement for playback." + .into(), + ); + } + if !scene.skin_deformers.as_ref().is_empty() { + warnings.push( + "Skins are recorded as metadata; skinned playback stays on the Scene Instance path." + .into(), + ); + } + + parts.sort_by(|a, b| a.mesh_label.cmp(&b.mesh_label).then(a.name.cmp(&b.name))); + + Ok(StaticMeshManifest { + schema_version: STATIC_MESH_MANIFEST_SCHEMA, + asset_id: record.id.as_string(), + label: record.label.clone(), + source: StaticMeshSource { + path: record.path.clone(), + format, + fingerprint, + dependencies: fbx_dependencies(&record.path), + }, + import, + metadata: StaticMeshMetadata { + mesh_count: scene.meshes.as_ref().len(), + material_count: scene.materials.as_ref().len(), + node_count: scene.nodes.as_ref().len(), + animation_count: scene.anim_stacks.as_ref().len(), + skin_count: scene.skin_deformers.as_ref().len(), + light_count: scene.lights.as_ref().len(), + camera_count: scene.cameras.as_ref().len(), + }, + parts, + warnings, + }) +} + +fn fbx_material_label(scene: &ufbx::Scene, element_id: u32) -> Option { + if element_id == 0 { + return None; + } + scene + .materials + .as_ref() + .iter() + .position(|material| material.element.element_id == element_id) + .map(|index| FbxAssetLabel::Material(index).to_string()) +} + +fn fbx_dependencies(source_path: &str) -> Vec { + let path = Path::new(source_path); + let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else { + return Vec::new(); + }; + let Some(parent) = path.parent() else { + return Vec::new(); + }; + let sidecar = parent.join(format!("{stem}.fbm")); + if !sidecar.is_dir() { + return Vec::new(); + } + walkdir::WalkDir::new(sidecar) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + .filter(|entry| entry.file_type().is_file()) + .map(|entry| entry.path().to_string_lossy().replace('\\', "/")) + .collect() +} + +fn source_format(path: &str) -> Result { + Path::new(path) + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) + .ok_or_else(|| format!("asset path `{path}` has no extension")) +} + +fn source_fingerprint(path: &str) -> Result { + let metadata = fs::metadata(path).map_err(|err| format!("could not stat {path}: {err}"))?; + let modified_unix_secs = metadata + .modified() + .ok() + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_secs()) + .unwrap_or_default(); + Ok(StaticMeshSourceFingerprint { + byte_len: metadata.len(), + modified_unix_secs, + }) +} + +fn resolve_dependency(source_path: &str, uri: &str) -> String { + if uri.starts_with("data:") || uri.contains("://") { + return uri.to_string(); + } + Path::new(source_path) + .parent() + .unwrap_or_else(|| Path::new("")) + .join(uri) + .to_string_lossy() + .replace('\\', "/") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_manifest() -> StaticMeshManifest { + StaticMeshManifest { + schema_version: STATIC_MESH_MANIFEST_SCHEMA, + asset_id: "asset-1".into(), + label: "Crate".into(), + source: StaticMeshSource { + path: "assets/models/crate.glb".into(), + format: "glb".into(), + fingerprint: StaticMeshSourceFingerprint { + byte_len: 42, + modified_unix_secs: 10, + }, + dependencies: Vec::new(), + }, + import: StaticMeshImportSnapshot { + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: ModelPlacementMode::StaticAsset, + hierarchy_mode: ModelHierarchyMode::SingleActor, + material_policy: MaterialImportPolicy::SourceMaterials, + }, + metadata: StaticMeshMetadata { + mesh_count: 1, + material_count: 1, + node_count: 1, + animation_count: 0, + skin_count: 0, + light_count: 0, + camera_count: 0, + }, + parts: vec![StaticMeshPart { + id: "mesh:mesh0_primitive0".into(), + name: "Crate / Primitive 0".into(), + mesh_label: "Mesh0/Primitive0".into(), + material_id: Some("material:material0".into()), + material_slot_name: "Wood".into(), + material_label: Some("Material0".into()), + local_transform: Transform::from_xyz(1.0, 2.0, 3.0), + source_node: Some("Node0".into()), + source_mesh: Some("Mesh0".into()), + source_material: Some("Wood".into()), + }], + warnings: Vec::new(), + } + } + + #[test] + fn static_mesh_manifest_path_uses_asset_id() { + assert_eq!( + static_mesh_manifest_path("abc"), + "assets/meshes/generated/abc.static_mesh.ron" + ); + } + + #[test] + fn renderer_from_manifest_preserves_labels_and_collider_policy() { + let manifest = test_manifest(); + let settings = ImportSettings { + generate_collider: true, + ..Default::default() + }; + + let renderer = renderer_from_manifest(&manifest, &settings); + + assert_eq!(renderer.slots.len(), 1); + let entry = &renderer.slots[0]; + assert_eq!(entry.mesh.asset_id, manifest.asset_id); + assert_eq!(entry.mesh.sub_asset_id, "mesh:mesh0_primitive0"); + assert_eq!( + entry + .material + .as_ref() + .map(|material| material.sub_asset_id.as_str()), + Some("material:material0") + ); + } + + #[test] + fn renderer_from_manifest_can_ignore_source_materials() { + let manifest = test_manifest(); + let settings = ImportSettings { + material_policy: MaterialImportPolicy::AuthoringOverride, + ..Default::default() + }; + + let renderer = renderer_from_manifest(&manifest, &settings); + + assert!(renderer.slots[0].material.is_none()); + } +} diff --git a/crates/editor/src/assets/thumbnails/cache.rs b/crates/editor/src/assets/thumbnails/cache.rs new file mode 100644 index 0000000..bb16a3a --- /dev/null +++ b/crates/editor/src/assets/thumbnails/cache.rs @@ -0,0 +1,379 @@ +//! Thumbnail cache for asset browser grid cells. + +use std::collections::{HashMap, HashSet}; + +use bevy::prelude::*; +use bevy_egui::{egui, EguiPrimaryContextPass, EguiTextureHandle, EguiUserTextures}; +use egui_phosphor_icons::icons; + +use super::sources::gltf::gltf_base_color_texture_path; +use super::studio::{model_file_exists, ThumbnailStudio}; +use crate::assets::{ + asset_cache_key, asset_server_path, EditorAsset, EditorAssetKind, EditorAssets, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ThumbnailState { + Pending, + Ready, + Failed { reason: String, retryable: bool }, +} + +#[derive(Debug, Clone)] +struct ThumbnailFailure { + reason: String, + retryable: bool, +} + +#[derive(Resource, Default)] +pub struct AssetThumbnailCache { + pub texture_ids: HashMap, + pending: HashMap>, + pub(crate) studio_pending: HashSet, + failed: HashMap, + prefetched_folder: Option, +} + +pub struct AssetThumbnailsPlugin; + +impl Plugin for AssetThumbnailsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(EguiPrimaryContextPass, register_loaded_thumbnails); + } +} + +impl AssetThumbnailCache { + pub fn state(&self, key: &str) -> Option { + if self.texture_ids.contains_key(key) { + return Some(ThumbnailState::Ready); + } + if self.failed.contains_key(key) { + let failure = self.failed.get(key)?; + return Some(ThumbnailState::Failed { + reason: failure.reason.clone(), + retryable: failure.retryable, + }); + } + if self.pending.contains_key(key) || self.studio_pending.contains(key) { + return Some(ThumbnailState::Pending); + } + None + } + + pub fn retry(&mut self, key: &str) { + self.failed.remove(key); + self.pending.remove(key); + self.studio_pending.remove(key); + self.texture_ids.remove(key); + } + + pub fn request_texture(&mut self, key: String, path: String, asset_server: &AssetServer) { + if matches!( + self.state(&key), + Some(ThumbnailState::Ready | ThumbnailState::Pending) + ) { + return; + } + if self.failed.contains_key(&key) { + return; + } + let handle: Handle = asset_server.load(asset_server_path(&path)); + self.pending.insert(key, handle); + } + + pub fn request_model( + &mut self, + key: String, + model_path: String, + asset_server: &AssetServer, + studio: &mut ThumbnailStudio, + ) { + if matches!( + self.state(&key), + Some(ThumbnailState::Ready | ThumbnailState::Pending) + ) { + return; + } + if self.failed.contains_key(&key) { + return; + } + + if let Some(texture_path) = gltf_base_color_texture_path(&model_path) { + self.request_texture(key, texture_path, asset_server); + return; + } + + if !model_file_exists(&model_path) { + self.mark_studio_failed(&key, "file not found", false); + return; + } + + if studio.enqueue(key.clone(), model_path) { + self.studio_pending.insert(key); + } + } + + pub(crate) fn complete_studio_thumbnail( + &mut self, + key: &str, + image: Handle, + textures: &mut EguiUserTextures, + ) { + self.studio_pending.remove(key); + self.failed.remove(key); + let texture_id = textures + .image_id(&image) + .unwrap_or_else(|| textures.add_image(EguiTextureHandle::Strong(image))); + self.texture_ids.insert(key.to_string(), texture_id); + } + + pub(crate) fn mark_studio_failed(&mut self, key: &str, reason: &str, retryable: bool) { + self.studio_pending.remove(key); + self.pending.remove(key); + self.failed.insert( + key.to_string(), + ThumbnailFailure { + reason: reason.to_string(), + retryable, + }, + ); + } + + pub fn invalidate_all(&mut self) { + self.texture_ids.clear(); + self.pending.clear(); + self.studio_pending.clear(); + self.failed.clear(); + self.prefetched_folder = None; + } + + pub fn snapshot(&self) -> ThumbnailCacheSnapshot { + ThumbnailCacheSnapshot { + texture_ids: self.texture_ids.clone(), + pending_keys: self + .pending + .keys() + .chain(self.studio_pending.iter()) + .cloned() + .collect(), + failed_keys: self.failed.keys().cloned().collect(), + } + } +} + +fn register_loaded_thumbnails( + asset_server: Res, + mut cache: ResMut, + mut textures: ResMut, +) { + let ready: Vec<(String, Handle)> = cache + .pending + .iter() + .filter(|(_, handle)| asset_server.is_loaded_with_dependencies(*handle)) + .map(|(key, handle)| (key.clone(), handle.clone())) + .collect(); + + for (key, handle) in ready { + cache.pending.remove(&key); + cache.failed.remove(&key); + let texture_id = textures + .image_id(&handle) + .unwrap_or_else(|| textures.add_image(EguiTextureHandle::Strong(handle))); + cache.texture_ids.insert(key, texture_id); + } +} + +pub fn kind_icon(kind: &EditorAssetKind) -> egui_phosphor_icons::Icon { + match kind { + EditorAssetKind::Primitive(_) => icons::CUBE, + EditorAssetKind::Light(_) => icons::LIGHTBULB, + EditorAssetKind::Model => icons::CUBE_TRANSPARENT, + EditorAssetKind::Texture => icons::IMAGE, + EditorAssetKind::Material => icons::PAINT_BRUSH, + EditorAssetKind::Level => icons::MAP_TRIFOLD, + EditorAssetKind::Prefab => icons::PACKAGE, + EditorAssetKind::PostProcessVolume => icons::CAMERA, + EditorAssetKind::PostProcessEffect => icons::SPARKLE, + EditorAssetKind::RenderingProfile => icons::SLIDERS, + EditorAssetKind::ShaderSchema => icons::CODE, + } +} + +pub fn draw_asset_cell_with( + ui: &mut egui::Ui, + asset: &EditorAsset, + texture_id: Option, + pending: bool, + failed: Option<&str>, + selected: bool, + thumbnail_size: f32, +) -> egui::Response { + let thumb_size = thumbnail_size.clamp(48.0, 112.0); + let cell_size = egui::vec2(thumb_size + 20.0, thumb_size + 30.0); + let (rect, response) = ui.allocate_exact_size(cell_size, egui::Sense::click_and_drag()); + + let fill = if selected { + crate::ui::theme::SELECTION_BG_MUTED + } else if response.hovered() { + crate::ui::theme::ELEVATED_BG + } else { + crate::ui::theme::WIDGET_BG + }; + ui.painter().rect( + rect, + 4.0, + fill, + egui::Stroke::new( + 1.0, + if selected { + crate::ui::theme::ACCENT_HOVER + } else { + crate::ui::theme::BORDER + }, + ), + egui::StrokeKind::Inside, + ); + + let thumb_rect = egui::Rect::from_min_size( + rect.min + egui::vec2(10.0, 8.0), + egui::vec2(thumb_size, thumb_size), + ); + + if let Some(texture_id) = texture_id { + ui.painter().image( + texture_id, + thumb_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + } else if pending { + ui.painter().text( + thumb_rect.center(), + egui::Align2::CENTER_CENTER, + icons::CIRCLE_NOTCH.as_str(), + egui::FontId::new(24.0, egui::FontFamily::Name("phosphor-regular".into())), + crate::ui::theme::TEXT_DIM, + ); + } else if failed.is_some() { + ui.painter().text( + thumb_rect.center(), + egui::Align2::CENTER_CENTER, + icons::WARNING_CIRCLE.as_str(), + egui::FontId::new(24.0, egui::FontFamily::Name("phosphor-regular".into())), + crate::ui::theme::TEXT_DIM, + ); + } else { + let icon = kind_icon(&asset.kind); + ui.painter().text( + thumb_rect.center(), + egui::Align2::CENTER_CENTER, + icon.as_str(), + egui::FontId::new(28.0, egui::FontFamily::Name("phosphor-regular".into())), + crate::ui::theme::TEXT, + ); + } + + ui.painter().text( + egui::pos2(rect.center().x, rect.max.y - 6.0), + egui::Align2::CENTER_BOTTOM, + asset.label.as_str(), + egui::FontId::new(11.0, egui::FontFamily::Proportional), + if selected { + crate::ui::theme::TEXT_SELECTED + } else { + crate::ui::theme::TEXT + }, + ); + + response +} + +#[derive(Clone)] +pub struct ThumbnailCacheSnapshot { + pub texture_ids: HashMap, + pub pending_keys: Vec, + pub failed_keys: Vec, +} + +impl ThumbnailCacheSnapshot { + pub fn texture_for(&self, asset: &EditorAsset) -> Option { + self.texture_ids.get(&asset_cache_key(asset)).copied() + } + + pub fn is_pending(&self, asset: &EditorAsset) -> bool { + self.pending_keys + .iter() + .any(|key| key == &asset_cache_key(asset)) + } + + pub fn is_failed(&self, asset: &EditorAsset) -> bool { + self.failed_keys + .iter() + .any(|key| key == &asset_cache_key(asset)) + } +} + +pub fn prefetch_folder_thumbnails(world: &mut World, folder: &str) { + { + let cache = world.resource::(); + if cache.prefetched_folder.as_deref() == Some(folder) { + return; + } + } + + let requests: Vec<(String, String, EditorAssetKind)> = { + let assets = world.resource::(); + assets + .assets + .iter() + .filter(|asset| asset.folder_path == folder) + .filter(|asset| { + matches!( + asset.kind, + EditorAssetKind::Texture | EditorAssetKind::Model + ) + }) + .filter_map(|asset| { + Some(( + asset_cache_key(asset), + asset.path.clone()?, + asset.kind.clone(), + )) + }) + .collect() + }; + let asset_server = world.resource::().clone(); + world.resource_scope(|world, mut cache: Mut| { + world.resource_scope(|_world, mut studio: Mut| { + for (key, path, kind) in requests { + match kind { + EditorAssetKind::Texture => cache.request_texture(key, path, &asset_server), + EditorAssetKind::Model => { + cache.request_model(key, path, &asset_server, &mut studio) + } + _ => {} + } + } + }); + }); + world + .resource_mut::() + .prefetched_folder = Some(folder.to_string()); +} + +pub fn invalidate_on_catalog_refresh(world: &mut World) { + world.resource_mut::().invalidate_all(); + if !world.contains_resource::() { + return; + } + let cleanup = { + let mut studio = world.resource_mut::(); + studio.clear_queue(); + studio.clear_jobs() + }; + if let Some(root) = cleanup.active_root { + world.commands().entity(root).despawn_children(); + world.commands().entity(root).despawn(); + } +} diff --git a/crates/editor/src/assets/thumbnails/job.rs b/crates/editor/src/assets/thumbnails/job.rs new file mode 100644 index 0000000..910d7f2 --- /dev/null +++ b/crates/editor/src/assets/thumbnails/job.rs @@ -0,0 +1,7 @@ +//! Thumbnail generation job descriptor. + +#[derive(Debug, Clone)] +pub struct ThumbnailJob { + pub cache_key: String, + pub model_path: String, +} diff --git a/crates/editor/src/assets/thumbnails/mod.rs b/crates/editor/src/assets/thumbnails/mod.rs new file mode 100644 index 0000000..71f3235 --- /dev/null +++ b/crates/editor/src/assets/thumbnails/mod.rs @@ -0,0 +1,26 @@ +//! Unified asset thumbnail pipeline (texture fast-path + model studio). + +use bevy::prelude::*; + +mod cache; +mod job; +mod sources; +mod studio; + +pub use cache::{ + draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails, + AssetThumbnailCache, AssetThumbnailsPlugin, ThumbnailCacheSnapshot, ThumbnailState, +}; +pub use job::ThumbnailJob; +pub use sources::{FbxThumbnailSource, GltfThumbnailSource, ThumbnailModelSource}; +pub use studio::{ + model_file_exists, model_scene_asset_path, ThumbnailStudio, ThumbnailStudioPlugin, +}; + +pub struct ThumbnailsPlugin; + +impl Plugin for ThumbnailsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((AssetThumbnailsPlugin, ThumbnailStudioPlugin)); + } +} diff --git a/crates/editor/src/assets/thumbnails/sources/fbx.rs b/crates/editor/src/assets/thumbnails/sources/fbx.rs new file mode 100644 index 0000000..8b90e1d --- /dev/null +++ b/crates/editor/src/assets/thumbnails/sources/fbx.rs @@ -0,0 +1,123 @@ +//! Direct FBX mesh extraction for thumbnail studio (no texture deps). + +use bevy::asset::RenderAssetUsages; +use bevy::camera::visibility::RenderLayers; +use bevy::mesh::{Indices, PrimitiveTopology}; +use bevy::prelude::*; +use bevy_ufbx::mesh::group_faces_by_material; +use bevy_ufbx::utils::convert_matrix; + +use super::ThumbnailModelSource; +use crate::assets::thumbnails::studio::THUMBNAIL_LAYER; + +pub struct FbxThumbnailSource; + +impl ThumbnailModelSource for FbxThumbnailSource { + fn spawn_preview( + &self, + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, + root: Entity, + catalog_path: &str, + ) -> bool { + let Ok(bytes) = std::fs::read(catalog_path) else { + warn!("FBX thumbnail: could not read {catalog_path}"); + return false; + }; + + let scene = match ufbx::load_memory( + &bytes, + ufbx::LoadOpts { + target_unit_meters: 1.0, + target_axes: ufbx::CoordinateAxes::right_handed_y_up(), + ..Default::default() + }, + ) { + Ok(scene) => scene, + Err(err) => { + warn!("FBX thumbnail: failed to parse {catalog_path}: {err:?}"); + return false; + } + }; + + let material = materials.add(StandardMaterial { + base_color: Color::srgb(0.72, 0.72, 0.75), + ..default() + }); + + let mut spawned = false; + for node in scene.nodes.as_ref().iter() { + let Some(mesh_ref) = node.mesh.as_ref() else { + continue; + }; + let ufbx_mesh = mesh_ref.as_ref(); + if ufbx_mesh.num_vertices == 0 || ufbx_mesh.faces.as_ref().is_empty() { + continue; + } + + for (_material_idx, indices) in group_faces_by_material(ufbx_mesh) { + if indices.is_empty() { + continue; + } + + let mesh_handle = meshes.add(build_mesh(ufbx_mesh, &indices)); + let transform = Transform::from_matrix(convert_matrix(&node.geometry_to_world)); + + commands.entity(root).with_children(|parent| { + parent.spawn(( + RenderLayers::layer(THUMBNAIL_LAYER), + Mesh3d(mesh_handle), + MeshMaterial3d(material.clone()), + transform, + )); + }); + spawned = true; + } + } + + if !spawned { + warn!("FBX thumbnail: no renderable meshes in {catalog_path}"); + } + spawned + } +} + +fn build_mesh(ufbx_mesh: &ufbx::Mesh, indices: &[u32]) -> Mesh { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + let positions: Vec<[f32; 3]> = ufbx_mesh + .vertex_position + .values + .as_ref() + .iter() + .map(|v| [v.x as f32, v.y as f32, v.z as f32]) + .collect(); + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + + if ufbx_mesh.vertex_normal.exists { + let normals: Vec<[f32; 3]> = (0..ufbx_mesh.num_vertices) + .map(|i| { + let n = ufbx_mesh.vertex_normal[i]; + [n.x as f32, n.y as f32, n.z as f32] + }) + .collect(); + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + } + + if ufbx_mesh.vertex_uv.exists { + let uvs: Vec<[f32; 2]> = (0..ufbx_mesh.num_vertices) + .map(|i| { + let uv = ufbx_mesh.vertex_uv[i]; + [uv.x as f32, uv.y as f32] + }) + .collect(); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + } + + mesh.insert_indices(Indices::U32(indices.to_vec())); + mesh +} diff --git a/crates/editor/src/assets/thumbnails/sources/gltf.rs b/crates/editor/src/assets/thumbnails/sources/gltf.rs new file mode 100644 index 0000000..91f100a --- /dev/null +++ b/crates/editor/src/assets/thumbnails/sources/gltf.rs @@ -0,0 +1,82 @@ +//! glTF/GLB fast-path texture extraction for asset browser thumbnails. + +use std::path::Path; + +/// Returns an asset-server path for the first base-color texture referenced by a glTF/GLB file. +/// +/// Embedded GLB buffers are skipped so callers can fall back to the render studio. +pub fn gltf_base_color_texture_path(model_asset_path: &str) -> Option { + let model_path = Path::new(model_asset_path); + if !is_gltf_path(model_path) { + return None; + } + + let (document, ..) = gltf::import(model_path).ok()?; + for material in document.materials() { + let Some(tex_info) = material.pbr_metallic_roughness().base_color_texture() else { + continue; + }; + let texture = document.textures().nth(tex_info.texture().index())?; + let image = document.images().nth(texture.source().index())?; + let uri = match image.source() { + gltf::image::Source::Uri { uri, .. } => uri, + gltf::image::Source::View { .. } => continue, + }; + if let Some(path) = resolve_texture_uri(model_path, uri) { + return Some(path); + } + } + None +} + +pub struct GltfThumbnailSource; + +fn is_gltf_path(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gltf" | "glb")) +} + +fn resolve_texture_uri(model_path: &Path, uri: &str) -> Option { + if uri.starts_with("data:") { + return None; + } + let model_dir = model_path.parent()?; + let texture_path = model_dir.join(uri); + normalize_asset_path(&texture_path) +} + +fn normalize_asset_path(path: &Path) -> Option { + let raw = path.to_string_lossy().replace('\\', "/"); + if raw.starts_with("assets/") { + return Some(raw); + } + let file_name = path.file_name()?.to_str()?; + if file_name.is_empty() { + return None; + } + Some(format!("assets/{file_name}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn normalize_asset_path_keeps_assets_prefix() { + assert_eq!( + normalize_asset_path(Path::new("assets/textures/albedo.png")), + Some("assets/textures/albedo.png".into()) + ); + } + + #[test] + fn resolve_texture_uri_relative_to_model() { + let model = PathBuf::from("assets/models/chair.gltf"); + assert_eq!( + resolve_texture_uri(&model, "textures/chair_albedo.png"), + Some("assets/models/textures/chair_albedo.png".into()) + ); + } +} diff --git a/crates/editor/src/assets/thumbnails/sources/mod.rs b/crates/editor/src/assets/thumbnails/sources/mod.rs new file mode 100644 index 0000000..b83e999 --- /dev/null +++ b/crates/editor/src/assets/thumbnails/sources/mod.rs @@ -0,0 +1,33 @@ +//! Model file sources for the thumbnail render studio. + +mod fbx; +pub(crate) mod gltf; + +pub use fbx::FbxThumbnailSource; +pub use gltf::GltfThumbnailSource; + +use bevy::prelude::*; + +/// Spawns untextured or scene-root preview content under `root` for the thumbnail studio. +pub trait ThumbnailModelSource: Send + Sync { + fn spawn_preview( + &self, + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, + root: Entity, + catalog_path: &str, + ) -> bool; +} + +pub fn source_for_extension(path: &str) -> Option<&'static dyn ThumbnailModelSource> { + if path.ends_with(".fbx") { + Some(&FbxThumbnailSource) + } else { + None + } +} + +pub fn uses_scene_root(path: &str) -> bool { + !path.ends_with(".fbx") +} diff --git a/crates/editor/src/assets/thumbnails/studio.rs b/crates/editor/src/assets/thumbnails/studio.rs new file mode 100644 index 0000000..a76b359 --- /dev/null +++ b/crates/editor/src/assets/thumbnails/studio.rs @@ -0,0 +1,544 @@ +//! Offscreen model thumbnail studio for FBX and untextured glTF assets. + +use std::collections::VecDeque; +use std::path::Path; + +use bevy::camera::visibility::RenderLayers; +use bevy::camera::RenderTarget; +use bevy::gltf::GltfAssetLabel; +use bevy::math::bounding::Aabb3d; +use bevy::mesh::VertexAttributeValues; +use bevy::prelude::*; +use bevy::render::render_resource::TextureFormat; +use bevy::scene::SceneRoot; +use bevy_egui::EguiUserTextures; +use shared::ModelRef; + +use super::cache::AssetThumbnailCache; +use super::job::ThumbnailJob; +use super::sources::{source_for_extension, uses_scene_root}; +use crate::assets::asset_server_path; +use crate::infra::EditorOnly; + +pub(crate) const THUMB_SIZE: u32 = 128; +pub(crate) const THUMBNAIL_LAYER: usize = 31; +const MESH_WARMUP_FRAMES: u8 = 12; +const POST_ATTACH_FRAMES: u8 = 8; +const RENDER_FRAMES: u8 = 3; +const COOLDOWN_FRAMES: u8 = 2; +const LOAD_TIMEOUT_FRAMES: u32 = 240; + +#[derive(Component)] +struct ThumbnailStudioCamera; + +#[derive(Component)] +struct ThumbnailStudioLight; + +#[derive(Component)] +struct ThumbnailStudioLayer; + +#[derive(Component)] +struct ThumbnailSceneRoot; + +#[derive(Resource)] +pub struct ThumbnailStudio { + camera: Entity, + render_image: Handle, + _lights: Vec, + queue: VecDeque, + active: Option, + cooldown_frames: u8, +} + +struct ActiveModelThumbnail { + cache_key: String, + model_path: String, + root: Entity, + meshes_ready: bool, + mesh_warmup_frames: u8, + camera_active: bool, + post_attach_frames: u8, + framed: bool, + frames_remaining: u8, + wait_frames: u32, +} + +pub struct ThumbnailStudioPlugin; + +impl Plugin for ThumbnailStudioPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup_thumbnail_studio) + .add_systems(PostUpdate, apply_thumbnail_studio_layers) + .add_systems(Last, process_thumbnail_studio); + } +} + +impl ThumbnailStudio { + pub fn enqueue(&mut self, cache_key: String, model_path: String) -> bool { + if self + .active + .as_ref() + .is_some_and(|active| active.cache_key == cache_key) + { + return false; + } + if self.queue.iter().any(|job| job.cache_key == cache_key) { + return false; + } + self.queue.push_back(ThumbnailJob { + cache_key, + model_path, + }); + true + } + + pub fn clear_queue(&mut self) { + self.queue.clear(); + self.cooldown_frames = 0; + } + + pub fn clear_jobs(&mut self) -> StudioCleanup { + StudioCleanup { + camera: self.camera, + active_root: self.active.take().map(|active| active.root), + } + } +} + +pub struct StudioCleanup { + pub camera: Entity, + pub active_root: Option, +} + +pub fn model_scene_asset_path(path: &str, scene_index: usize) -> String { + let path = asset_server_path(path); + if path.ends_with(".fbx") { + ModelRef::fbx_scene_asset_path(&path, scene_index) + } else { + GltfAssetLabel::Scene(scene_index) + .from_asset(path) + .to_string() + } +} + +fn setup_thumbnail_studio(mut commands: Commands, mut images: ResMut>) { + let render_image = images.add(Image::new_target_texture( + THUMB_SIZE, + THUMB_SIZE, + TextureFormat::Rgba8UnormSrgb, + None, + )); + + let camera = commands + .spawn(( + Name::new("Thumbnail Studio Camera"), + EditorOnly, + ThumbnailStudioCamera, + ThumbnailStudioLayer, + RenderLayers::layer(THUMBNAIL_LAYER), + Camera3d::default(), + Camera { + is_active: false, + order: -50, + clear_color: ClearColorConfig::Custom(Color::srgb(0.12, 0.12, 0.14)), + ..default() + }, + RenderTarget::Image(render_image.clone().into()), + Transform::from_xyz(2.0, 1.4, 2.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y), + Projection::Perspective(PerspectiveProjection { + fov: 40.0_f32.to_radians(), + ..default() + }), + )) + .id(); + + let key_light = commands + .spawn(( + EditorOnly, + ThumbnailStudioLight, + ThumbnailStudioLayer, + RenderLayers::layer(THUMBNAIL_LAYER), + DirectionalLight { + illuminance: 12_000.0, + shadows_enabled: false, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.8, 0.9, 0.0)), + )) + .id(); + + let fill_light = commands + .spawn(( + EditorOnly, + ThumbnailStudioLight, + ThumbnailStudioLayer, + RenderLayers::layer(THUMBNAIL_LAYER), + DirectionalLight { + illuminance: 3_500.0, + shadows_enabled: false, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.4, -2.2, 0.0)), + )) + .id(); + + commands.insert_resource(ThumbnailStudio { + camera, + render_image, + _lights: vec![key_light, fill_light], + queue: VecDeque::new(), + active: None, + cooldown_frames: 0, + }); +} + +#[allow(clippy::too_many_arguments)] +fn process_thumbnail_studio( + mut commands: Commands, + mut studio: ResMut, + mut cache: ResMut, + mut textures: ResMut, + mut materials: ResMut>, + mut mesh_storage: ResMut>, + asset_server: Res, + mut cameras: Query<&mut Camera, With>, + mut camera_transforms: Query<&mut Transform, With>, + children: Query<&Children>, + transforms: Query<&GlobalTransform>, + mesh3d: Query<&Mesh3d>, +) { + if studio.cooldown_frames > 0 { + studio.cooldown_frames -= 1; + return; + } + + if let Some(mut active) = studio.active.take() { + if !active.framed { + active.wait_frames += 1; + + if !active.meshes_ready { + if thumbnail_meshes_present(active.root, &children, &mesh3d, &mesh_storage) { + active.meshes_ready = true; + active.mesh_warmup_frames = 0; + } + } else if active.mesh_warmup_frames < MESH_WARMUP_FRAMES { + active.mesh_warmup_frames += 1; + } else if frame_thumbnail_scene( + active.root, + &children, + &transforms, + &mesh3d, + &mesh_storage, + &mut camera_transforms, + ) { + active.framed = true; + active.post_attach_frames = 0; + studio.active = Some(active); + return; + } + + if active.wait_frames >= LOAD_TIMEOUT_FRAMES { + warn!( + "Model thumbnail timed out framing {} (meshes may still be loading)", + active.model_path + ); + fail_active_thumbnail( + &mut commands, + &mut studio, + &mut cache, + &mut cameras, + active, + "load timeout", + true, + ); + } else { + studio.active = Some(active); + } + return; + } + + if !active.camera_active { + active.post_attach_frames += 1; + if active.post_attach_frames >= POST_ATTACH_FRAMES { + if let Ok(mut camera) = cameras.get_mut(studio.camera) { + camera.is_active = true; + } + active.camera_active = true; + active.frames_remaining = RENDER_FRAMES; + } + studio.active = Some(active); + return; + } + + if active.camera_active { + if active.frames_remaining > 0 { + active.frames_remaining -= 1; + studio.active = Some(active); + } else { + cache.complete_studio_thumbnail( + &active.cache_key, + studio.render_image.clone(), + &mut textures, + ); + finish_active_thumbnail(&mut commands, &mut studio, &mut cameras, active); + } + return; + } + + studio.active = Some(active); + return; + } + + let Some(job) = studio.queue.pop_front() else { + deactivate_studio_camera(&mut cameras, studio.camera); + return; + }; + + let mut root_entity = commands.spawn(( + EditorOnly, + ThumbnailSceneRoot, + ThumbnailStudioLayer, + RenderLayers::layer(THUMBNAIL_LAYER), + Transform::default(), + )); + + if uses_scene_root(&job.model_path) { + root_entity.insert(SceneRoot( + asset_server.load(model_scene_asset_path(&job.model_path, 0)), + )); + } + + let root = root_entity.id(); + + let content_spawned = if let Some(source) = source_for_extension(&job.model_path) { + source.spawn_preview( + &mut commands, + &mut mesh_storage, + &mut materials, + root, + &job.model_path, + ) + } else { + uses_scene_root(&job.model_path) + }; + + if source_for_extension(&job.model_path).is_some() && !content_spawned { + fail_active_thumbnail( + &mut commands, + &mut studio, + &mut cache, + &mut cameras, + ActiveModelThumbnail { + cache_key: job.cache_key, + model_path: job.model_path, + root, + meshes_ready: false, + mesh_warmup_frames: 0, + camera_active: false, + post_attach_frames: 0, + framed: false, + frames_remaining: 0, + wait_frames: 0, + }, + "no renderable meshes", + true, + ); + return; + } + + studio.active = Some(ActiveModelThumbnail { + cache_key: job.cache_key, + model_path: job.model_path, + root, + meshes_ready: false, + mesh_warmup_frames: 0, + camera_active: false, + post_attach_frames: 0, + framed: false, + frames_remaining: 0, + wait_frames: 0, + }); +} + +fn finish_active_thumbnail( + commands: &mut Commands, + studio: &mut ThumbnailStudio, + cameras: &mut Query<&mut Camera, With>, + active: ActiveModelThumbnail, +) { + despawn_thumbnail_root(commands, active.root); + deactivate_studio_camera(cameras, studio.camera); + studio.cooldown_frames = COOLDOWN_FRAMES; +} + +fn fail_active_thumbnail( + commands: &mut Commands, + studio: &mut ThumbnailStudio, + cache: &mut AssetThumbnailCache, + cameras: &mut Query<&mut Camera, With>, + active: ActiveModelThumbnail, + reason: &str, + retryable: bool, +) { + cache.mark_studio_failed(&active.cache_key, reason, retryable); + finish_active_thumbnail(commands, studio, cameras, active); +} + +fn apply_thumbnail_studio_layers( + mut commands: Commands, + roots: Query>, + children: Query<&Children>, + tagged: Query<(), With>, +) { + for root in &roots { + let mut stack = vec![root]; + while let Some(entity) = stack.pop() { + if tagged.get(entity).is_err() { + commands + .entity(entity) + .insert((ThumbnailStudioLayer, RenderLayers::layer(THUMBNAIL_LAYER))); + } + if let Ok(kids) = children.get(entity) { + stack.extend(kids.iter()); + } + } + } +} + +fn deactivate_studio_camera( + cameras: &mut Query<&mut Camera, With>, + camera: Entity, +) { + if let Ok(mut camera) = cameras.get_mut(camera) { + camera.is_active = false; + } +} + +fn despawn_thumbnail_root(commands: &mut Commands, root: Entity) { + commands.entity(root).despawn_children(); + commands.entity(root).despawn(); +} + +fn thumbnail_meshes_present( + root: Entity, + children: &Query<&Children>, + meshes: &Query<&Mesh3d>, + mesh_assets: &Assets, +) -> bool { + let mut entities = Vec::new(); + collect_descendants(root, children, &mut entities); + + entities.iter().any(|&entity| { + let Ok(mesh3d) = meshes.get(entity) else { + return false; + }; + mesh_assets + .get(&mesh3d.0) + .is_some_and(|mesh| mesh_positions(mesh).next().is_some()) + }) +} + +fn mesh_positions(mesh: &Mesh) -> impl Iterator + '_ { + let positions = mesh + .attribute(Mesh::ATTRIBUTE_POSITION) + .and_then(|values| match values { + VertexAttributeValues::Float32x3(positions) => Some(positions.as_slice()), + _ => None, + }); + positions + .into_iter() + .flat_map(|slice| slice.iter().copied()) +} + +fn frame_thumbnail_scene( + root: Entity, + children: &Query<&Children>, + transforms: &Query<&GlobalTransform>, + meshes: &Query<&Mesh3d>, + mesh_assets: &Assets, + camera_transforms: &mut Query<&mut Transform, With>, +) -> bool { + let mut entities = Vec::new(); + collect_descendants(root, children, &mut entities); + + let mut min = Vec3::splat(f32::INFINITY); + let mut max = Vec3::splat(f32::NEG_INFINITY); + let mut found_mesh = false; + + for entity in entities { + let Ok(mesh3d) = meshes.get(entity) else { + continue; + }; + let Ok(global) = transforms.get(entity) else { + continue; + }; + + if let Some(mesh) = mesh_assets.get(&mesh3d.0) { + let mut local_min = Vec3::splat(f32::INFINITY); + let mut local_max = Vec3::splat(f32::NEG_INFINITY); + let mut has_positions = false; + for position in mesh_positions(mesh) { + let p = Vec3::from_array(position); + local_min = local_min.min(p); + local_max = local_max.max(p); + has_positions = true; + } + if has_positions { + found_mesh = true; + for corner in aabb_corners(&Aabb3d::from_min_max( + Vec3A::from(local_min), + Vec3A::from(local_max), + )) { + let world = global.transform_point(corner); + min = min.min(world); + max = max.max(world); + } + } + } + } + + if !found_mesh { + return false; + } + + let center = (min + max) * 0.5; + let radius = (max - min).length().max(0.25) * 0.5; + let distance = radius / (20.0_f32.to_radians()).tan() * 1.35; + let eye = center + Vec3::new(1.0, 0.75, 1.0).normalize() * distance; + + if let Ok(mut transform) = camera_transforms.single_mut() { + *transform = Transform::from_translation(eye).looking_at(center, Vec3::Y); + } + + true +} + +fn collect_descendants(entity: Entity, children: &Query<&Children>, out: &mut Vec) { + out.push(entity); + let Ok(child_list) = children.get(entity) else { + return; + }; + for child in child_list.iter() { + collect_descendants(child, children, out); + } +} + +fn aabb_corners(aabb: &Aabb3d) -> [Vec3; 8] { + let min = Vec3::from(aabb.min); + let max = Vec3::from(aabb.max); + [ + Vec3::new(min.x, min.y, min.z), + Vec3::new(max.x, min.y, min.z), + Vec3::new(min.x, max.y, min.z), + Vec3::new(max.x, max.y, min.z), + Vec3::new(min.x, min.y, max.z), + Vec3::new(max.x, min.y, max.z), + Vec3::new(min.x, max.y, max.z), + Vec3::new(max.x, max.y, max.z), + ] +} + +/// Skip studio jobs when the source file is missing on disk. +pub fn model_file_exists(catalog_path: &str) -> bool { + Path::new(catalog_path).is_file() +} diff --git a/crates/editor/src/bin/main.rs b/crates/editor/src/bin/main.rs new file mode 100644 index 0000000..bd3fce6 --- /dev/null +++ b/crates/editor/src/bin/main.rs @@ -0,0 +1,13 @@ +//! In-process editor binary entry point. + +fn main() { + if std::env::var_os("WGPU_VALIDATION").is_none() { + // SAFETY: called before any threads or GPU init. + unsafe { std::env::set_var("WGPU_VALIDATION", "0") }; + } + + let mut app = bevy::prelude::App::new(); + editor::configure_editor_app(&mut app); + editor::register_game_editor_plugins(&mut app); + app.run(); +} diff --git a/crates/editor/src/ext/brp.rs b/crates/editor/src/ext/brp.rs new file mode 100644 index 0000000..36bd6e3 --- /dev/null +++ b/crates/editor/src/ext/brp.rs @@ -0,0 +1,13 @@ +use bevy::prelude::*; +use bevy::remote::{http::RemoteHttpPlugin, RemotePlugin}; + +/// Bevy Remote Protocol support for future external editor, automation, and AI +/// tooling. This is editor-only so the shipped game does not expose a port. +pub struct BrpPlugin; + +impl Plugin for BrpPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(RemotePlugin::default()) + .add_plugins(RemoteHttpPlugin::default()); + } +} diff --git a/crates/editor/src/ext/command_queue.rs b/crates/editor/src/ext/command_queue.rs new file mode 100644 index 0000000..4f33e7c --- /dev/null +++ b/crates/editor/src/ext/command_queue.rs @@ -0,0 +1,15 @@ +//! Deferred editor command execution (palette, BRP bridge). + +use bevy::prelude::*; + +#[derive(Resource, Default)] +pub struct PendingEditorCommands { + pub queue: Vec, +} + +pub fn queue_editor_command(world: &mut World, name: String) { + world + .resource_mut::() + .queue + .push(name); +} diff --git a/crates/editor/src/ext/extensibility.rs b/crates/editor/src/ext/extensibility.rs new file mode 100644 index 0000000..06c8893 --- /dev/null +++ b/crates/editor/src/ext/extensibility.rs @@ -0,0 +1,370 @@ +//! Editor extensibility: command registry, inspector sections, and extension hooks. + +use bevy::prelude::*; +use bevy_egui::{egui, EguiContexts, EguiPrimaryContextPass}; + +use crate::history::group_selection_with_history; +use crate::state::{EditorMode, PlayPossession}; +use crate::ui::helpers::{ + reset_scene_lighting_to_project_defaults, toggle_play_mode, toggle_play_paused, +}; +use crate::ui::selection_ops::focus_editor_camera_on_selection; +use crate::ui::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM}; +use crate::ui::UiState; + +/// Named editor command invokable from the palette and BRP. +pub trait EditorCommand: Send + Sync { + fn name(&self) -> &str; + fn execute(&self, world: &mut World); +} + +#[derive(Resource, Default)] +pub struct EditorCommandRegistry { + commands: Vec>, +} + +impl EditorCommandRegistry { + pub fn register(&mut self, command: Box) { + self.commands.push(command); + } + + pub fn names(&self) -> Vec { + self.commands + .iter() + .map(|cmd| cmd.name().to_string()) + .collect() + } + + pub fn run(&self, world: &mut World, name: &str) -> bool { + if let Some(command) = self.commands.iter().find(|cmd| cmd.name() == name) { + command.execute(world); + true + } else { + false + } + } +} + +/// Custom inspector body for level objects (game crates register via [`register_actor_inspector_section`]). +pub trait ActorInspectorSection: Send + Sync { + fn id(&self) -> &str; + fn title(&self) -> &str; + fn order(&self) -> i32; + fn applies_to(&self, world: &World, entity: Entity) -> bool; + fn ui(&self, world: &mut World, ui: &mut egui::Ui, entity: Entity); +} + +#[derive(Resource, Default)] +pub struct ActorInspectorSectionRegistry { + sections: Vec>, +} + +impl ActorInspectorSectionRegistry { + pub fn register(&mut self, section: Box) { + self.sections.push(section); + } + + pub fn sections_for(&self, world: &World, entity: Entity) -> Vec<&dyn ActorInspectorSection> { + let mut matched: Vec<_> = self + .sections + .iter() + .filter(|section| section.applies_to(world, entity)) + .map(|section| section.as_ref()) + .collect(); + matched.sort_by_key(|section| section.order()); + matched + } + + pub fn draw_sections(&self, world: &mut World, ui: &mut egui::Ui, entity: Entity) { + let mut matched: Vec<_> = self + .sections + .iter() + .filter(|section| section.applies_to(world, entity)) + .collect(); + matched.sort_by_key(|section| section.order()); + for section in matched { + section.ui(world, ui, entity); + } + } +} + +pub fn register_actor_inspector_section(app: &mut App, section: Box) { + if !app + .world() + .contains_resource::() + { + app.init_resource::(); + } + app.world_mut() + .resource_mut::() + .register(section); +} + +/// Optional extension point for registering editor panels and menu hooks. +pub trait EditorExtension: Send + Sync { + fn name(&self) -> &str; + fn register(&self, _app: &mut App) {} +} + +/// Formal editor plugin surface for game-specific panels (H5). +pub trait EditorPlugin: Send + Sync { + fn name(&self) -> &str; + fn register(&self, app: &mut App); +} + +#[derive(Resource, Default)] +pub struct EditorExtensions { + pub extensions: Vec>, +} + +#[derive(Resource, Default)] +pub struct RegisteredEditorPlugins { + pub plugins: Vec>, +} + +#[derive(Resource, Default)] +pub struct CommandPalette { + pub open: bool, + pub filter: String, + pub pending_run: Option, +} + +pub struct ExtensibilityPlugin; + +impl Plugin for ExtensibilityPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(Startup, register_builtin_commands) + .add_systems(Update, run_palette_commands) + .add_systems(EguiPrimaryContextPass, command_palette_ui); + } +} + +pub fn register_editor_plugin(app: &mut App, plugin: Box) { + plugin.register(app); + app.world_mut() + .resource_mut::() + .plugins + .push(plugin); +} + +fn register_builtin_commands(mut registry: ResMut) { + registry.register(Box::new(TogglePlayCommand)); + registry.register(Box::new(TogglePlayPausedCommand)); + registry.register(Box::new(TogglePossessionCommand)); + registry.register(Box::new(ResetLightingCommand)); + registry.register(Box::new(GroupSelectionCommand)); + registry.register(Box::new(FocusSelectionCommand)); + registry.register(Box::new(CreatePostProcessVolumeCommand)); + registry.register(Box::new(FocusActiveVolumesCommand)); + registry.register(Box::new(SelectVolumesAtCameraCommand)); +} + +fn run_palette_commands(world: &mut World) { + let pending = world.resource_mut::().pending_run.take(); + if let Some(name) = pending { + dispatch_editor_command(world, &name); + } + let names = std::mem::take( + &mut world + .resource_mut::() + .queue, + ); + for name in names { + dispatch_editor_command(world, &name); + } +} + +fn dispatch_editor_command(world: &mut World, name: &str) { + let ran = world + .resource_scope(|world, registry: Mut| registry.run(world, name)); + if !ran { + warn!("unknown editor command: {name}"); + } +} + +struct TogglePlayCommand; + +impl EditorCommand for TogglePlayCommand { + fn name(&self) -> &str { + "play.toggle" + } + + fn execute(&self, world: &mut World) { + toggle_play_mode(world); + } +} + +struct TogglePlayPausedCommand; + +impl EditorCommand for TogglePlayPausedCommand { + fn name(&self) -> &str { + "play.toggle_pause" + } + + fn execute(&self, world: &mut World) { + toggle_play_paused(world); + } +} + +struct TogglePossessionCommand; + +impl EditorCommand for TogglePossessionCommand { + fn name(&self) -> &str { + "play.toggle_possession" + } + + fn execute(&self, world: &mut World) { + if *world.resource::>().get() != EditorMode::Playing { + return; + } + let next = match *world.resource::() { + PlayPossession::Possessed => PlayPossession::Ejected, + PlayPossession::Ejected => PlayPossession::Possessed, + }; + *world.resource_mut::() = next; + } +} + +struct ResetLightingCommand; + +impl EditorCommand for ResetLightingCommand { + fn name(&self) -> &str { + "scene.reset_lighting" + } + + fn execute(&self, world: &mut World) { + reset_scene_lighting_to_project_defaults(world); + } +} + +struct GroupSelectionCommand; + +impl EditorCommand for GroupSelectionCommand { + fn name(&self) -> &str { + "selection.group" + } + + fn execute(&self, world: &mut World) { + let selected: Vec = world + .resource::() + .selected_entities + .iter() + .collect(); + group_selection_with_history(world, &selected); + } +} + +struct FocusSelectionCommand; + +impl EditorCommand for FocusSelectionCommand { + fn name(&self) -> &str { + "selection.focus" + } + + fn execute(&self, world: &mut World) { + focus_editor_camera_on_selection(world); + } +} + +struct CreatePostProcessVolumeCommand; + +impl EditorCommand for CreatePostProcessVolumeCommand { + fn name(&self) -> &str { + "rendering.create_volume" + } + + fn execute(&self, world: &mut World) { + crate::rendering_diagnostics::spawn_post_process_volume_at_camera(world); + } +} + +struct FocusActiveVolumesCommand; + +impl EditorCommand for FocusActiveVolumesCommand { + fn name(&self) -> &str { + "rendering.focus_active_volumes" + } + + fn execute(&self, world: &mut World) { + crate::rendering_diagnostics::select_volumes_at_camera(world); + focus_editor_camera_on_selection(world); + } +} + +struct SelectVolumesAtCameraCommand; + +impl EditorCommand for SelectVolumesAtCameraCommand { + fn name(&self) -> &str { + "rendering.select_volumes_at_camera" + } + + fn execute(&self, world: &mut World) { + crate::rendering_diagnostics::select_volumes_at_camera(world); + } +} + +fn command_palette_ui( + mut contexts: EguiContexts, + mut palette: ResMut, + registry: Res, +) -> Result { + let Ok(ctx) = contexts.ctx_mut() else { + return Ok(()); + }; + + apply_editor_theme(ctx); + + if ctx.input(|input| input.modifiers.command && input.key_pressed(egui::Key::P)) { + palette.open = true; + } + + if !palette.open { + return Ok(()); + } + + let mut open = palette.open; + egui::Window::new("Command Palette") + .open(&mut open) + .default_width(400.0) + .frame(egui::Frame::window(&ctx.style()).fill(PANEL_BG)) + .show(ctx, |ui| { + ui.label( + egui::RichText::new("Ctrl+P — filter and run commands") + .color(TEXT_DIM) + .small(), + ); + ui.add( + egui::TextEdit::singleline(&mut palette.filter) + .hint_text("Type to filter...") + .desired_width(f32::INFINITY), + ); + ui.separator(); + egui::ScrollArea::vertical() + .max_height(280.0) + .show(ui, |ui| { + for name in registry.names() { + if !palette.filter.is_empty() + && !name.to_lowercase().contains(&palette.filter.to_lowercase()) + { + continue; + } + let response = + ui.selectable_label(false, egui::RichText::new(&name).color(TEXT)); + if response.clicked() { + palette.pending_run = Some(name); + palette.open = false; + ui.close(); + } + } + }); + }); + palette.open = open; + + Ok(()) +} diff --git a/crates/editor/src/ext/game_inspector.rs b/crates/editor/src/ext/game_inspector.rs new file mode 100644 index 0000000..342e75b --- /dev/null +++ b/crates/editor/src/ext/game_inspector.rs @@ -0,0 +1,50 @@ +//! Game-specific [`ActorInspectorSection`] implementations (ADR 0007 dogfood). + +use bevy::prelude::*; +use bevy_egui::egui; +use shared::{LevelObject, WeaponSpawn}; + +use super::extensibility::{register_actor_inspector_section, ActorInspectorSection}; + +/// Registers inspector sections declared in [`game::editor_ext::actor_inspector_section_ids`]. +pub fn register_game_inspector_sections(app: &mut App) { + for id in game::editor_ext::actor_inspector_section_ids() { + match *id { + "game.fps_weapon_stats" => { + register_actor_inspector_section(app, Box::new(FpsWeaponStatsSection)); + } + other => { + warn!("unknown game actor inspector section id: {other}"); + } + } + } +} + +struct FpsWeaponStatsSection; + +impl ActorInspectorSection for FpsWeaponStatsSection { + fn id(&self) -> &str { + "game.fps_weapon_stats" + } + + fn title(&self) -> &str { + "Weapon Stats (Demo)" + } + + fn order(&self) -> i32 { + 200 + } + + fn applies_to(&self, world: &World, entity: Entity) -> bool { + world + .get_entity(entity) + .is_ok_and(|e| e.contains::() && e.contains::()) + } + + fn ui(&self, _world: &mut World, ui: &mut egui::Ui, entity: Entity) { + ui.collapsing(self.title(), |ui| { + ui.label(format!("Entity {entity:?}")); + ui.small("Registered from `game::editor_ext` — extend with gameplay fields."); + }); + } +} diff --git a/crates/editor/src/ext/game_panels.rs b/crates/editor/src/ext/game_panels.rs new file mode 100644 index 0000000..40c3e80 --- /dev/null +++ b/crates/editor/src/ext/game_panels.rs @@ -0,0 +1,48 @@ +//! Renders game-registered editor panel state (ADR 0007 dogfood UI). + +use bevy::prelude::*; +use bevy_egui::{egui, EguiContexts, EguiPrimaryContextPass}; + +use game::editor_ext::FpsDemoPanelState; + +pub struct GameEditorPanelsPlugin; + +impl Plugin for GameEditorPanelsPlugin { + fn build(&self, app: &mut App) { + app.add_systems(EguiPrimaryContextPass, fps_demo_panel_ui); + } +} + +fn fps_demo_panel_ui(mut contexts: EguiContexts, mut panel: ResMut) -> Result { + let Ok(ctx) = contexts.ctx_mut() else { + return Ok(()); + }; + + if panel.headline.is_empty() { + panel.headline = "FPS Foundation Demo".into(); + } + + if ctx.input(|input| { + input.modifiers.command && input.key_pressed(egui::Key::G) && input.modifiers.shift + }) { + panel.open = !panel.open; + } + + if !panel.open { + return Ok(()); + } + + let mut open = panel.open; + egui::Window::new("FPS Demo (Game EditorPlugin)") + .open(&mut open) + .default_width(280.0) + .show(ctx, |ui| { + ui.label("Registered from `game::editor_ext` via ADR 0007."); + ui.small("Toggle: Ctrl+Shift+G"); + ui.separator(); + ui.heading(&panel.headline); + ui.label("Game crate owns panel state; editor renders the dock window."); + }); + panel.open = open; + Ok(()) +} diff --git a/crates/editor/src/ext/hot_reload.rs b/crates/editor/src/ext/hot_reload.rs new file mode 100644 index 0000000..f3996a9 --- /dev/null +++ b/crates/editor/src/ext/hot_reload.rs @@ -0,0 +1,149 @@ +//! Dev-only hot reload polling and status for the `game_hot` dylib. + +use std::path::PathBuf; +use std::sync::{mpsc, Mutex}; +use std::time::Duration; + +use bevy::prelude::*; +use game::{hot_reload_subscribe, on_hot_reload}; +use hot_lib_reloader::LibReloadObserver; +use notify::Event; + +/// Status label shown in the bottom status bar. +#[derive(Resource, Debug, Clone)] +pub struct HotReloadState { + pub label: String, +} + +impl Default for HotReloadState { + fn default() -> Self { + Self { + label: "Hot reload: idle".into(), + } + } +} + +pub struct HotReloadPlugin; + +impl Plugin for HotReloadPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Startup, spawn_dylib_watcher) + .add_systems(Last, poll_lib_reload) + .add_systems(Last, manual_hot_reload); + } +} + +fn spawn_dylib_watcher(mut commands: Commands) { + let Some(path) = game_hot_dylib_path() else { + return; + }; + + let (tx, rx) = mpsc::channel(); + let watch_path = path.clone(); + std::thread::spawn(move || { + use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher}; + + let tx = tx.clone(); + let mut watcher = RecommendedWatcher::new( + move |result: notify::Result| { + if let Ok(event) = result { + if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) { + let _ = tx.send(()); + } + } + }, + notify::Config::default(), + ) + .expect("hot reload watcher"); + + if let Some(parent) = watch_path.parent() { + let _ = watcher.watch(parent, RecursiveMode::NonRecursive); + } + + loop { + std::thread::sleep(Duration::from_secs(3600)); + } + }); + + commands.insert_resource(DylibWatchReceiver(Mutex::new(rx))); + info!("Watching for game_hot dylib changes at {}", path.display()); +} + +#[derive(Resource)] +struct DylibWatchReceiver(Mutex>); + +fn poll_lib_reload(world: &mut World, mut observer: Local>) { + if observer.is_none() { + *observer = Some(hot_reload_subscribe()); + } + + let mut building = false; + if let Some(watch) = world.get_resource::() { + if let Ok(rx) = watch.0.lock() { + while rx.try_recv().is_ok() { + building = true; + } + } + } + if building { + world.resource_mut::().label = "Hot reload: building…".into(); + } + + let Some(obs) = observer.as_ref() else { + return; + }; + + if obs.wait_for_reload_timeout(Duration::ZERO) { + on_hot_reload(world); + world.resource_mut::().label = "Hot reload: reloaded".into(); + info!("game_hot dylib reloaded"); + } +} + +fn manual_hot_reload(world: &mut World) { + let ctrl = world + .resource::>() + .pressed(KeyCode::ControlLeft) + || world + .resource::>() + .pressed(KeyCode::ControlRight); + let shift = world + .resource::>() + .pressed(KeyCode::ShiftLeft) + || world + .resource::>() + .pressed(KeyCode::ShiftRight); + let reload = world + .resource::>() + .just_pressed(KeyCode::KeyR); + + if ctrl && shift && reload { + on_hot_reload(world); + world.resource_mut::().label = "Hot reload: manual refresh".into(); + info!("Manual hot reload refresh (Ctrl+Shift+R)"); + } +} + +fn game_hot_dylib_path() -> Option { + let target = std::env::var("CARGO_TARGET_DIR").map_or_else( + |_| PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target")), + PathBuf::from, + ); + let profile = if cfg!(debug_assertions) { + "debug" + } else { + "release" + }; + Some(target.join(profile).join(game_hot_lib_name())) +} + +fn game_hot_lib_name() -> &'static str { + if cfg!(target_os = "macos") { + "libgame_hot.dylib" + } else if cfg!(target_os = "windows") { + "game_hot.dll" + } else { + "libgame_hot.so" + } +} diff --git a/crates/editor/src/ext/mod.rs b/crates/editor/src/ext/mod.rs new file mode 100644 index 0000000..fca2f1c --- /dev/null +++ b/crates/editor/src/ext/mod.rs @@ -0,0 +1,9 @@ +//! Editor extensibility, BRP, command queue, and optional hot reload. + +pub mod brp; +pub mod command_queue; +pub mod extensibility; +pub mod game_inspector; +pub mod game_panels; +#[cfg(feature = "hot-reload")] +pub mod hot_reload; diff --git a/crates/editor/src/history/commands.rs b/crates/editor/src/history/commands.rs new file mode 100644 index 0000000..dc32fc4 --- /dev/null +++ b/crates/editor/src/history/commands.rs @@ -0,0 +1,151 @@ +use bevy::prelude::*; +use shared::{ + ActorId, ActorKind, ActorName, ColliderDesc, EditorVisibility, InspectorOrder, LightDesc, + MaterialDesc, MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody, PostProcessVolumeDesc, + PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, + TriggerVolume, WeaponSpawn, +}; +#[derive(Debug, Clone)] +pub struct EditorEntitySnapshot { + pub actor_id: Option, + pub actor_kind: ActorKind, + pub actor_name: Option, + pub name: Option, + pub transform: Transform, + pub primitive: Option, + pub static_mesh_renderer: Option, + pub material: Option, + pub material_override: Option, + pub rigid_body: Option, + pub collider: Option, + pub physics: Option, + pub light: Option, + pub player_spawn: bool, + pub model: Option, + pub prefab: Option, + pub prefab_instance: Option, + pub weapon_spawn: Option, + pub trigger_volume: Option, + pub post_process_volume: Option, + pub team_spawn: Option, + pub objective: Option, + pub hierarchy_sibling_index: i32, + pub editor_visibility: EditorVisibility, + pub children: Vec, +} + +#[derive(Debug, Clone)] +pub struct SiblingChange { + pub entity: Entity, + pub old_parent: Option, + pub new_parent: Option, + pub old_index: i32, + pub new_index: i32, +} + +#[derive(Debug, Clone)] +pub enum EditorCommand { + Spawn { + snapshot: EditorEntitySnapshot, + entity: Option, + }, + Despawn { + snapshot: EditorEntitySnapshot, + entity: Option, + }, + Duplicate { + snapshots: Vec, + entities: Vec, + }, + Rename { + entity: Entity, + old: String, + new: String, + }, + SetTransform { + entity: Entity, + old: Transform, + new: Transform, + }, + SetMaterial { + entity: Entity, + old: Option, + new: MaterialDesc, + }, + SetMaterialOverride { + entity: Entity, + old: Option, + new: MaterialOverride, + }, + SetLight { + entity: Entity, + old: Option, + new: LightDesc, + }, + SetPhysics { + entity: Entity, + old: Option, + new: PhysicsBody, + }, + SetRigidBody { + entity: Entity, + old: Option, + new: RigidBodyDesc, + }, + SetCollider { + entity: Entity, + old: Option, + new: ColliderDesc, + }, + SetPrimitive { + entity: Entity, + old: Option, + new: Primitive, + }, + SetStaticMeshRenderer { + entity: Entity, + old: Option, + new: StaticMeshRenderer, + }, + SetPostProcessVolume { + entity: Entity, + old: Option, + new: PostProcessVolumeDesc, + }, + SetTransformGroup { + entities: Vec, + olds: Vec, + news: Vec, + }, + Reparent { + entity: Entity, + old_parent: Option, + new_parent: Option, + }, + SetActorKind { + entity: Entity, + old: ActorKind, + new: ActorKind, + }, + ReorderSibling { + changes: Vec, + }, + SetEditorVisibility { + entity: Entity, + old: EditorVisibility, + new: EditorVisibility, + }, + SetInspectorOrder { + entity: Entity, + old: Option, + new: InspectorOrder, + }, + AddComponent { + entity: Entity, + snapshot: EditorEntitySnapshot, + }, + RemoveComponent { + entity: Entity, + snapshot: EditorEntitySnapshot, + }, +} diff --git a/crates/editor/src/history/mod.rs b/crates/editor/src/history/mod.rs new file mode 100644 index 0000000..6c6851c --- /dev/null +++ b/crates/editor/src/history/mod.rs @@ -0,0 +1,1385 @@ +use bevy::prelude::*; +use shared::{ + infer_actor_kind, ActorId, ActorKind, ActorName, ColliderDesc, EditorVisibility, + HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride, + ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, + PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn, +}; + +use crate::scene_io::SceneIo; +use crate::selection::SelectedEntity; +use crate::state::scene_tools_active; +use crate::ui::hierarchy_ops::{ + apply_sibling_change, apply_sibling_change_new, next_sibling_index, + reorder_entities_under_parent, would_create_cycle, +}; +use crate::ui::UiState; + +mod commands; +pub use commands::{EditorCommand, EditorEntitySnapshot, SiblingChange}; + +#[derive(Resource, Debug, Default)] +pub struct EditorHistory { + undo_stack: Vec, + redo_stack: Vec, + pub status: String, +} + +impl EditorHistory { + pub fn push(&mut self, command: EditorCommand) { + self.undo_stack.push(command); + self.redo_stack.clear(); + self.status = format!("Undo: {} command(s), redo cleared", self.undo_stack.len()); + } + + pub fn can_undo(&self) -> bool { + !self.undo_stack.is_empty() + } + + pub fn can_redo(&self) -> bool { + !self.redo_stack.is_empty() + } + + fn pop_undo(&mut self) -> Option { + self.undo_stack.pop() + } + + fn push_undo(&mut self, command: EditorCommand) { + self.undo_stack.push(command); + } + + fn pop_redo(&mut self) -> Option { + self.redo_stack.pop() + } + + fn push_redo(&mut self, command: EditorCommand) { + self.redo_stack.push(command); + } + + pub fn clear(&mut self) { + self.undo_stack.clear(); + self.redo_stack.clear(); + self.status = "History cleared".to_string(); + } +} + +#[derive(Resource, Debug, Default)] +struct TransformEditTracker { + active: Option<(Entity, Transform, Vec<(Entity, Transform)>)>, +} + +pub struct EditorHistoryPlugin; + +impl Plugin for EditorHistoryPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_systems( + Update, + (handle_history_hotkeys, capture_gizmo_transform_edits) + .chain() + .run_if(scene_tools_active), + ); + } +} + +pub fn snapshot_entity(world: &World, entity: Entity) -> Option { + let entity_ref = world.get_entity(entity).ok()?; + if !entity_ref.contains::() { + return None; + } + + let children = entity_ref + .get::() + .map(|children| { + children + .iter() + .filter_map(|child| snapshot_entity(world, child)) + .collect() + }) + .unwrap_or_default(); + + let actor_kind = entity_ref + .get::() + .copied() + .or_else(|| infer_actor_kind(entity_ref)) + .unwrap_or(ActorKind::Empty); + + Some(EditorEntitySnapshot { + actor_id: entity_ref.get::().cloned(), + actor_kind, + actor_name: entity_ref.get::().cloned(), + name: entity_ref + .get::() + .map(|name| name.as_str().to_string()), + transform: entity_ref.get::().copied().unwrap_or_default(), + primitive: entity_ref.get::().cloned(), + static_mesh_renderer: entity_ref.get::().cloned(), + material: entity_ref.get::().cloned(), + material_override: entity_ref.get::().cloned(), + rigid_body: entity_ref.get::().cloned(), + collider: entity_ref.get::().cloned(), + physics: entity_ref.get::().cloned(), + light: entity_ref.get::().cloned(), + player_spawn: entity_ref.contains::(), + model: entity_ref.get::().cloned(), + prefab: entity_ref.get::().cloned(), + prefab_instance: entity_ref.get::().cloned(), + weapon_spawn: entity_ref.get::().cloned(), + trigger_volume: entity_ref.get::().cloned(), + post_process_volume: entity_ref.get::().cloned(), + team_spawn: entity_ref.get::().cloned(), + objective: entity_ref.get::().cloned(), + hierarchy_sibling_index: entity_ref + .get::() + .map(|index| index.0) + .unwrap_or(0), + editor_visibility: entity_ref + .get::() + .copied() + .unwrap_or_default(), + children, + }) +} + +pub fn spawn_snapshot(world: &mut World, snapshot: &EditorEntitySnapshot) -> Entity { + spawn_snapshot_with_parent(world, snapshot, None) +} + +fn new_actor_id() -> ActorId { + ActorId::new(uuid::Uuid::new_v4().to_string()) +} + +fn spawn_snapshot_with_parent( + world: &mut World, + snapshot: &EditorEntitySnapshot, + parent: Option, +) -> Entity { + let mut entity_mut = world.spawn((LevelObject, snapshot.actor_kind, snapshot.transform)); + entity_mut.insert( + snapshot + .actor_id + .clone() + .filter(|id| !id.0.trim().is_empty()) + .unwrap_or_else(new_actor_id), + ); + if let Some(actor_name) = &snapshot.actor_name { + entity_mut.insert(actor_name.clone()); + } + if let Some(name) = &snapshot.name { + entity_mut.insert(Name::new(name.clone())); + } + if let Some(primitive) = &snapshot.primitive { + entity_mut.insert(primitive.clone()); + } + if let Some(renderer) = &snapshot.static_mesh_renderer { + entity_mut.insert(renderer.clone()); + } + if let Some(material) = &snapshot.material { + entity_mut.insert(material.clone()); + } + if let Some(material_override) = &snapshot.material_override { + entity_mut.insert(material_override.clone()); + } + if let Some(rigid_body) = &snapshot.rigid_body { + entity_mut.insert(*rigid_body); + } + if let Some(collider) = &snapshot.collider { + entity_mut.insert(collider.clone()); + } + if let Some(physics) = &snapshot.physics { + entity_mut.insert(physics.clone()); + } + if let Some(light) = &snapshot.light { + entity_mut.insert(light.clone()); + } + if snapshot.player_spawn { + entity_mut.insert(PlayerSpawn); + } + if let Some(model) = &snapshot.model { + entity_mut.insert(model.clone()); + } + if let Some(prefab) = &snapshot.prefab { + entity_mut.insert(prefab.clone()); + } + if let Some(instance) = &snapshot.prefab_instance { + entity_mut.insert(instance.clone()); + } + if let Some(weapon) = &snapshot.weapon_spawn { + entity_mut.insert(weapon.clone()); + } + if let Some(trigger) = &snapshot.trigger_volume { + entity_mut.insert(trigger.clone()); + } + if let Some(pp) = &snapshot.post_process_volume { + entity_mut.insert(pp.clone()); + } + if let Some(team) = &snapshot.team_spawn { + entity_mut.insert(team.clone()); + } + if let Some(objective) = &snapshot.objective { + entity_mut.insert(objective.clone()); + } + entity_mut.insert(HierarchySiblingIndex(snapshot.hierarchy_sibling_index)); + entity_mut.insert(snapshot.editor_visibility); + if let Some(parent) = parent { + entity_mut.insert(ChildOf(parent)); + } + + let entity = entity_mut.id(); + for child in &snapshot.children { + spawn_snapshot_with_parent(world, child, Some(entity)); + } + entity +} + +pub fn clear_level_objects(world: &mut World) { + let mut query = world.query_filtered::>(); + let entities: Vec = query.iter(world).collect(); + for entity in entities { + if let Ok(entity_mut) = world.get_entity_mut(entity) { + entity_mut.despawn(); + } + } +} + +pub fn spawn_with_history(world: &mut World, mut snapshot: EditorEntitySnapshot) -> Entity { + snapshot.hierarchy_sibling_index = next_sibling_index(world, None); + let entity = spawn_snapshot(world, &snapshot); + push_history( + world, + EditorCommand::Spawn { + snapshot, + entity: Some(entity), + }, + ); + select_one(world, entity); + entity +} + +pub fn delete_entities_with_history(world: &mut World, entities: &[Entity]) { + let mut deleted = Vec::new(); + for entity in entities { + if let Some(snapshot) = snapshot_entity(world, *entity) { + deleted.push((snapshot, *entity)); + } + } + + for (_, entity) in &deleted { + if let Ok(entity_mut) = world.get_entity_mut(*entity) { + entity_mut.despawn(); + } + } + + for (snapshot, entity) in deleted { + push_history( + world, + EditorCommand::Despawn { + snapshot, + entity: Some(entity), + }, + ); + } + clear_selection(world); +} + +pub fn duplicate_entities_with_history(world: &mut World, entities: &[Entity]) { + let snapshots: Vec = entities + .iter() + .filter_map(|entity| snapshot_entity(world, *entity)) + .map(|mut snapshot| { + snapshot.transform.translation += Vec3::new(1.0, 0.0, 1.0); + if let Some(name) = &mut snapshot.name { + name.push_str(" Copy"); + } + snapshot + }) + .collect(); + + if snapshots.is_empty() { + return; + } + + let entities: Vec = snapshots + .iter() + .map(|snapshot| spawn_snapshot(world, snapshot)) + .collect(); + push_history( + world, + EditorCommand::Duplicate { + snapshots, + entities: entities.clone(), + }, + ); + select_many(world, &entities); +} + +pub fn rename_entity_with_history(world: &mut World, entity: Entity, new_name: String) { + let old = world + .get::(entity) + .map(|name| name.as_str().to_string()) + .unwrap_or_default(); + if old == new_name { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(Name::new(new_name.clone())); + } + push_history( + world, + EditorCommand::Rename { + entity, + old, + new: new_name, + }, + ); +} + +pub fn set_material_with_history(world: &mut World, entity: Entity, new: MaterialDesc) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if old.as_ref().is_some_and(|old| material_eq(old, &new)) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetMaterial { entity, old, new }); +} + +pub fn set_material_override_with_history( + world: &mut World, + entity: Entity, + new: MaterialOverride, +) { + let old = world.get::(entity).cloned(); + if old.as_ref() == Some(&new) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history( + world, + EditorCommand::SetMaterialOverride { entity, old, new }, + ); +} + +pub fn set_light_with_history(world: &mut World, entity: Entity, new: LightDesc) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetLight { entity, old, new }); +} + +pub fn set_post_process_volume_with_history( + world: &mut World, + entity: Entity, + new: PostProcessVolumeDesc, +) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history( + world, + EditorCommand::SetPostProcessVolume { entity, old, new }, + ); +} + +pub fn set_physics_with_history(world: &mut World, entity: Entity, new: PhysicsBody) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetPhysics { entity, old, new }); +} + +pub fn set_rigid_body_with_history(world: &mut World, entity: Entity, new: RigidBodyDesc) { + let old = world.get::(entity).copied(); + if old == Some(new) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new); + } + push_history(world, EditorCommand::SetRigidBody { entity, old, new }); +} + +pub fn set_collider_with_history(world: &mut World, entity: Entity, new: ColliderDesc) { + let old = world.get::(entity).cloned(); + if old.as_ref() == Some(&new) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetCollider { entity, old, new }); +} + +pub fn set_primitive_with_history(world: &mut World, entity: Entity, new: Primitive) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetPrimitive { entity, old, new }); +} + +pub fn set_static_mesh_renderer_with_history( + world: &mut World, + entity: Entity, + new: StaticMeshRenderer, +) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if old.as_ref() == Some(&new) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history( + world, + EditorCommand::SetStaticMeshRenderer { entity, old, new }, + ); +} + +pub fn reparent_with_history(world: &mut World, entity: Entity, new_parent: Option) { + reorder_siblings_with_history(world, &[entity], new_parent, i32::MAX); +} + +pub fn reorder_siblings_with_history( + world: &mut World, + entities: &[Entity], + new_parent: Option, + insert_index: i32, +) { + let entities: Vec = entities + .iter() + .copied() + .filter(|entity| is_level_object(world, *entity)) + .collect(); + if entities.is_empty() { + return; + } + if would_create_cycle(world, &entities, new_parent) { + return; + } + + let changes = reorder_entities_under_parent(world, &entities, new_parent, insert_index); + if changes.is_empty() { + return; + } + push_history(world, EditorCommand::ReorderSibling { changes }); +} + +pub fn set_editor_visibility_with_history( + world: &mut World, + entity: Entity, + new: EditorVisibility, +) { + if !is_level_object(world, entity) { + return; + } + let old = world + .get::(entity) + .copied() + .unwrap_or_default(); + if old.visible == new.visible { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new); + } + push_history( + world, + EditorCommand::SetEditorVisibility { entity, old, new }, + ); +} + +pub fn set_inspector_order_with_history(world: &mut World, entity: Entity, new: InspectorOrder) { + if !is_level_object(world, entity) { + return; + } + let old = world.get::(entity).cloned(); + if old.as_ref() == Some(&new) { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(new.clone()); + } + push_history(world, EditorCommand::SetInspectorOrder { entity, old, new }); +} + +pub fn group_selection_with_history(world: &mut World, entities: &[Entity]) { + let entities: Vec = entities + .iter() + .copied() + .filter(|entity| is_level_object(world, *entity)) + .collect(); + if entities.is_empty() { + return; + } + + let snapshot = EditorEntitySnapshot { + actor_id: None, + actor_kind: ActorKind::Empty, + actor_name: None, + name: Some("Group".to_string()), + transform: Transform::default(), + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: None, + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: next_sibling_index(world, None), + editor_visibility: EditorVisibility::default(), + children: Vec::new(), + }; + let group = spawn_snapshot(world, &snapshot); + push_history( + world, + EditorCommand::Spawn { + snapshot, + entity: Some(group), + }, + ); + reorder_siblings_with_history(world, &entities, Some(group), i32::MAX); +} + +pub fn create_empty_child_with_history(world: &mut World, parent: Entity) { + if !is_level_object(world, parent) { + return; + } + let snapshot = EditorEntitySnapshot { + actor_id: None, + actor_kind: ActorKind::Empty, + actor_name: None, + name: Some("Empty".to_string()), + transform: Transform::default(), + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: None, + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: next_sibling_index(world, Some(parent)), + editor_visibility: EditorVisibility::default(), + children: Vec::new(), + }; + let entity = spawn_snapshot_with_parent(world, &snapshot, Some(parent)); + push_history( + world, + EditorCommand::Spawn { + snapshot, + entity: Some(entity), + }, + ); + select_one(world, entity); +} + +pub fn unpack_prefab_instance(world: &mut World, entity: Entity) { + if world.get::(entity).is_none() { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.remove::(); + } + mark_dirty(world); +} + +pub fn set_transform_with_history( + world: &mut World, + entity: Entity, + old: Transform, + new: Transform, +) { + if transform_nearly_eq(&old, &new) { + return; + } + push_history(world, EditorCommand::SetTransform { entity, old, new }); +} + +pub fn set_actor_kind_with_history( + world: &mut World, + entity: Entity, + old: ActorKind, + new: ActorKind, +) { + if old == new { + return; + } + push_history(world, EditorCommand::SetActorKind { entity, old, new }); +} + +pub fn apply_actor_kind(world: &mut World, entity: Entity, kind: ActorKind) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(kind); + } +} + +pub fn apply_command_undo(world: &mut World) { + let Some(mut command) = world.resource_mut::().pop_undo() else { + return; + }; + undo_command(world, &mut command); + world.resource_mut::().push_redo(command); + mark_dirty(world); +} + +pub fn apply_command_redo(world: &mut World) { + let Some(mut command) = world.resource_mut::().pop_redo() else { + return; + }; + redo_command(world, &mut command); + world.resource_mut::().push_undo(command); + mark_dirty(world); +} + +fn undo_command(world: &mut World, command: &mut EditorCommand) { + match command { + EditorCommand::Spawn { entity, .. } => { + despawn_entity(world, entity.take()); + clear_selection(world); + } + EditorCommand::Despawn { snapshot, entity } => { + let spawned = spawn_snapshot(world, snapshot); + *entity = Some(spawned); + select_one(world, spawned); + } + EditorCommand::Duplicate { entities, .. } => { + for entity in std::mem::take(entities) { + despawn_entity(world, Some(entity)); + } + clear_selection(world); + } + EditorCommand::Rename { entity, old, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(Name::new(old.clone())); + } + } + EditorCommand::SetTransform { entity, old, .. } => { + if let Some(mut transform) = world.get_mut::(*entity) { + *transform = *old; + } + } + EditorCommand::SetMaterial { entity, old, .. } => { + apply_material(world, *entity, old); + } + EditorCommand::SetMaterialOverride { entity, old, .. } => { + apply_material_override(world, *entity, old); + } + EditorCommand::SetLight { entity, old, .. } => { + apply_light(world, *entity, old); + } + EditorCommand::SetPostProcessVolume { entity, old, .. } => { + apply_post_process_volume(world, *entity, old); + } + EditorCommand::SetPhysics { entity, old, .. } => { + apply_physics(world, *entity, old); + } + EditorCommand::SetRigidBody { entity, old, .. } => { + apply_rigid_body(world, *entity, old); + } + EditorCommand::SetCollider { entity, old, .. } => { + apply_collider(world, *entity, old); + } + EditorCommand::SetPrimitive { entity, old, .. } => { + apply_primitive(world, *entity, old); + } + EditorCommand::SetStaticMeshRenderer { entity, old, .. } => { + apply_static_mesh_renderer(world, *entity, old); + } + EditorCommand::SetTransformGroup { entities, olds, .. } => { + for (entity, old) in entities.iter().zip(olds.iter()) { + if let Some(mut transform) = world.get_mut::(*entity) { + *transform = *old; + } + } + } + EditorCommand::Reparent { + entity, old_parent, .. + } => { + apply_parent(world, *entity, *old_parent); + } + EditorCommand::SetActorKind { entity, old, .. } => { + apply_actor_kind(world, *entity, *old); + } + EditorCommand::ReorderSibling { changes } => { + for change in changes.iter().rev() { + apply_sibling_change(world, change); + } + } + EditorCommand::SetEditorVisibility { entity, old, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(*old); + } + } + EditorCommand::SetInspectorOrder { entity, old, .. } => { + apply_inspector_order(world, *entity, old); + } + EditorCommand::AddComponent { entity, snapshot } => { + remove_authored_components(world, *entity, snapshot); + } + EditorCommand::RemoveComponent { entity, snapshot } => { + apply_authored_components(world, *entity, snapshot); + } + } +} + +fn redo_command(world: &mut World, command: &mut EditorCommand) { + match command { + EditorCommand::Spawn { snapshot, entity } => { + let spawned = spawn_snapshot(world, snapshot); + *entity = Some(spawned); + select_one(world, spawned); + } + EditorCommand::Despawn { entity, .. } => { + despawn_entity(world, entity.take()); + clear_selection(world); + } + EditorCommand::Duplicate { + snapshots, + entities, + } => { + let spawned: Vec = snapshots + .iter() + .map(|snapshot| spawn_snapshot(world, snapshot)) + .collect(); + *entities = spawned.clone(); + select_many(world, &spawned); + } + EditorCommand::Rename { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(Name::new(new.clone())); + } + } + EditorCommand::SetTransform { entity, new, .. } => { + if let Some(mut transform) = world.get_mut::(*entity) { + *transform = *new; + } + } + EditorCommand::SetMaterial { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetMaterialOverride { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetLight { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetPostProcessVolume { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetPhysics { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetRigidBody { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(*new); + } + } + EditorCommand::SetCollider { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetPrimitive { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetStaticMeshRenderer { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::SetTransformGroup { entities, news, .. } => { + for (entity, new) in entities.iter().zip(news.iter()) { + if let Some(mut transform) = world.get_mut::(*entity) { + *transform = *new; + } + } + } + EditorCommand::Reparent { + entity, new_parent, .. + } => { + apply_parent(world, *entity, *new_parent); + } + EditorCommand::SetActorKind { entity, new, .. } => { + apply_actor_kind(world, *entity, *new); + } + EditorCommand::ReorderSibling { changes } => { + for change in changes { + apply_sibling_change_new(world, change); + } + } + EditorCommand::SetEditorVisibility { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(*new); + } + } + EditorCommand::SetInspectorOrder { entity, new, .. } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(new.clone()); + } + } + EditorCommand::AddComponent { entity, snapshot } => { + apply_authored_components(world, *entity, snapshot); + } + EditorCommand::RemoveComponent { entity, snapshot } => { + remove_authored_components(world, *entity, snapshot); + } + } +} + +fn apply_material(world: &mut World, entity: Entity, material: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match material { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_material_override( + world: &mut World, + entity: Entity, + material_override: &Option, +) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match material_override { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_light(world: &mut World, entity: Entity, light: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match light { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_post_process_volume( + world: &mut World, + entity: Entity, + volume: &Option, +) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match volume { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_physics(world: &mut World, entity: Entity, physics: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match physics { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_rigid_body(world: &mut World, entity: Entity, rigid_body: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match rigid_body { + Some(value) => { + entity_mut.insert(*value); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_collider(world: &mut World, entity: Entity, collider: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match collider { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_primitive(world: &mut World, entity: Entity, primitive: &Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match primitive { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_static_mesh_renderer( + world: &mut World, + entity: Entity, + renderer: &Option, +) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match renderer { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_inspector_order( + world: &mut World, + entity: Entity, + inspector_order: &Option, +) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match inspector_order { + Some(value) => { + entity_mut.insert(value.clone()); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_parent(world: &mut World, entity: Entity, parent: Option) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + match parent { + Some(p) => { + entity_mut.insert(ChildOf(p)); + } + None => { + entity_mut.remove::(); + } + } + } +} + +fn apply_authored_components(world: &mut World, entity: Entity, snapshot: &EditorEntitySnapshot) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(snapshot.actor_kind); + if let Some(v) = &snapshot.primitive { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.static_mesh_renderer { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.material { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.material_override { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.rigid_body { + entity_mut.insert(*v); + } + if let Some(v) = &snapshot.collider { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.physics { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.light { + entity_mut.insert(v.clone()); + } + if snapshot.player_spawn { + entity_mut.insert(PlayerSpawn); + } + if let Some(v) = &snapshot.model { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.prefab { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.prefab_instance { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.weapon_spawn { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.trigger_volume { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.post_process_volume { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.team_spawn { + entity_mut.insert(v.clone()); + } + if let Some(v) = &snapshot.objective { + entity_mut.insert(v.clone()); + } + } +} + +fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &EditorEntitySnapshot) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + if snapshot.primitive.is_some() { + entity_mut.remove::(); + } + if snapshot.static_mesh_renderer.is_some() { + entity_mut.remove::(); + } + if snapshot.material.is_some() { + entity_mut.remove::(); + } + if snapshot.material_override.is_some() { + entity_mut.remove::(); + } + if snapshot.rigid_body.is_some() { + entity_mut.remove::(); + } + if snapshot.collider.is_some() { + entity_mut.remove::(); + } + if snapshot.physics.is_some() { + entity_mut.remove::(); + } + if snapshot.light.is_some() { + entity_mut.remove::(); + } + if snapshot.player_spawn { + entity_mut.remove::(); + } + if snapshot.model.is_some() { + entity_mut.remove::(); + } + if snapshot.prefab.is_some() { + entity_mut.remove::(); + } + if snapshot.prefab_instance.is_some() { + entity_mut.remove::(); + } + if snapshot.weapon_spawn.is_some() { + entity_mut.remove::(); + } + if snapshot.trigger_volume.is_some() { + entity_mut.remove::(); + } + if snapshot.post_process_volume.is_some() { + entity_mut.remove::(); + } + if snapshot.team_spawn.is_some() { + entity_mut.remove::(); + } + if snapshot.objective.is_some() { + entity_mut.remove::(); + } + } +} + +fn handle_history_hotkeys(world: &mut World) { + let (undo, redo, delete, duplicate) = { + let keys = world.resource::>(); + let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]); + let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); + ( + ctrl && keys.just_pressed(KeyCode::KeyZ) && !shift, + ctrl && (keys.just_pressed(KeyCode::KeyY) + || (shift && keys.just_pressed(KeyCode::KeyZ))), + keys.just_pressed(KeyCode::Delete) || keys.just_pressed(KeyCode::Backspace), + ctrl && keys.just_pressed(KeyCode::KeyD), + ) + }; + + if undo { + apply_command_undo(world); + return; + } + if redo { + apply_command_redo(world); + return; + } + + let selection: Vec = world + .get_resource::() + .map(|ui_state| { + ui_state + .selected_entities + .iter() + .filter(|entity| is_level_object(world, *entity)) + .collect() + }) + .unwrap_or_default(); + if delete && !selection.is_empty() { + delete_entities_with_history(world, &selection); + } else if duplicate && !selection.is_empty() { + duplicate_entities_with_history(world, &selection); + } +} + +fn capture_gizmo_transform_edits(world: &mut World) { + let selection: Vec = world + .get_resource::() + .map(|ui| { + ui.selected_entities + .iter() + .filter(|entity| is_level_object(world, *entity)) + .collect() + }) + .unwrap_or_default(); + + let mut query = world.query::<( + Entity, + &Transform, + &transform_gizmo_bevy::prelude::GizmoTarget, + )>(); + let active = query + .iter(world) + .find(|(_, _, target)| target.is_active()) + .map(|(entity, transform, _)| (entity, *transform)); + + let start_group = + active.map(|(entity, transform)| selected_transforms(world, &selection, entity, transform)); + + let finished_edit = { + let mut tracker = world.resource_mut::(); + if tracker.active.is_none() { + if let (Some((entity, transform)), Some(group)) = (active, start_group) { + tracker.active = Some((entity, transform, group)); + return; + } + } + match tracker.active.take() { + Some((primary, old, group)) if active.is_none() => Some((primary, old, group)), + Some(state) if active.is_some() => { + tracker.active = Some(state); + None + } + _ => None, + } + }; + + if let Some((primary, old_primary, group)) = finished_edit { + let Some(new_primary) = world.get::(primary).copied() else { + return; + }; + if group.len() <= 1 { + set_transform_with_history(world, primary, old_primary, new_primary); + return; + } + let translation_delta = new_primary.translation - old_primary.translation; + let rotation_delta = new_primary.rotation * old_primary.rotation.inverse(); + let scale_ratio = new_primary.scale / old_primary.scale; + let mut entities = Vec::new(); + let mut olds = Vec::new(); + let mut news = Vec::new(); + for (entity, old) in group { + let new = Transform { + translation: old.translation + translation_delta, + rotation: rotation_delta * old.rotation, + scale: old.scale * scale_ratio, + }; + if let Some(mut transform) = world.get_mut::(entity) { + *transform = new; + } + entities.push(entity); + olds.push(old); + news.push(new); + } + if !entities.is_empty() { + push_history( + world, + EditorCommand::SetTransformGroup { + entities, + olds, + news, + }, + ); + } + } +} + +fn selected_transforms( + world: &World, + selection: &[Entity], + primary: Entity, + primary_transform: Transform, +) -> Vec<(Entity, Transform)> { + if selection.len() > 1 && selection.contains(&primary) { + selection + .iter() + .filter_map(|entity| { + world + .get::(*entity) + .copied() + .map(|transform| (*entity, transform)) + }) + .collect() + } else { + vec![(primary, primary_transform)] + } +} + +fn push_history(world: &mut World, command: EditorCommand) { + world.resource_mut::().push(command); + mark_dirty(world); +} + +pub fn push_command(world: &mut World, command: EditorCommand) { + push_history(world, command); +} + +fn mark_dirty(world: &mut World) { + if let Some(mut io) = world.get_resource_mut::() { + io.mark_dirty(); + } +} + +fn despawn_entity(world: &mut World, entity: Option) { + if let Some(entity) = entity { + if let Ok(entity_mut) = world.get_entity_mut(entity) { + entity_mut.despawn(); + } + } +} + +fn select_one(world: &mut World, entity: Entity) { + if let Some(mut ui_state) = world.get_resource_mut::() { + ui_state.selected_entities.clear(); + ui_state.selected_entities.select_replace(entity); + } + world.resource_mut::().0 = Some(entity); +} + +fn select_many(world: &mut World, entities: &[Entity]) { + if let Some(mut ui_state) = world.get_resource_mut::() { + ui_state.selected_entities.clear(); + for (index, entity) in entities.iter().enumerate() { + if index == 0 { + ui_state.selected_entities.select_replace(*entity); + } else { + ui_state.selected_entities.select_maybe_add(*entity, true); + } + } + } + world.resource_mut::().0 = entities.first().copied(); +} + +pub fn clear_selection(world: &mut World) { + if let Some(mut ui_state) = world.get_resource_mut::() { + ui_state.selected_entities.clear(); + } + world.resource_mut::().0 = None; +} + +pub fn is_level_object(world: &World, entity: Entity) -> bool { + world + .get_entity(entity) + .is_ok_and(|entity_ref| entity_ref.contains::()) +} + +fn transform_nearly_eq(a: &Transform, b: &Transform) -> bool { + a.translation.abs_diff_eq(b.translation, 0.0001) + && a.rotation.abs_diff_eq(b.rotation, 0.0001) + && a.scale.abs_diff_eq(b.scale, 0.0001) +} + +pub(crate) fn material_eq(a: &MaterialDesc, b: &MaterialDesc) -> bool { + a.shader == b.shader + && a.base_color.r == b.base_color.r + && a.base_color.g == b.base_color.g + && a.base_color.b == b.base_color.b + && a.base_color.a == b.base_color.a + && a.metallic == b.metallic + && a.roughness == b.roughness + && a.emissive_color.r == b.emissive_color.r + && a.emissive_color.g == b.emissive_color.g + && a.emissive_color.b == b.emissive_color.b + && a.emissive_color.a == b.emissive_color.a + && a.emissive_intensity == b.emissive_intensity + && a.base_color_texture == b.base_color_texture + && a.emissive_texture == b.emissive_texture + && a.normal_map_texture == b.normal_map_texture + && a.metallic_roughness_texture == b.metallic_roughness_texture + && a.material_asset_path == b.material_asset_path + && a.parameters == b.parameters + && a.textures == b.textures +} diff --git a/crates/editor/src/infra.rs b/crates/editor/src/infra.rs new file mode 100644 index 0000000..e9560d0 --- /dev/null +++ b/crates/editor/src/infra.rs @@ -0,0 +1,25 @@ +use bevy::picking::Pickable; +use bevy::prelude::*; + +/// Marker for editor/runtime helper entities that are never authored content. +#[derive(Component, Reflect, Default, Debug, Clone, Copy)] +#[reflect(Component, Default, Debug)] +pub struct EditorOnly; + +pub struct EditorInfraPlugin; + +impl Plugin for EditorInfraPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .add_systems(Update, make_editor_only_non_pickable); + } +} + +fn make_editor_only_non_pickable( + mut commands: Commands, + editor_only: Query>, +) { + for entity in &editor_only { + commands.entity(entity).insert(Pickable::IGNORE); + } +} diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs new file mode 100644 index 0000000..670a43a --- /dev/null +++ b/crates/editor/src/lib.rs @@ -0,0 +1,140 @@ +//! In-process level editor library for the Bevy FPS foundation. +#![allow(clippy::type_complexity)] + +pub mod assets; +pub mod ext; +pub mod history; +pub mod infra; +pub mod play; +pub mod project; +pub mod render_target; +pub mod scene; +pub mod ui; +pub mod viewport; + +// Flat re-exports preserve legacy `crate::module` paths. +pub use assets::asset_db; +pub use ext::brp; +pub use ext::command_queue; +pub use ext::extensibility; +#[cfg(feature = "hot-reload")] +pub use ext::hot_reload; +pub use play::net_editor; +pub use play::state; +pub use project::project_io; +pub use project::settings_ui; +pub use project::workspace; +pub use scene::scene_io; +pub use scene::scene_schema; +pub use scene::scene_view; +pub use viewport::actor_icons; +pub use viewport::camera; +pub use viewport::gizmos; +pub use viewport::render_view; +pub use viewport::rendering_diagnostics; +pub use viewport::selection; +pub use viewport::selection_outline; +pub use viewport::visualizers; + +use bevy::app::PluginGroupBuilder; +use bevy::prelude::*; +use bevy_egui::EguiPlugin; +use game::{ + launch, GameInputEnabled, GameInputFocused, GamePlugin, GameRenderBootstrap, SimEnabled, +}; +use transform_gizmo_bevy::prelude::TransformGizmoPlugin; + +use actor_icons::ActorIconsPlugin; +use asset_db::AssetDbPlugin; +use camera::EditorCameraPlugin; +use extensibility::ExtensibilityPlugin; +use gizmos::EditorGizmoPlugin; +use history::EditorHistoryPlugin; +use infra::EditorInfraPlugin; +use play::PlaySessionPlugin; +use project_io::ProjectIoPlugin; +use render_view::RenderViewPlugin; +use scene_io::SceneIoPlugin; +use scene_view::SceneViewPlugin; +use selection::EditorSelectionPlugin; +use selection_outline::SelectionOutlinePlugin; +use settings_ui::SettingsUiPlugin; +use state::EditorStatePlugin; +use ui::EditorUiPlugin; +use viewport::ViewportPlugin; +use visualizers::EditorVisualizersPlugin; + +/// Ordered editor plugin bundle. Registration order is part of the startup contract +/// (see `docs/editor/architecture.md` and ADR 0008). +pub struct EditorPluginGroup; + +impl PluginGroup for EditorPluginGroup { + fn build(self) -> PluginGroupBuilder { + let group = PluginGroupBuilder::start::() + .add(EditorInfraPlugin) + .add(ProjectIoPlugin) + .add(workspace::WorkspacePlugin) + .add(scene_schema::SceneSchemaPlugin) + .add(net_editor::NetEditorPlugin) + .add(AssetDbPlugin) + .add(ExtensibilityPlugin) + .add(SettingsUiPlugin) + .add(assets::EditorAssetsPlugin) + .add(assets::ThumbnailsPlugin) + .add(EditorStatePlugin) + .add(RenderViewPlugin) + .add(SceneViewPlugin) + .add(PlaySessionPlugin) + .add(ViewportPlugin) + .add(EditorCameraPlugin) + .add(ActorIconsPlugin) + .add(EditorSelectionPlugin) + .add(SelectionOutlinePlugin) + .add(EditorGizmoPlugin) + .add(EditorVisualizersPlugin) + .add(EditorHistoryPlugin) + .add(SceneIoPlugin) + .add(EditorUiPlugin) + .add(brp::BrpPlugin); + #[cfg(feature = "hot-reload")] + let group = group.add(hot_reload::HotReloadPlugin); + group + } +} + +/// Shared Bevy app wiring for the in-process editor (game sim + egui shell). +pub fn configure_editor_app(app: &mut App) { + app.add_plugins(launch::default_plugins("Bevy FPS Editor")) + .insert_resource(GameInputEnabled(false)) + .insert_resource(GameInputFocused(false)) + .insert_resource(SimEnabled(false)) + .insert_resource(GameRenderBootstrap { + defer_player_camera_fx: true, + }) + .add_plugins(GamePlugin) + .add_plugins(MeshPickingPlugin) + .add_plugins(TransformGizmoPlugin) + .add_plugins(EguiPlugin::default()) + .add_plugins(EditorPluginGroup); +} + +/// Register game-provided editor panel hooks (ADR 0007 dogfood). +pub fn register_game_editor_plugins(app: &mut App) { + for setup in game::editor_ext::editor_panel_setups() { + extensibility::register_editor_plugin(app, Box::new(GamePanelAdapter(*setup))); + } + ext::game_inspector::register_game_inspector_sections(app); +} + +struct GamePanelAdapter(game::editor_ext::EditorPanelSetup); + +impl extensibility::EditorPlugin for GamePanelAdapter { + fn name(&self) -> &str { + "game.fps_demo_panel" + } + + fn register(&self, app: &mut App) { + (self.0)(app); + app.add_plugins(ext::game_panels::GameEditorPanelsPlugin); + } +} diff --git a/crates/editor/src/play/mod.rs b/crates/editor/src/play/mod.rs new file mode 100644 index 0000000..517f11c --- /dev/null +++ b/crates/editor/src/play/mod.rs @@ -0,0 +1,7 @@ +//! Play-in-editor session, editor mode state, and net editor profiles. + +pub mod net_editor; +mod session; +pub mod state; + +pub use session::*; diff --git a/crates/editor/src/play/net_editor.rs b/crates/editor/src/play/net_editor.rs new file mode 100644 index 0000000..0d6b73b --- /dev/null +++ b/crates/editor/src/play/net_editor.rs @@ -0,0 +1,57 @@ +//! Multiplayer / determinism editor tooling (H4). + +use bevy::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NetPlayProfile { + #[default] + LocalPie, + ListenServer, + ConnectToServer, +} + +#[derive(Resource)] +pub struct NetEditorState { + pub profile: NetPlayProfile, + pub server_address: String, + pub show_determinism_hud: bool, + pub last_state_hash: u64, + pub replay_tick: u64, +} + +impl Default for NetEditorState { + fn default() -> Self { + Self { + profile: NetPlayProfile::LocalPie, + server_address: "127.0.0.1:7777".into(), + show_determinism_hud: true, + last_state_hash: 0, + replay_tick: 0, + } + } +} + +pub struct NetEditorPlugin; + +impl Plugin for NetEditorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, update_determinism_hud); + } +} + +fn update_determinism_hud( + mut state: ResMut, + players: Query<(&Transform, &sim::PlayerVelocity), With>, +) { + if !state.show_determinism_hud { + return; + } + let Some((transform, velocity)) = players.iter().next() else { + return; + }; + state.replay_tick = state.replay_tick.wrapping_add(1); + let bits = transform.translation.to_array().map(f32::to_bits); + let vel = velocity.0.to_array().map(f32::to_bits); + state.last_state_hash = bits[0] as u64 ^ (bits[1] as u64) << 16 ^ vel[0] as u64; +} diff --git a/crates/editor/src/play/session.rs b/crates/editor/src/play/session.rs new file mode 100644 index 0000000..1b44049 --- /dev/null +++ b/crates/editor/src/play/session.rs @@ -0,0 +1,117 @@ +//! Play-in-editor session: player bootstrap and viewport camera binding. + +use bevy::camera::RenderTarget; +use bevy::prelude::*; +use bevy::window::WindowRef; +use game::player::PlayerCamera; +use protocol::PlayerInputIntent; +use settings::SimTuning; +use shared::{inspector_component_active, InspectorOrder, PlayerSpawn, COMPONENT_PLAYER_SPAWN}; +use sim::{Crouching, Grounded, JumpState, Player, PlayerVelocity}; + +use crate::render_target::ViewportRenderTarget; +use crate::state::{EditorMode, PlayPossession}; + +pub struct PlaySessionPlugin; + +impl Plugin for PlaySessionPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(EditorMode::Playing), bootstrap_player_on_play) + .add_systems( + PostUpdate, + sync_play_camera_target.after(crate::scene_view::ensure_viewport_render_target), + ); + } +} + +/// Default player spawn used when no [`PlayerSpawn`] marker exists in the scene. +pub fn default_player_spawn() -> Transform { + Transform::from_xyz(0.0, 1.5, 10.0) +} + +pub(crate) fn sync_play_camera_target( + mode: Res>, + possession: Res, + target: Res, + mut player_cameras: Query<(Entity, &mut Camera), With>, + mut commands: Commands, +) { + let possessed = *mode.get() == EditorMode::Playing && *possession == PlayPossession::Possessed; + + for (entity, mut camera) in &mut player_cameras { + camera.viewport = None; + + if possessed { + let Some(viewport_target) = target.0.as_ref() else { + camera.is_active = false; + continue; + }; + camera.is_active = true; + commands + .entity(entity) + .insert(RenderTarget::Image(viewport_target.image.clone().into())); + } else { + camera.is_active = false; + commands + .entity(entity) + .insert(RenderTarget::Window(WindowRef::Primary)); + } + } +} + +fn bootstrap_player_on_play(world: &mut World) { + let tuning = world + .get_resource::() + .copied() + .unwrap_or_else(|| SimTuning::from_physics(&settings::PhysicsSettings::default())); + + let spawn = world + .query_filtered::<(&Transform, Option<&InspectorOrder>), With>() + .iter(world) + .filter(|(_, order)| inspector_component_active(*order, COMPONENT_PLAYER_SPAWN)) + .map(|(transform, _)| transform) + .next() + .copied() + .unwrap_or_else(default_player_spawn); + + let mut player_query = world.query_filtered::<( + Entity, + &mut Transform, + &mut PlayerVelocity, + &mut PlayerInputIntent, + &mut JumpState, + &mut Grounded, + &mut Crouching, + ), With>(); + let Some(( + player_entity, + mut transform, + mut velocity, + mut intent, + mut jump, + mut grounded, + mut crouching, + )) = player_query.iter_mut(world).next() + else { + warn!("Play mode entered but no Player entity exists"); + return; + }; + + *transform = spawn; + velocity.0 = Vec3::ZERO; + *intent = PlayerInputIntent::default(); + *jump = JumpState::default(); + grounded.0 = false; + crouching.0 = false; + let position = transform.translation; + + { + let mut camera_query = world.query_filtered::<&mut Transform, With>(); + for mut camera_transform in camera_query.iter_mut(world) { + camera_transform.translation = Vec3::new(0.0, tuning.eye_offset_stand, 0.0); + camera_transform.rotation = Quat::IDENTITY; + } + } + + info!("Play session started: player {player_entity:?} at {position:?}"); +} diff --git a/crates/editor/src/play/state.rs b/crates/editor/src/play/state.rs new file mode 100644 index 0000000..d5262d5 --- /dev/null +++ b/crates/editor/src/play/state.rs @@ -0,0 +1,415 @@ +use bevy::prelude::*; +use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow}; +use bevy_egui::PrimaryEguiContext; +use game::player::PlayerCamera; +use game::{GameInputEnabled, GameInputFocused, SimEnabled}; +use protocol::PlayerInputIntent; +use sim::{Crouching, Grounded, JumpState, Player, PlayerVelocity}; + +use crate::camera::EditorCamera; +use crate::scene_io::SceneIo; + +/// Editor/game mode. `Playing` runs the real game sim; use [`PlayPossession`] for camera control. +#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum EditorMode { + #[default] + Editing, + Playing, +} + +/// FPS vs editor fly camera while sim runs in Play mode. +#[derive(Resource, Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PlayPossession { + #[default] + Possessed, + Ejected, +} + +/// Player/sim state captured on play enter, restored on play stop. +#[derive(Resource, Default, Clone)] +pub struct PlayModeSnapshot { + pub player: Option, +} + +#[derive(Clone)] +pub struct PlayerSimSnapshot { + pub transform: Transform, + pub velocity: Vec3, + pub intent: PlayerInputIntent, + pub jump_coyote: f32, + pub jump_buffer: f32, + pub grounded: bool, + pub crouching: bool, + pub camera_local: Transform, +} + +/// When true, Play mode is active but the fixed-step sim is frozen. +#[derive(Resource, Debug, Clone, Copy, Default)] +pub struct PlayPaused(pub bool); + +/// Request one fixed-step sim tick while paused in Play mode (H3). +#[derive(Resource, Debug, Default)] +pub struct PlayStepOnce(pub bool); + +pub struct EditorStatePlugin; + +impl Plugin for EditorStatePlugin { + fn build(&self, app: &mut App) { + app.init_state::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(PostStartup, init_player_camera_for_editor) + .add_systems( + Update, + ( + toggle_editor_mode, + toggle_play_pause, + toggle_play_step, + toggle_play_possession, + sync_mode_resources_and_cameras, + sync_play_input_focus, + ) + .chain(), + ) + .add_systems( + OnEnter(EditorMode::Playing), + ( + enter_play_mode, + reset_possession, + reset_play_paused, + grab_play_cursor, + ), + ) + .add_systems( + OnEnter(EditorMode::Editing), + (exit_play_mode, reset_play_paused, release_play_cursor), + ); + } +} + +/// Scene tools (selection, gizmos, undo, fly cam) are active in Edit mode or ejected PIE. +pub fn scene_tools_active(mode: Res>, possession: Res) -> bool { + match mode.get() { + EditorMode::Editing => true, + EditorMode::Playing => *possession == PlayPossession::Ejected, + } +} + +fn toggle_editor_mode( + keys: Res>, + state: Res>, + mut next: ResMut>, +) { + if keys.just_pressed(KeyCode::F5) { + next.set(match state.get() { + EditorMode::Editing => EditorMode::Playing, + EditorMode::Playing => EditorMode::Editing, + }); + } +} + +fn toggle_play_possession( + keys: Res>, + mode: Res>, + mut possession: ResMut, + mut cursor: Single<&mut CursorOptions, With>, + mut editor_cameras: Query<&mut Transform, With>, + player: Query<&GlobalTransform, With>, + player_cameras: Query<&GlobalTransform, With>, +) { + if *mode.get() != EditorMode::Playing || !keys.just_pressed(KeyCode::F8) { + return; + } + + match *possession { + PlayPossession::Possessed => { + *possession = PlayPossession::Ejected; + if let Ok(camera_gt) = player_cameras.single() { + for mut editor_transform in &mut editor_cameras { + *editor_transform = Transform { + translation: camera_gt.translation(), + rotation: camera_gt.rotation(), + ..default() + }; + } + } else if let Ok(player_gt) = player.single() { + for mut editor_transform in &mut editor_cameras { + editor_transform.translation = player_gt.translation(); + } + } + release_play_cursor_internal(&mut cursor); + } + PlayPossession::Ejected => { + *possession = PlayPossession::Possessed; + grab_play_cursor_internal(&mut cursor); + } + } +} + +fn toggle_play_pause( + keys: Res>, + mode: Res>, + mut paused: ResMut, +) { + if *mode.get() != EditorMode::Playing || !keys.just_pressed(KeyCode::F6) { + return; + } + paused.0 = !paused.0; +} + +fn toggle_play_step( + keys: Res>, + mode: Res>, + paused: Res, + mut step: ResMut, +) { + if *mode.get() != EditorMode::Playing || !paused.0 || !keys.just_pressed(KeyCode::F7) { + return; + } + step.0 = true; +} + +fn reset_play_paused(mut paused: ResMut) { + paused.0 = false; +} + +fn reset_possession(mut possession: ResMut) { + *possession = PlayPossession::Possessed; +} + +pub(crate) fn init_player_camera_for_editor( + mut player_cameras: Query<&mut Camera, (With, Without)>, +) { + for mut camera in &mut player_cameras { + camera.order = 1; + camera.is_active = false; + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn sync_mode_resources_and_cameras( + state: Res>, + possession: Res, + paused: Res, + mut step: ResMut, + mut input_enabled: ResMut, + mut sim_enabled: ResMut, + mut editor_cameras: Query<&mut Camera, With>, + mut player_cameras: Query<&mut Camera, (With, Without)>, + mut egui_cameras: Query< + &mut Camera, + ( + With, + Without, + Without, + ), + >, + mut last_mode: Local>, +) { + let mode = *state.get(); + let playing = mode == EditorMode::Playing; + let ejected = playing && *possession == PlayPossession::Ejected; + let possessed = playing && *possession == PlayPossession::Possessed; + + sim_enabled.0 = playing && (!paused.0 || step.0); + input_enabled.0 = possessed && !paused.0; + if step.0 { + step.0 = false; + } + + for mut camera in &mut editor_cameras { + camera.is_active = !playing || ejected; + } + for mut camera in &mut player_cameras { + camera.is_active = possessed; + } + + for mut camera in &mut egui_cameras { + camera.clear_color = if !playing || ejected { + ClearColorConfig::None + } else { + ClearColorConfig::Custom(Color::BLACK) + }; + } + + let key = (mode, *possession); + if *last_mode != Some(key) { + *last_mode = Some(key); + info!("EditorMode -> {mode:?}, possession -> {possession:?}"); + } +} + +fn sync_play_input_focus( + mode: Res>, + possession: Res, + paused: Res, + mut focused: ResMut, +) { + focused.0 = + *mode.get() == EditorMode::Playing && *possession == PlayPossession::Possessed && !paused.0; +} + +fn enter_play_mode(world: &mut World) { + let snapshot = snapshot_player_sim(world); + world.resource_mut::().player = Some(snapshot); +} + +fn exit_play_mode(world: &mut World) { + if let Some(snapshot) = world.resource_mut::().player.take() { + restore_player_sim(world, &snapshot); + } + if let Some(mut scene_io) = world.get_resource_mut::() { + scene_io.dirty = true; + } +} + +fn snapshot_player_sim(world: &mut World) -> PlayerSimSnapshot { + let player_data = { + let mut player_query = world.query_filtered::<( + &Transform, + &PlayerVelocity, + &PlayerInputIntent, + &JumpState, + &Grounded, + &Crouching, + ), With>(); + + player_query.iter(world).next().map( + |(transform, velocity, intent, jump, grounded, crouching)| { + ( + *transform, + velocity.0, + *intent, + jump.coyote, + jump.buffer, + grounded.0, + crouching.0, + ) + }, + ) + }; + + let Some((transform, velocity, intent, jump_coyote, jump_buffer, grounded, crouching)) = + player_data + else { + return PlayerSimSnapshot { + transform: Transform::default(), + velocity: Vec3::ZERO, + intent: PlayerInputIntent::default(), + jump_coyote: 0.0, + jump_buffer: 0.0, + grounded: false, + crouching: false, + camera_local: Transform::IDENTITY, + }; + }; + + let camera_local = { + let mut camera_query = world.query_filtered::<&Transform, With>(); + camera_query + .iter(world) + .next() + .copied() + .unwrap_or(Transform::IDENTITY) + }; + + PlayerSimSnapshot { + transform, + velocity, + intent, + jump_coyote, + jump_buffer, + grounded, + crouching, + camera_local, + } +} + +fn restore_player_sim(world: &mut World, snapshot: &PlayerSimSnapshot) { + let mut player_query = world.query_filtered::<( + &mut Transform, + &mut PlayerVelocity, + &mut PlayerInputIntent, + &mut JumpState, + &mut Grounded, + &mut Crouching, + ), With>(); + + if let Some((mut transform, mut velocity, mut intent, mut jump, mut grounded, mut crouching)) = + player_query.iter_mut(world).next() + { + *transform = snapshot.transform; + velocity.0 = snapshot.velocity; + *intent = snapshot.intent; + *jump = JumpState { + coyote: snapshot.jump_coyote, + buffer: snapshot.jump_buffer, + }; + grounded.0 = snapshot.grounded; + crouching.0 = snapshot.crouching; + } + + let mut camera_query = world.query_filtered::<&mut Transform, With>(); + for mut camera in camera_query.iter_mut(world) { + *camera = snapshot.camera_local; + } +} + +fn grab_play_cursor(mut cursor: Single<&mut CursorOptions, With>) { + grab_play_cursor_internal(&mut cursor); +} + +fn release_play_cursor(mut cursor: Single<&mut CursorOptions, With>) { + release_play_cursor_internal(&mut cursor); +} + +fn grab_play_cursor_internal(cursor: &mut CursorOptions) { + cursor.grab_mode = CursorGrabMode::Locked; + cursor.visible = false; +} + +fn release_play_cursor_internal(cursor: &mut CursorOptions) { + cursor.grab_mode = CursorGrabMode::None; + cursor.visible = true; +} + +/// Toggles possess/eject from UI (mirrors F8 handling in [`toggle_play_possession`]). +pub fn toggle_play_possession_world(world: &mut World) { + if *world.resource::>().get() != EditorMode::Playing { + return; + } + let next = match *world.resource::() { + PlayPossession::Possessed => PlayPossession::Ejected, + PlayPossession::Ejected => PlayPossession::Possessed, + }; + *world.resource_mut::() = next; + + let mut cursor = world + .query_filtered::<&mut CursorOptions, With>() + .single_mut(world) + .expect("primary window cursor"); + + match next { + PlayPossession::Possessed => grab_play_cursor_internal(&mut cursor), + PlayPossession::Ejected => { + release_play_cursor_internal(&mut cursor); + let camera_pose = world + .query_filtered::<&GlobalTransform, With>() + .single(world) + .ok() + .map(|gt| (gt.translation(), gt.rotation())); + if let Some((translation, rotation)) = camera_pose { + let mut editor_query = world.query_filtered::<&mut Transform, With>(); + for mut editor_transform in editor_query.iter_mut(world) { + *editor_transform = Transform { + translation, + rotation, + ..default() + }; + } + } + } + } +} diff --git a/crates/editor/src/project/mod.rs b/crates/editor/src/project/mod.rs new file mode 100644 index 0000000..ebcbc80 --- /dev/null +++ b/crates/editor/src/project/mod.rs @@ -0,0 +1,5 @@ +//! Project workspace I/O, settings UI, and recent-level sync. + +pub mod project_io; +pub mod settings_ui; +pub mod workspace; diff --git a/crates/editor/src/project/project_io.rs b/crates/editor/src/project/project_io.rs new file mode 100644 index 0000000..c59b7fe --- /dev/null +++ b/crates/editor/src/project/project_io.rs @@ -0,0 +1,162 @@ +//! Project workspace I/O and user-local preferences (separate from `assets/project.ron`). + +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; +use settings::{ProjectSettings, ProjectSettingsIo, DEFAULT_PROJECT_PATH}; + +use crate::scene_io::SceneIo; + +const USER_CONFIG_DIR: &str = ".config/bevy-fps"; +const USER_PREFS_FILE: &str = "editor_prefs.ron"; + +/// User-local editor preferences (not checked into the repo). +#[derive(Debug, Clone, Default, Serialize, Deserialize, Resource)] +pub struct UserPreferences { + pub recent_projects: Vec, + pub recent_levels: Vec, + pub last_project_root: Option, + /// Serialized [`crate::ui::layout::DockLayoutSnapshot`] (RON). + pub dock_layout: Option, + #[serde(default)] + pub save_before_play: bool, + /// Hierarchy search filter (persisted between sessions). + #[serde(default)] + pub hierarchy_filter: String, + /// Hierarchy paths expanded in the outliner (name paths like `Group/Cube`). + #[serde(default)] + pub hierarchy_expanded_paths: Vec, + #[serde(default = "default_true")] + pub hierarchy_show_generated: bool, + #[serde(default)] + pub hierarchy_show_runtime: bool, + #[serde(default)] + pub hierarchy_sort_mode: crate::ui::hierarchy_state::HierarchySort, +} + +fn default_true() -> bool { + true +} + +/// Active project workspace metadata (mirrors manifest fields in `ProjectSettings`). +#[derive(Resource, Debug, Clone)] +pub struct ProjectWorkspace { + pub root: String, + pub settings_path: String, + pub settings_dirty: bool, + pub layout_dirty: bool, +} + +impl Default for ProjectWorkspace { + fn default() -> Self { + let root = std::env::current_dir() + .ok() + .and_then(|path| path.into_os_string().into_string().ok()) + .unwrap_or_else(|| ".".into()); + Self { + root, + settings_path: DEFAULT_PROJECT_PATH.into(), + settings_dirty: false, + layout_dirty: false, + } + } +} + +pub struct ProjectIoPlugin; + +impl Plugin for ProjectIoPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_systems(Startup, load_user_preferences_on_startup) + .add_systems(Update, sync_workspace_from_settings_system); + } +} + +fn load_user_preferences(mut commands: Commands) { + let prefs = read_user_preferences(); + commands.insert_resource(prefs); +} + +/// Startup system; registered before [`crate::ui::apply_ui_state_from_prefs`]. +pub(crate) fn load_user_preferences_on_startup(commands: Commands) { + load_user_preferences(commands); +} + +fn sync_workspace_from_settings_system( + settings: Res, + settings_io: Res, + mut workspace: ResMut, +) { + if !settings_io.is_changed() && !settings.is_changed() { + return; + } + sync_workspace_from_settings(&mut workspace, &settings, &settings_io); +} + +fn user_prefs_path() -> Option { + std::env::var_os("HOME").map(|home| { + std::path::PathBuf::from(home) + .join(USER_CONFIG_DIR) + .join(USER_PREFS_FILE) + }) +} + +pub fn read_user_preferences() -> UserPreferences { + let Some(path) = user_prefs_path() else { + return UserPreferences::default(); + }; + match std::fs::read_to_string(path) { + Ok(text) => ron::from_str(&text).unwrap_or_default(), + Err(_) => UserPreferences::default(), + } +} + +pub fn write_user_preferences(prefs: &UserPreferences) -> Result<(), String> { + let path = user_prefs_path().ok_or_else(|| "HOME not set".to_string())?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let text = ron::ser::to_string_pretty(prefs, ron::ser::PrettyConfig::default()) + .map_err(|e| e.to_string())?; + std::fs::write(path, text).map_err(|e| e.to_string()) +} + +pub fn push_recent(list: &mut Vec, entry: String, max: usize) { + list.retain(|item| item != &entry); + list.insert(0, entry); + list.truncate(max); +} + +pub fn sync_workspace_from_settings( + workspace: &mut ProjectWorkspace, + settings: &ProjectSettings, + settings_io: &ProjectSettingsIo, +) { + workspace.settings_path = settings_io.path.clone(); + workspace.settings_dirty = settings_io.dirty; + let _ = settings; +} + +pub fn window_title( + scene_io: &SceneIo, + workspace: &ProjectWorkspace, + settings: &ProjectSettings, +) -> String { + let mut parts = vec![format!("{} — Bevy FPS Editor", settings.name)]; + if !workspace.root.is_empty() && workspace.root != "." { + parts.push(workspace.root.clone()); + } + if let Some(path) = &scene_io.active_path { + parts.push(path.display().to_string()); + } + if scene_io.dirty { + parts.push("*".into()); + } + if workspace.settings_dirty { + parts.push("[project*]".into()); + } + if workspace.layout_dirty { + parts.push("[layout*]".into()); + } + parts.join(" — ") +} diff --git a/crates/editor/src/project/settings_ui.rs b/crates/editor/src/project/settings_ui.rs new file mode 100644 index 0000000..80b0ee9 --- /dev/null +++ b/crates/editor/src/project/settings_ui.rs @@ -0,0 +1,306 @@ +//! Editor-only Project Settings panel (egui). Data lives in the `settings` crate. + +use bevy::prelude::*; +use bevy_egui::egui; +use game_hot::sync_project_sun_from_settings; +use settings::{ + load_project_settings_from_path, save_project_settings, sync_sim_tuning, ExposureMode, + ProjectSettings, ProjectSettingsChanged, ProjectSettingsIo, +}; + +/// Whether the Project Settings window is visible (editor UI state only). +#[derive(Resource, Debug, Default)] +pub struct ProjectSettingsPanel { + pub open: bool, + pub draft: Option, +} + +pub struct SettingsUiPlugin; + +impl Plugin for SettingsUiPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_systems( + PostUpdate, + apply_project_settings_changes + .after(crate::scene_view::sync_editor_camera_viewport_target), + ); + } +} + +pub fn open_project_settings_panel(panel: &mut ProjectSettingsPanel) { + panel.open = true; + panel.draft = None; +} + +pub fn project_settings_window( + world: &mut World, + ctx: &egui::Context, + panel: &mut ProjectSettingsPanel, +) { + if !panel.open { + panel.draft = None; + return; + } + + if panel.draft.is_none() { + panel.draft = Some(world.resource::().clone()); + } + + let mut open = panel.open; + egui::Window::new("Project Settings") + .open(&mut open) + .default_width(420.0) + .show(ctx, |ui| { + let Some(draft) = panel.draft.as_mut() else { + return; + }; + let io_path = world.resource::().path.clone(); + let mut io_dirty = world.resource::().dirty; + draw_project_settings_form(world, ui, draft, &io_path, &mut io_dirty); + world.resource_mut::().dirty = io_dirty; + }); + panel.open = open; + if !open { + panel.draft = None; + } +} + +fn draw_project_settings_form( + world: &mut World, + ui: &mut egui::Ui, + draft: &mut ProjectSettings, + io_path: &str, + io_dirty: &mut bool, +) { + let caps = world + .get_resource::() + .cloned() + .unwrap_or_default(); + + ui.collapsing("Global Illumination", |ui| { + egui::ComboBox::from_label("GiMode") + .selected_text(format!("{:?}", draft.rendering.gi_mode)) + .show_ui(ui, |ui| { + for mode in [ + settings::GiMode::Auto, + settings::GiMode::Forward, + settings::GiMode::Solari, + ] { + if ui + .selectable_value(&mut draft.rendering.gi_mode, mode, format!("{mode:?}")) + .clicked() + {} + } + }); + ui.small("Auto uses Solari when RT GPU support is available; otherwise Forward PBR."); + let status = match (draft.rendering.gi_mode, caps.rt_supported) { + (settings::GiMode::Forward, _) => "Forward PBR", + (settings::GiMode::Solari, true) => "Solari active", + (settings::GiMode::Solari, false) => "Solari requested — will fall back (no RT)", + (settings::GiMode::Auto, true) => "Auto → Solari when playing", + (settings::GiMode::Auto, false) => "Auto → Forward fallback (no RT)", + }; + ui.label(format!("Status: {status}")); + ui.small("Open Window → Rendering for resolved active-camera stack."); + }); + + ui.collapsing("Lighting", |ui| { + ui.label("Ambient"); + ui.horizontal(|ui| { + let mut rgb = [ + draft.rendering.ambient_color[0], + draft.rendering.ambient_color[1], + draft.rendering.ambient_color[2], + ]; + if ui.color_edit_button_rgb(&mut rgb).changed() { + draft.rendering.ambient_color = rgb; + } + }); + ui.add( + egui::Slider::new(&mut draft.rendering.ambient_brightness, 0.0..=200.0) + .text("Ambient brightness"), + ); + ui.add( + egui::Slider::new(&mut draft.rendering.sun_illuminance, 1_000.0..=150_000.0) + .text("Sun illuminance (lux)"), + ); + ui.add( + egui::Slider::new(&mut draft.rendering.shadow_cascades, 1..=4).text("Shadow cascades"), + ); + ui.add( + egui::Slider::new(&mut draft.rendering.shadow_first_cascade, 1.0..=20.0) + .text("First cascade (m)"), + ); + ui.add( + egui::Slider::new(&mut draft.rendering.shadow_max_distance, 50.0..=500.0) + .text("Shadow max distance"), + ); + }); + + ui.collapsing("Post Process", |ui| { + ui.checkbox(&mut draft.rendering.hdr, "HDR"); + ui.small("The viewport uses HDR when Lit mode is active."); + ui.horizontal(|ui| { + ui.label("Exposure"); + if ui + .selectable_label( + draft.rendering.exposure_mode == ExposureMode::Manual, + "Manual", + ) + .clicked() + { + draft.rendering.exposure_mode = ExposureMode::Manual; + } + if ui + .selectable_label(draft.rendering.exposure_mode == ExposureMode::Auto, "Auto") + .clicked() + { + draft.rendering.exposure_mode = ExposureMode::Auto; + } + }); + let manual = draft.rendering.exposure_mode == ExposureMode::Manual; + if manual { + ui.add( + egui::Slider::new(&mut draft.rendering.exposure_ev100, 0.0..=20.0) + .text("Exposure EV100"), + ); + } else { + ui.add_enabled( + false, + egui::Slider::new(&mut draft.rendering.exposure_ev100, 0.0..=20.0) + .text("Exposure EV100"), + ); + ui.small("Scene brightness adapts automatically (histogram metering)."); + ui.add( + egui::Slider::new( + &mut draft.rendering.auto_exposure_speed_brighten, + 0.1..=10.0, + ) + .text("Auto brighten (f-stops/s)"), + ); + ui.add( + egui::Slider::new(&mut draft.rendering.auto_exposure_speed_darken, 0.1..=10.0) + .text("Auto darken (f-stops/s)"), + ); + if !draft.rendering.hdr { + ui.colored_label( + egui::Color32::from_rgb(255, 180, 100), + "Auto exposure requires HDR — enable HDR or use Manual.", + ); + } else { + ui.small("Requires GPU compute; see Window → Rendering if unavailable."); + } + } + ui.checkbox(&mut draft.rendering.tonemapping_aces, "Tonemapping ACES"); + ui.checkbox(&mut draft.rendering.bloom, "Bloom"); + ui.checkbox(&mut draft.rendering.taa, "TAA"); + ui.checkbox(&mut draft.rendering.ssao, "SSAO"); + ui.checkbox(&mut draft.rendering.atmosphere, "Atmosphere"); + ui.label("Fog"); + ui.horizontal(|ui| { + let mut fog_rgb = draft.rendering.fog_color; + if ui.color_edit_button_rgb(&mut fog_rgb).changed() { + draft.rendering.fog_color = fog_rgb; + } + }); + ui.add( + egui::Slider::new(&mut draft.rendering.fog_density, 0.0..=0.01) + .logarithmic(true) + .text("Fog density"), + ); + }); + + ui.separator(); + ui.heading("Physics"); + ui.add(egui::Slider::new(&mut draft.physics.walk_speed, 1.0..=15.0).text("Walk speed")); + ui.add(egui::Slider::new(&mut draft.physics.run_speed, 1.0..=20.0).text("Run speed")); + ui.add(egui::Slider::new(&mut draft.physics.gravity, 5.0..=40.0).text("Gravity")); + ui.add(egui::Slider::new(&mut draft.physics.jump_speed, 3.0..=15.0).text("Jump speed")); + + ui.separator(); + ui.heading("Input"); + ui.add( + egui::Slider::new(&mut draft.input.yaw_sensitivity, 0.0005..=0.01).text("Yaw sensitivity"), + ); + ui.add( + egui::Slider::new(&mut draft.input.pitch_sensitivity, 0.0005..=0.01) + .text("Pitch sensitivity"), + ); + ui.add( + egui::Slider::new(&mut draft.input.editor_fly_speed, 2.0..=40.0).text("Editor fly speed"), + ); + + ui.separator(); + if ui.button("Apply").clicked() { + commit_project_settings(world, draft); + *io_dirty = true; + } + if ui.button("Save to disk").clicked() { + commit_project_settings(world, draft); + let settings = world.resource::().clone(); + let mut io = world.resource_mut::(); + if let Err(error) = save_project_settings(&settings, &mut io) { + warn!("Save project settings failed: {error}"); + } else { + *io_dirty = false; + } + } + if ui.button("Revert from disk").clicked() { + *draft = load_project_settings_from_path(io_path); + commit_project_settings(world, draft); + world.resource_mut::().dirty = false; + *io_dirty = false; + } + if *io_dirty { + ui.label(egui::RichText::new("Unsaved changes").color(egui::Color32::YELLOW)); + } +} + +fn commit_project_settings(world: &mut World, draft: &ProjectSettings) { + let previous = world.resource::().rendering.gi_mode; + *world.resource_mut::() = draft.clone(); + if previous != draft.rendering.gi_mode { + info!( + "Project settings GI mode changed: {:?} -> {:?}", + previous, draft.rendering.gi_mode + ); + } else { + debug!( + "Project settings applied: gi_mode={:?}", + draft.rendering.gi_mode + ); + } + world.write_message(ProjectSettingsChanged); +} + +fn apply_project_settings_changes( + mut reader: MessageReader, + mut commands: Commands, + settings: Res, + tuning: ResMut, + scene_suns: Query< + (&shared::LightDesc, Option<&shared::InspectorOrder>), + With, + >, + project_suns: Query<(&mut DirectionalLight, &mut Visibility), With>, +) { + if reader.read().next().is_none() { + return; + } + sync_sim_tuning(&settings, tuning); + { + let ambient = settings.rendering.clone(); + commands.insert_resource(GlobalAmbientLight { + color: Color::srgb( + ambient.ambient_color[0], + ambient.ambient_color[1], + ambient.ambient_color[2], + ), + brightness: ambient.ambient_brightness, + ..default() + }); + } + sync_project_sun_from_settings(settings, scene_suns, project_suns); + // Full stack refresh runs next frame via `render_view::sync_project_render_view` + // when `ProjectSettings` changes. +} diff --git a/crates/editor/src/project/workspace.rs b/crates/editor/src/project/workspace.rs new file mode 100644 index 0000000..2c76ea0 --- /dev/null +++ b/crates/editor/src/project/workspace.rs @@ -0,0 +1,150 @@ +//! Project workspace New/Open and recent scene switcher (H2). + +use std::path::{Path, PathBuf}; + +use bevy::prelude::*; +use settings::{ + load_project_settings_from_path, ProjectSettings, ProjectSettingsIo, DEFAULT_PROJECT_PATH, +}; + +use crate::history::EditorHistory; +use crate::project_io::{push_recent, ProjectWorkspace, UserPreferences}; +use crate::scene_io::{SceneIo, SceneIoRequest}; + +pub struct WorkspacePlugin; + +impl Plugin for WorkspacePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_systems(Update, process_workspace_requests); + } +} + +#[derive(Resource, Default)] +pub struct WorkspaceState { + pub project_name: String, +} + +#[derive(Resource, Default, Debug)] +pub struct WorkspaceIoRequest { + pub new_project: bool, + pub open_project: bool, +} + +pub fn request_new_project(world: &mut World) { + world.resource_mut::().new_project = true; +} + +pub fn request_open_project(world: &mut World) { + world.resource_mut::().open_project = true; +} + +pub fn request_open_recent_scene(world: &mut World, index: usize) { + world.resource_mut::().request = Some(SceneIoRequest::OpenRecent(index)); +} + +pub fn sync_recent_levels_to_scene_io(world: &mut World) { + let recent: Vec = world + .resource::() + .recent_levels + .iter() + .map(PathBuf::from) + .collect(); + world.resource_mut::().recent_paths = recent; +} + +pub fn remember_opened_level(world: &mut World, path: String) { + if let Some(mut prefs) = world.get_resource_mut::() { + push_recent(&mut prefs.recent_levels, path, 8); + let _ = crate::project_io::write_user_preferences(&prefs); + } +} + +fn process_workspace_requests(world: &mut World) { + let (new_project, open_project) = { + let mut req = world.resource_mut::(); + let new_project = req.new_project; + let open_project = req.open_project; + req.new_project = false; + req.open_project = false; + (new_project, open_project) + }; + + if new_project { + world.resource_mut::().status = apply_new_project(world); + } else if open_project { + world.resource_mut::().status = apply_open_project(world); + } +} + +fn apply_new_project(world: &mut World) -> String { + let default_settings = ProjectSettings::default(); + let settings_path = DEFAULT_PROJECT_PATH.to_string(); + + *world.resource_mut::() = default_settings.clone(); + world.resource_mut::().path = settings_path.clone(); + world.resource_mut::().dirty = false; + + if let Some(mut workspace) = world.get_resource_mut::() { + workspace.root = std::env::current_dir() + .ok() + .and_then(|p| p.into_os_string().into_string().ok()) + .unwrap_or_else(|| ".".into()); + workspace.settings_path = settings_path; + workspace.settings_dirty = false; + } + + world.resource_mut::().project_name = default_settings.name.clone(); + world.resource_mut::().request = Some(SceneIoRequest::New); + world.resource_mut::().clear(); + sync_recent_levels_to_scene_io(world); + format!("New project: {}", default_settings.name) +} + +fn apply_open_project(world: &mut World) -> String { + let Some(root) = rfd::FileDialog::new() + .set_title("Open Project Folder") + .pick_folder() + else { + return "Open project cancelled".to_string(); + }; + + let settings_path = root.join("assets/project.ron"); + if !settings_path.exists() { + return format!( + "No assets/project.ron in {} — pick a project root folder", + root.display() + ); + } + + let settings_path_label = settings_path.display().to_string(); + let settings = load_project_settings_from_path(&settings_path_label); + + *world.resource_mut::() = settings.clone(); + world.resource_mut::().path = settings_path_label.clone(); + world.resource_mut::().dirty = false; + + if let Some(mut workspace) = world.get_resource_mut::() { + workspace.root = root.display().to_string(); + workspace.settings_path = settings_path_label; + workspace.settings_dirty = false; + } + + if let Some(mut prefs) = world.get_resource_mut::() { + push_recent(&mut prefs.recent_projects, workspace_root_label(&root), 8); + prefs.last_project_root = Some(root.display().to_string()); + let _ = crate::project_io::write_user_preferences(&prefs); + } + + world.resource_mut::().project_name = settings.name.clone(); + sync_recent_levels_to_scene_io(world); + format!("Opened project: {}", settings.name) +} + +fn workspace_root_label(path: &Path) -> String { + path.file_name() + .and_then(|name| name.to_str()) + .map(str::to_string) + .unwrap_or_else(|| path.display().to_string()) +} diff --git a/crates/editor/src/render_target.rs b/crates/editor/src/render_target.rs new file mode 100644 index 0000000..994eaf7 --- /dev/null +++ b/crates/editor/src/render_target.rs @@ -0,0 +1,35 @@ +//! Shared HDR render-to-texture helpers for editor viewport panels. + +use bevy::prelude::*; +use bevy::render::render_resource::TextureFormat; + +/// Offscreen render target displayed by the primary editor viewport. +#[derive(Debug, Clone)] +pub struct ViewportTarget { + pub image: Handle, +} + +/// Current primary viewport render target, if the panel has a valid size. +#[derive(Resource, Default, Debug, Clone)] +pub struct ViewportRenderTarget(pub Option); + +/// Allocates or reuses an HDR offscreen color target when the panel size changes. +pub fn ensure_hdr_panel_target( + physical_size: UVec2, + last_size: &mut Option, + images: &mut Assets, +) -> Option> { + if *last_size == Some(physical_size) { + return None; + } + *last_size = Some(physical_size); + Some(images.add(Image::new_target_texture( + physical_size.x, + physical_size.y, + TextureFormat::Rgba16Float, + None, + ))) +} + +/// Standard HDR format for editor panel render targets. +pub const PANEL_TARGET_FORMAT: TextureFormat = TextureFormat::Rgba16Float; diff --git a/crates/editor/src/scene/mod.rs b/crates/editor/src/scene/mod.rs new file mode 100644 index 0000000..072cad3 --- /dev/null +++ b/crates/editor/src/scene/mod.rs @@ -0,0 +1,5 @@ +//! Scene I/O, schema validation, and viewport render-target setup. + +pub mod scene_io; +pub mod scene_schema; +pub mod scene_view; diff --git a/crates/editor/src/scene/scene_io.rs b/crates/editor/src/scene/scene_io.rs new file mode 100644 index 0000000..5e70f1f --- /dev/null +++ b/crates/editor/src/scene/scene_io.rs @@ -0,0 +1,628 @@ +use std::path::{Path, PathBuf}; + +use bevy::ecs::entity::EntityHashMap; +use bevy::ecs::system::SystemState; +use bevy::prelude::*; +use bevy::scene::serde::SceneDeserializer; +use bevy::scene::DynamicScene; +use bevy::scene::DynamicSceneBuilder; +use bevy::window::PrimaryWindow; +use scene::{migrate_scene_text, stamp_schema_version, strip_schema_version, validate_level_text}; +use serde::de::DeserializeSeed; +use shared::{ + flush_level_object_hydration, infer_actor_kind, strip_hydrated_entity, validate_actor, ActorId, + ActorKind, ActorName, ActorValidationError, ColliderDesc, EditorVisibility, + HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride, + ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, + PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn, +}; + +use crate::assets::{import_external_assets, EditorAssets}; +use crate::history::{clear_level_objects, snapshot_entity, EditorHistory}; +use crate::selection::SelectedEntity; +use crate::ui::hierarchy_ops::{ + backfill_missing_editor_visibility, backfill_missing_sibling_indices, +}; +use crate::ui::UiState; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SceneIoRequest { + New, + Open, + Save, + SaveAs, + ImportAssets, + ExportSelection, + SaveSelectionAsPrefab, + OpenRecent(usize), +} + +#[derive(Resource, Debug)] +pub struct SceneIo { + pub active_path: Option, + pub recent_paths: Vec, + pub request: Option, + pub status: String, + pub dirty: bool, + /// When set, the next I/O request needs unsaved-changes confirmation. + pub pending_request: Option, +} + +impl Default for SceneIo { + fn default() -> Self { + Self { + active_path: Some(PathBuf::from("assets/levels/editor_scene.scn.ron")), + recent_paths: Vec::new(), + request: None, + status: "Ready".to_string(), + dirty: false, + pending_request: None, + } + } +} + +impl SceneIo { + pub fn mark_dirty(&mut self) { + self.dirty = true; + } + + pub fn mark_clean(&mut self) { + self.dirty = false; + } + + pub fn active_path_label(&self) -> String { + self.active_path + .as_ref() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "Unsaved scene".to_string()) + } +} + +#[derive(Component)] +struct LoadedSceneRoot; + +pub struct SceneIoPlugin; + +impl Plugin for SceneIoPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems( + Update, + (process_scene_io_requests, update_window_title).chain(), + ) + .add_systems(PostStartup, load_startup_scene); + } +} + +fn load_startup_scene(world: &mut World) { + let active_path = world.resource::().active_path.clone(); + let default_path = PathBuf::from( + world + .resource::() + .default_level + .clone(), + ); + let path = active_path.unwrap_or(default_path); + + match load_level(world, &path) { + Ok(()) => { + world.resource_mut::().active_path = Some(path.clone()); + world.resource_mut::().mark_clean(); + remember_path(world, path.clone()); + world.resource_mut::().clear(); + world.resource_mut::().status = format!("Loaded {}", path.display()); + } + Err(err) => { + world.resource_mut::().status = + format!("Startup scene load failed: {err}; using generated starter arena"); + } + } +} + +fn process_scene_io_requests(world: &mut World) { + let request = { + let mut io = world.resource_mut::(); + if let Some(pending) = io.pending_request.take() { + pending + } else { + match io.request.take() { + Some(req) if io.dirty && needs_dirty_confirm(&req) => { + io.pending_request = Some(req); + return; + } + Some(req) => req, + None => return, + } + } + }; + + if world.resource::().dirty && needs_dirty_confirm(&request) { + match confirm_discard_or_save() { + DirtyConfirm::Save => { + let save_status = save_active_or_prompt(world); + if save_status.starts_with("Save failed") || save_status.ends_with("cancelled") { + world.resource_mut::().status = save_status; + return; + } + } + DirtyConfirm::Discard => {} + DirtyConfirm::Cancel => { + world.resource_mut::().status = "Operation cancelled".to_string(); + return; + } + } + } + + let status = match request { + SceneIoRequest::New => { + clear_loaded_scene_roots(world); + clear_level_objects(world); + world.resource_mut::().active_path = None; + world.resource_mut::().mark_clean(); + world.resource_mut::().clear(); + "Created a new empty level".to_string() + } + SceneIoRequest::Save => save_active_or_prompt(world), + SceneIoRequest::SaveAs => save_with_dialog(world), + SceneIoRequest::Open => open_with_dialog(world), + SceneIoRequest::ImportAssets => import_with_dialog(world), + SceneIoRequest::ExportSelection => export_selection_with_dialog(world), + SceneIoRequest::SaveSelectionAsPrefab => save_selection_as_prefab(world), + SceneIoRequest::OpenRecent(index) => open_recent(world, index), + }; + + world.resource_mut::().status = status; +} + +enum DirtyConfirm { + Save, + Discard, + Cancel, +} + +fn needs_dirty_confirm(request: &SceneIoRequest) -> bool { + matches!( + request, + SceneIoRequest::New | SceneIoRequest::Open | SceneIoRequest::OpenRecent(_) + ) +} + +fn confirm_discard_or_save() -> DirtyConfirm { + use rfd::{MessageButtons, MessageDialog, MessageLevel}; + match MessageDialog::new() + .set_title("Unsaved Changes") + .set_description("The scene has unsaved changes. Save before continuing?") + .set_level(MessageLevel::Warning) + .set_buttons(MessageButtons::YesNoCancel) + .show() + { + rfd::MessageDialogResult::Yes => DirtyConfirm::Save, + rfd::MessageDialogResult::No => DirtyConfirm::Discard, + _ => DirtyConfirm::Cancel, + } +} + +fn open_recent(world: &mut World, index: usize) -> String { + let path = world.resource::().recent_paths.get(index).cloned(); + let Some(path) = path else { + return "Recent scene not found".to_string(); + }; + match load_level(world, &path) { + Ok(()) => { + world.resource_mut::().active_path = Some(path.clone()); + world.resource_mut::().mark_clean(); + remember_path(world, path.clone()); + world.resource_mut::().clear(); + format!("Loading {}", path.display()) + } + Err(err) => format!("Open failed: {err}"), + } +} + +fn save_selection_as_prefab(world: &mut World) -> String { + let selection: Vec = world + .get_resource::() + .map(|ui| { + ui.selected_entities + .iter() + .filter(|entity| { + world + .get_entity(*entity) + .is_ok_and(|e| e.contains::()) + }) + .collect() + }) + .unwrap_or_default(); + if selection.is_empty() { + return "Select level objects to save as prefab".to_string(); + } + + let Some(path) = rfd::FileDialog::new() + .set_directory("assets/prefabs") + .add_filter("Bevy prefab", &["scn.ron", "ron"]) + .set_file_name("prefab.scn.ron") + .save_file() + else { + return "Save prefab cancelled".to_string(); + }; + + match save_entities(world, &path, selection) { + Ok(count) => { + world.resource_mut::().refresh(); + format!( + "Saved prefab with {count} root entity(s) to {}", + path.display() + ) + } + Err(err) => format!("Save prefab failed: {err}"), + } +} + +/// Save helper for play-mode and other cross-module callers. +pub fn save_active_or_prompt_world(world: &mut World) -> String { + save_active_or_prompt(world) +} + +fn save_active_or_prompt(world: &mut World) -> String { + let path = world.resource::().active_path.clone(); + match path { + Some(path) => match save_level(world, &path) { + Ok(count) => { + world.resource_mut::().mark_clean(); + remember_path(world, path.clone()); + format!("Saved {count} level entities to {}", path.display()) + } + Err(err) => format!("Save failed: {err}"), + }, + None => save_with_dialog(world), + } +} + +fn save_with_dialog(world: &mut World) -> String { + let Some(path) = rfd::FileDialog::new() + .set_directory("assets/levels") + .add_filter("Bevy scene", &["scn.ron", "ron"]) + .set_file_name("editor_scene.scn.ron") + .save_file() + else { + return "Save cancelled".to_string(); + }; + + match save_level(world, &path) { + Ok(count) => { + world.resource_mut::().active_path = Some(path.clone()); + world.resource_mut::().mark_clean(); + remember_path(world, path.clone()); + format!("Saved {count} level entities to {}", path.display()) + } + Err(err) => format!("Save failed: {err}"), + } +} + +fn open_with_dialog(world: &mut World) -> String { + let Some(path) = rfd::FileDialog::new() + .set_directory("assets/levels") + .add_filter("Bevy scene", &["scn.ron", "ron"]) + .pick_file() + else { + return "Open cancelled".to_string(); + }; + + match load_level(world, &path) { + Ok(()) => { + world.resource_mut::().active_path = Some(path.clone()); + world.resource_mut::().mark_clean(); + remember_path(world, path.clone()); + world.resource_mut::().clear(); + format!("Loading {}", path.display()) + } + Err(err) => format!("Open failed: {err}"), + } +} + +fn import_with_dialog(world: &mut World) -> String { + let Some(paths) = rfd::FileDialog::new() + .add_filter( + "Editor assets", + &["gltf", "glb", "fbx", "png", "jpg", "jpeg", "webp", "ktx2"], + ) + .pick_files() + else { + return "Import cancelled".to_string(); + }; + + match import_external_assets(&paths) { + Ok(count) => { + world.resource_mut::().refresh(); + format!("Imported {count} asset(s)") + } + Err(err) => format!("Import failed: {err}"), + } +} + +fn export_selection_with_dialog(world: &mut World) -> String { + let Some(entity) = world.resource::().0 else { + return "Nothing selected to export".to_string(); + }; + if snapshot_entity(world, entity).is_none() { + return "Selected entity is not an authored level object".to_string(); + } + + let Some(path) = rfd::FileDialog::new() + .set_directory("assets/levels") + .add_filter("Bevy prefab", &["scn.ron", "ron"]) + .set_file_name("selection.scn.ron") + .save_file() + else { + return "Export cancelled".to_string(); + }; + + match save_entities(world, &path, vec![entity]) { + Ok(count) => format!("Exported {count} selected entity to {}", path.display()), + Err(err) => format!("Export failed: {err}"), + } +} + +fn clear_loaded_scene_roots(world: &mut World) { + let mut query = world.query_filtered::>(); + let entities: Vec = query.iter(world).collect(); + for entity in entities { + if let Ok(entity_mut) = world.get_entity_mut(entity) { + entity_mut.despawn(); + } + } +} + +fn save_level(world: &mut World, path: &Path) -> Result { + let mut query = world + .try_query_filtered::>() + .ok_or_else(|| "could not create LevelObject query".to_string())?; + let entities: Vec = query.iter(world).collect(); + save_entities(world, path, entities) +} + +fn format_actor_validation(err: ActorValidationError) -> String { + match err { + ActorValidationError::MissingActorKind => { + "Save failed: level object missing ActorKind (re-open scene to migrate)".into() + } + ActorValidationError::MissingTransform => { + "Save failed: level object missing Transform".into() + } + ActorValidationError::StaticMeshMissingPrimitive => { + "Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot" + .into() + } + ActorValidationError::StaticMeshHasLight => { + "Save failed: StaticMesh actor cannot have LightDesc".into() + } + ActorValidationError::StaticMeshHasModelRef => { + "Save failed: StaticMesh actor cannot have ModelRef".into() + } + ActorValidationError::ImportedModelMissingModelRef => { + "Save failed: ImportedModel actor requires ModelRef".into() + } + ActorValidationError::ImportedModelHasPrimitive => { + "Save failed: ImportedModel actor cannot have Primitive".into() + } + ActorValidationError::ImportedModelHasStaticMeshRenderer => { + "Save failed: ImportedModel actor cannot have StaticMeshRenderer".into() + } + ActorValidationError::LightMissingLightDesc => { + "Save failed: Light actor requires LightDesc".into() + } + ActorValidationError::LightHasPrimitive => { + "Save failed: Light actor cannot have Primitive".into() + } + ActorValidationError::LightHasModelRef => { + "Save failed: Light actor cannot have ModelRef".into() + } + ActorValidationError::LightHasStaticMeshRenderer => { + "Save failed: Light actor cannot have StaticMeshRenderer".into() + } + ActorValidationError::PostProcessVolumeMissingDesc => { + "Save failed: PostProcessVolume actor requires PostProcessVolumeDesc".into() + } + ActorValidationError::PostProcessVolumeInvalidHalfExtents => { + "Save failed: post-process volume half_extents must be positive".into() + } + ActorValidationError::PostProcessVolumeInvalidBlendDistance => { + "Save failed: post-process volume blend_distance must be non-negative".into() + } + ActorValidationError::PostProcessVolumeInvalidOverrideScalar => { + "Save failed: post-process volume override contains invalid scalar".into() + } + } +} + +fn save_entities(world: &mut World, path: &Path, entities: Vec) -> Result { + let count = entities.len(); + + for entity in &entities { + let entity_ref = world.entity(*entity); + if let Err(err) = validate_actor(entity_ref) { + return Err(format_actor_validation(err)); + } + } + + for entity in &entities { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + strip_hydrated_entity(&mut entity_mut); + } + } + + let scene = DynamicSceneBuilder::from_world(world) + .deny_all() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .allow_component::() + .extract_entities(entities.into_iter()) + .remove_empty_entities() + .build(); + + let ron = { + let registry = world.resource::().read(); + scene + .serialize(®istry) + .map_err(|err| format!("could not serialize scene: {err}"))? + }; + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|err| format!("could not create {}: {err}", parent.display()))?; + } + let stamped = stamp_schema_version(&ron)?; + std::fs::write(path, stamped) + .map_err(|err| format!("could not write {}: {err}", path.display()))?; + finalize_scene_load(world); + + Ok(count) +} + +fn load_level(world: &mut World, path: &Path) -> Result<(), String> { + if !path.exists() { + return Err(format!("{} does not exist yet", path.display())); + } + + let text = std::fs::read_to_string(path) + .map_err(|err| format!("could not read {}: {err}", path.display()))?; + validate_level_text(&text)?; + let normalized = migrate_scene_text(&text)?; + let bevy_ron = strip_schema_version(&normalized)?; + + clear_loaded_scene_roots(world); + clear_level_objects(world); + + let dynamic_scene: DynamicScene = { + let registry = world.resource::().read(); + let scene_deserializer = SceneDeserializer { + type_registry: ®istry, + }; + let mut deserializer = + ron::de::Deserializer::from_str(&bevy_ron).map_err(|err| err.to_string())?; + scene_deserializer + .deserialize(&mut deserializer) + .map_err(|err| format!("could not deserialize scene: {err}"))? + }; + + dynamic_scene + .write_to_world(world, &mut EntityHashMap::default()) + .map_err(|err| format!("could not spawn scene: {err}"))?; + + backfill_missing_actor_kinds(world); + backfill_missing_sibling_indices(world); + backfill_missing_editor_visibility(world); + finalize_scene_load(world); + + if world.contains_resource::() { + world + .resource_mut::() + .last_validated = scene::CURRENT_SCENE_SCHEMA_VERSION; + } + + Ok(()) +} + +fn finalize_scene_load(world: &mut World) { + flush_level_object_hydration(world); + + let rendering = world + .resource::() + .rendering + .clone(); + world.insert_resource(GlobalAmbientLight { + color: Color::srgb( + rendering.ambient_color[0], + rendering.ambient_color[1], + rendering.ambient_color[2], + ), + brightness: rendering.ambient_brightness, + ..default() + }); + + let mut state: SystemState<( + Res, + Query<(&LightDesc, Option<&InspectorOrder>), With>, + Query<(&mut DirectionalLight, &mut Visibility), With>, + )> = SystemState::new(world); + + { + let (settings, scene_suns, project_suns) = state.get_mut(world); + game_hot::sync_project_sun_from_settings(settings, scene_suns, project_suns); + } + state.apply(world); +} + +fn backfill_missing_actor_kinds(world: &mut World) { + let mut query = world.query_filtered::>(); + let entities: Vec = query.iter(world).collect(); + for entity in entities { + let Ok(entity_ref) = world.get_entity(entity) else { + continue; + }; + if entity_ref.get::().is_some() { + continue; + } + if let Some(kind) = infer_actor_kind(entity_ref) { + world.entity_mut(entity).insert(kind); + } + } +} + +fn remember_path(world: &mut World, path: PathBuf) { + let mut io = world.resource_mut::(); + io.recent_paths.retain(|recent| recent != &path); + io.recent_paths.insert(0, path.clone()); + io.recent_paths.truncate(8); + + if world.contains_resource::() { + let path_label = path.display().to_string(); + let mut prefs = world.resource_mut::(); + crate::project_io::push_recent(&mut prefs.recent_levels, path_label, 8); + if let Err(error) = crate::project_io::write_user_preferences(&prefs) { + warn!("Failed to save editor preferences: {error}"); + } + } +} + +fn update_window_title( + io: Res, + workspace: Res, + settings: Res, + settings_io: Res, + mut window: Single<&mut Window, With>, +) { + if !io.is_changed() + && !workspace.is_changed() + && !settings.is_changed() + && !settings_io.is_changed() + { + return; + } + + window.title = crate::project_io::window_title(&io, &workspace, &settings); +} diff --git a/crates/editor/src/scene/scene_schema.rs b/crates/editor/src/scene/scene_schema.rs new file mode 100644 index 0000000..fd6d825 --- /dev/null +++ b/crates/editor/src/scene/scene_schema.rs @@ -0,0 +1,20 @@ +//! Scene format versioning plugin (editor); logic lives in the `scene` crate. + +use bevy::prelude::*; +pub use scene::{ + migrate_scene_text, stamp_schema_version, strip_schema_version, validate_level_file, + validate_level_text, SceneSchemaHeader, CURRENT_SCENE_SCHEMA_VERSION, +}; + +pub struct SceneSchemaPlugin; + +impl Plugin for SceneSchemaPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + } +} + +#[derive(Resource, Default)] +pub struct SceneSchemaState { + pub last_validated: u32, +} diff --git a/crates/editor/src/scene/scene_view.rs b/crates/editor/src/scene/scene_view.rs new file mode 100644 index 0000000..5725d33 --- /dev/null +++ b/crates/editor/src/scene/scene_view.rs @@ -0,0 +1,78 @@ +//! Primary editor viewport render-to-texture target and editor camera binding. + +use bevy::camera::RenderTarget; +use bevy::prelude::*; +use bevy::window::{PrimaryWindow, WindowRef}; + +use crate::camera::EditorCamera; +use crate::render_target::{ensure_hdr_panel_target, ViewportRenderTarget, ViewportTarget}; +use crate::state::{EditorMode, PlayPossession}; +use crate::ui::UiState; +use crate::viewport::panel_physical_size; + +pub struct SceneViewPlugin; + +impl Plugin for SceneViewPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_systems( + PostUpdate, + ( + ensure_viewport_render_target, + sync_editor_camera_viewport_target, + ) + .chain() + .after(crate::ui::show_ui_system), + ); + } +} + +pub(crate) fn ensure_viewport_render_target( + ui_state: Res, + window: Single<&Window, With>, + mut images: ResMut>, + mut target: ResMut, + mut last_size: Local>, +) { + let Some(physical_size) = panel_physical_size(&window, ui_state.viewport_rect) else { + return; + }; + + if let Some(handle) = ensure_hdr_panel_target(physical_size, &mut last_size, &mut images) { + target.0 = Some(ViewportTarget { image: handle }); + } +} + +pub(crate) fn sync_editor_camera_viewport_target( + mode: Res>, + possession: Res, + target: Res, + mut cameras: Query<(Entity, &mut Camera), With>, + mut commands: Commands, +) { + let scene_active = matches!(*mode.get(), EditorMode::Editing) + || (*mode.get() == EditorMode::Playing && *possession == PlayPossession::Ejected); + + for (entity, mut camera) in &mut cameras { + if scene_active { + let Some(viewport_target) = target.0.as_ref() else { + camera.viewport = None; + camera.is_active = false; + continue; + }; + // Render the full offscreen target. A panel-sized + // viewport on `RenderTarget::Image` can exceed the texture for a frame after + // resize (wgpu: scissor rect not contained in render target). + camera.viewport = None; + camera.is_active = true; + commands + .entity(entity) + .insert(RenderTarget::Image(viewport_target.image.clone().into())); + } else { + camera.viewport = None; + camera.is_active = false; + commands + .entity(entity) + .insert(RenderTarget::Window(WindowRef::Primary)); + } + } +} diff --git a/crates/editor/src/ui/actor_inspector/mod.rs b/crates/editor/src/ui/actor_inspector/mod.rs new file mode 100644 index 0000000..f661c15 --- /dev/null +++ b/crates/editor/src/ui/actor_inspector/mod.rs @@ -0,0 +1,138 @@ +//! Actor-centric inspector (authoring components only). + +mod transform; + +use bevy::prelude::*; +use bevy_egui::egui; +use egui_phosphor_icons::icons; +use shared::{ActorId, ActorKind, ActorName, LevelObject}; + +use crate::ext::extensibility::ActorInspectorSectionRegistry; +use crate::ui::inspector; +use crate::ui::theme::{panel_heading, BORDER, TEXT_DIM, WIDGET_BG}; +use crate::ui::widgets::phosphor_icon; + +use transform::transform_inspector_ui; + +const COMPACT_ACTOR_HEADER_WIDTH: f32 = 360.0; + +pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity) { + let Some(entity_ref) = world.get_entity(entity).ok() else { + ui.label("Entity no longer exists."); + return; + }; + if !entity_ref.contains::() { + ui.label("Not a level object."); + return; + } + + let kind = entity_ref + .get::() + .copied() + .unwrap_or(ActorKind::Empty); + + egui::Frame::new() + .fill(WIDGET_BG) + .stroke(egui::Stroke::new(1.0, BORDER)) + .corner_radius(egui::CornerRadius::same(4)) + .inner_margin(egui::Margin::symmetric(8, 8)) + .show(ui, |ui| { + let compact = ui.available_width() < COMPACT_ACTOR_HEADER_WIDTH; + ui.horizontal(|ui| { + ui.label(phosphor_icon(actor_icon(kind), 28.0).color(TEXT_DIM)); + let text_max_width = (ui.available_width() - 28.0).max(96.0); + ui.vertical(|ui| { + ui.set_max_width(text_max_width); + let name = entity_ref + .get::() + .map(|name| name.0.as_str()) + .or_else(|| entity_ref.get::().map(|name| name.as_str())) + .unwrap_or("Unnamed"); + let actor_id = entity_ref + .get::() + .map(|id| id.0.as_str()) + .filter(|id| !id.trim().is_empty()) + .unwrap_or("unpersisted"); + ui.add(egui::Label::new(panel_heading(name)).truncate()); + if compact { + ui.small( + egui::RichText::new(format!("{kind:?} | Actor {actor_id}")) + .color(TEXT_DIM), + ); + ui.small( + egui::RichText::new(format!("Runtime {entity:?}")).color(TEXT_DIM), + ); + } else { + ui.small( + egui::RichText::new(format!( + "{kind:?} | Actor {actor_id} | Runtime {entity:?}" + )) + .color(TEXT_DIM), + ); + } + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(phosphor_icon(icons::DOTS_THREE_VERTICAL, 16.0).color(TEXT_DIM)); + ui.label(phosphor_icon(icons::LOCK_SIMPLE, 16.0).color(TEXT_DIM)); + }); + }); + }); + ui.add_space(6.0); + + let footer_reserve = 52.0; + let body_height = (ui.available_height() - footer_reserve).max(1.0); + ui.allocate_ui_with_layout( + egui::vec2(ui.available_width(), body_height), + egui::Layout::top_down(egui::Align::Min), + |ui| { + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + transform_inspector_ui(world, ui, entity); + + match kind { + ActorKind::StaticMesh + | ActorKind::ImportedModel + | ActorKind::Light + | ActorKind::Empty + | ActorKind::PrefabAnchor + | ActorKind::PlayerSpawn + | ActorKind::WeaponSpawn + | ActorKind::TriggerVolume + | ActorKind::PostProcessVolume + | ActorKind::TeamSpawn + | ActorKind::Objective => { + inspector::authoring_inspector_ui(world, ui, entity); + } + } + + world.resource_scope(|world, registry: Mut| { + registry.draw_sections(world, ui, entity); + }); + }); + }, + ); + inspector::add_component_footer(world, ui, entity); +} + +fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon { + match kind { + ActorKind::StaticMesh | ActorKind::ImportedModel => icons::CUBE, + ActorKind::Light => icons::LIGHTBULB, + ActorKind::PrefabAnchor => icons::PACKAGE, + ActorKind::PlayerSpawn => icons::PERSON_SIMPLE_RUN, + ActorKind::WeaponSpawn => icons::CROSSHAIR, + ActorKind::TriggerVolume => icons::SELECTION, + ActorKind::PostProcessVolume => icons::APERTURE, + ActorKind::TeamSpawn | ActorKind::Objective => icons::FLAG, + ActorKind::Empty => icons::CIRCLE, + } +} + +pub fn draw_multi_actor_inspector(world: &mut World, ui: &mut egui::Ui, entities: &[Entity]) { + ui.heading(format!("{} objects selected", entities.len())); + ui.label("Multi-edit: use the viewport gizmo for shared transform."); + if entities.len() == 1 { + draw_actor_inspector(world, ui, entities[0]); + } +} diff --git a/crates/editor/src/ui/actor_inspector/transform.rs b/crates/editor/src/ui/actor_inspector/transform.rs new file mode 100644 index 0000000..7ae8d03 --- /dev/null +++ b/crates/editor/src/ui/actor_inspector/transform.rs @@ -0,0 +1,109 @@ +//! Transform section (authoring) for the actor inspector. + +use bevy::prelude::*; +use bevy_egui::egui; +use egui_phosphor_icons::icons; + +use crate::history::set_transform_with_history; +use crate::ui::inspector::{ + apply_component_card_response, component_card, component_card_context, ComponentCardOptions, + COMPONENT_TRANSFORM, +}; +use crate::ui::theme::TEXT_DIM; + +const WIDE_TRANSFORM_ROW_WIDTH: f32 = 360.0; +const TRANSFORM_LABEL_WIDTH: f32 = 124.0; +const AXIS_FIELD_WIDTH: f32 = 76.0; + +pub fn transform_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { + let Some(mut transform) = world.get::(entity).cloned() else { + return; + }; + let old = transform; + let mut changed = false; + + let card = component_card_context( + world, + entity, + ComponentCardOptions::fixed(COMPONENT_TRANSFORM, "Transform", icons::ARROWS_OUT_CARDINAL), + ); + let card_response = component_card(ui, &card, |ui| { + if transform_row(ui, "Position", &mut transform.translation, 0.1) { + changed = true; + } + + let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ); + let mut rotation_deg = Vec3::new(pitch.to_degrees(), yaw.to_degrees(), roll.to_degrees()); + if transform_row(ui, "Rotation (degrees)", &mut rotation_deg, 0.5) { + transform.rotation = Quat::from_euler( + EulerRot::YXZ, + rotation_deg.y.to_radians(), + rotation_deg.x.to_radians(), + rotation_deg.z.to_radians(), + ); + changed = true; + } + + if transform_row(ui, "Scale", &mut transform.scale, 0.05) { + changed = true; + } + }); + + let reset = card_response.reset; + apply_component_card_response(world, entity, card_response); + + if reset { + set_transform_with_history(world, entity, old, Transform::default()); + } else if changed { + set_transform_with_history(world, entity, old, transform); + } +} + +fn transform_row(ui: &mut egui::Ui, label: &str, value: &mut Vec3, speed: f64) -> bool { + let mut changed = false; + if ui.available_width() < WIDE_TRANSFORM_ROW_WIDTH { + ui.vertical(|ui| { + ui.label(label); + ui.horizontal_wrapped(|ui| { + changed |= axis_drag(ui, "X", &mut value.x, speed); + changed |= axis_drag(ui, "Y", &mut value.y, speed); + changed |= axis_drag(ui, "Z", &mut value.z, speed); + }); + }); + } else { + ui.horizontal(|ui| { + ui.add_sized( + [TRANSFORM_LABEL_WIDTH.min(ui.available_width()), 20.0], + egui::Label::new(label), + ); + changed |= axis_drag(ui, "X", &mut value.x, speed); + changed |= axis_drag(ui, "Y", &mut value.y, speed); + changed |= axis_drag(ui, "Z", &mut value.z, speed); + }); + } + changed +} + +fn axis_drag(ui: &mut egui::Ui, axis: &str, value: &mut f32, speed: f64) -> bool { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(axis).color(axis_color(axis)).strong()); + ui.add_sized( + [AXIS_FIELD_WIDTH.min(ui.available_width().max(44.0)), 20.0], + egui::DragValue::new(value) + .speed(speed) + .max_decimals(3) + .min_decimals(1), + ) + .changed() + }) + .inner +} + +fn axis_color(axis: &str) -> egui::Color32 { + match axis { + "X" => egui::Color32::from_rgb(229, 82, 73), + "Y" => egui::Color32::from_rgb(103, 190, 92), + "Z" => egui::Color32::from_rgb(88, 132, 241), + _ => TEXT_DIM, + } +} diff --git a/crates/editor/src/ui/asset_browser/mod.rs b/crates/editor/src/ui/asset_browser/mod.rs new file mode 100644 index 0000000..ecc8578 --- /dev/null +++ b/crates/editor/src/ui/asset_browser/mod.rs @@ -0,0 +1,5 @@ +mod panel; +mod state; + +pub use panel::*; +pub use state::AssetBrowserUiState; diff --git a/crates/editor/src/ui/asset_browser/panel.rs b/crates/editor/src/ui/asset_browser/panel.rs new file mode 100644 index 0000000..6f149de --- /dev/null +++ b/crates/editor/src/ui/asset_browser/panel.rs @@ -0,0 +1,1167 @@ +//! Asset browser dock panel: project tree, file grid/list, and asset details. + +use std::fs; +use std::path::Path; +use std::time::SystemTime; + +use bevy::prelude::*; +use bevy_egui::egui; +use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities; +use egui_phosphor_icons::{icons, Icon}; + +use crate::assets::{ + spawn_asset_at, AssetSelection, EditorAsset, EditorAssetKind, EditorAssets, ASSETS_ROOT, + BUILTINS_FOLDER, +}; +use crate::scene_io::{SceneIo, SceneIoRequest}; + +use super::state::{AssetBrowserUiState, AssetBrowserView, AssetKindFilter, AssetSort}; +use crate::asset_db::{MaterialImportPolicy, ModelHierarchyMode, ModelPlacementMode}; +use crate::assets::{ + draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails, + AssetThumbnailCache, ThumbnailCacheSnapshot, +}; +use crate::ui::helpers::{ + apply_material_asset_to_selection, apply_texture_to_selection, asset_label, +}; +use crate::ui::theme::{panel_heading, ACCENT, BORDER, ELEVATED_BG, TEXT, TEXT_DIM, WIDGET_BG}; +use crate::ui::widgets::{icon_button_small, tool_button}; + +const FOOTER_HEIGHT: f32 = 64.0; +const DETAILS_WIDTH: f32 = 240.0; +const DETAILS_MIN_PANEL_WIDTH: f32 = 760.0; +const DETAILS_MIN_WIDTH: f32 = 200.0; +const TREE_MIN_PANEL_WIDTH: f32 = 540.0; +const TREE_MIN_WIDTH: f32 = 150.0; +const TREE_MAX_WIDTH: f32 = 240.0; +const MIN_CONTENT_WIDTH: f32 = 160.0; +const COMPACT_LIST_WIDTH: f32 = 560.0; +const PHOSPHOR: &str = "phosphor-regular"; + +#[derive(Clone)] +struct FolderSnapshot { + path: String, + name: String, + parent: Option, +} + +#[derive(Clone)] +struct AssetRow { + selection: AssetSelection, + asset: EditorAsset, + file_size: Option, + modified: Option, +} + +fn fit_width(ui: &egui::Ui, min: f32, max: f32) -> f32 { + let available = ui.available_width().max(1.0); + available.min(max).max(min.min(available)) +} + +fn icon_text(icon: Icon, size: f32) -> egui::RichText { + egui::RichText::new(icon.as_str()).font(egui::FontId::new( + size, + egui::FontFamily::Name(PHOSPHOR.into()), + )) +} + +fn exact_region( + ui: &mut egui::Ui, + size: egui::Vec2, + layout: egui::Layout, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) -> R { + let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover()); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect).layout(layout)); + add_contents(&mut child) +} + +pub fn asset_browser_ui( + world: &mut World, + ui: &mut egui::Ui, + selected_entities: &SelectedEntities, +) { + ui.vertical(|ui| { + asset_toolbar(world, ui); + + ui.separator(); + + let (current_folder, selected, folders, assets, cache_snapshot, state_snapshot) = { + let assets = world.resource::(); + let cache = world.resource::(); + let state = world.resource::(); + let folders: Vec = assets + .folders + .iter() + .map(|folder| FolderSnapshot { + path: folder.path.clone(), + name: folder.name.clone(), + parent: folder.parent.clone(), + }) + .collect(); + let asset_rows: Vec = assets + .assets + .iter() + .map(|asset| AssetRow { + selection: AssetSelection::from_asset(asset), + asset: asset.clone(), + file_size: asset.path.as_deref().and_then(file_size), + modified: asset.path.as_deref().and_then(modified_time), + }) + .collect(); + ( + assets.current_folder.clone(), + assets.selected.clone(), + folders, + asset_rows, + cache.snapshot(), + state_snapshot(state), + ) + }; + + prefetch_folder_thumbnails(world, ¤t_folder); + + let footer_reserved_height = FOOTER_HEIGHT + ui.spacing().item_spacing.y + 2.0; + let body_height = (ui.available_height() - footer_reserved_height).max(40.0); + let available_width = ui.available_width().max(1.0); + let show_tree = available_width >= TREE_MIN_PANEL_WIDTH; + let show_details = + state_snapshot.show_details && available_width >= DETAILS_MIN_PANEL_WIDTH; + let tree_width = if show_tree { + (available_width * 0.22).clamp(TREE_MIN_WIDTH, TREE_MAX_WIDTH) + } else { + 0.0 + }; + let details_width = if show_details { + DETAILS_WIDTH.min((available_width * 0.32).max(DETAILS_MIN_WIDTH)) + } else { + 0.0 + }; + let separator_budget = (show_tree as u8 as f32 + show_details as u8 as f32) * 10.0; + let content_width = (available_width - tree_width - details_width - separator_budget) + .max(MIN_CONTENT_WIDTH.min(available_width)); + + exact_region( + ui, + egui::vec2(available_width, body_height), + egui::Layout::left_to_right(egui::Align::Min), + |ui| { + if show_tree { + exact_region( + ui, + egui::vec2(tree_width, body_height), + egui::Layout::top_down(egui::Align::Min), + |ui| { + ui.label(panel_heading("Project")); + egui::ScrollArea::vertical() + .id_salt("asset_folder_tree") + .max_height(ui.available_height()) + .auto_shrink([false, false]) + .show(ui, |ui| { + for root in [BUILTINS_FOLDER, ASSETS_ROOT] { + folder_tree_branch( + world, + ui, + &folders, + root, + 0, + ¤t_folder, + ); + } + }); + }, + ); + ui.separator(); + } + + exact_region( + ui, + egui::vec2(content_width, body_height), + egui::Layout::top_down(egui::Align::Min), + |ui| { + content_header(world, ui, ¤t_folder, &folders, &assets); + egui::ScrollArea::vertical() + .id_salt("asset_content_area") + .max_height(ui.available_height()) + .auto_shrink([false, false]) + .show(ui, |ui| { + let child_folders = child_folders_for_content( + &folders, + ¤t_folder, + &state_snapshot.search, + ); + let visible_assets = + visible_assets(&assets, ¤t_folder, &state_snapshot); + + if child_folders.is_empty() && visible_assets.is_empty() { + empty_content(ui, &state_snapshot.search); + } else if state_snapshot.view == AssetBrowserView::Grid { + let thumbnail_size = state_snapshot + .thumbnail_size + .min((content_width - 26.0).max(48.0)); + asset_grid( + world, + ui, + selected_entities, + &selected, + &child_folders, + &visible_assets, + &cache_snapshot, + thumbnail_size, + ); + } else { + asset_list( + world, + ui, + selected_entities, + &selected, + &child_folders, + &visible_assets, + ); + } + }); + }, + ); + + if show_details { + ui.separator(); + exact_region( + ui, + egui::vec2(details_width, body_height), + egui::Layout::top_down(egui::Align::Min), + |ui| { + details_panel(world, ui, selected_entities); + }, + ); + } + }, + ); + + ui.separator(); + exact_region( + ui, + egui::vec2(available_width, FOOTER_HEIGHT), + egui::Layout::top_down(egui::Align::Min), + |ui| { + egui::Frame::new() + .fill(ELEVATED_BG.linear_multiply(0.5)) + .inner_margin(egui::Margin::symmetric(8, 6)) + .show(ui, |ui| { + ui.set_max_height(FOOTER_HEIGHT - 12.0); + selection_footer(world, ui, selected_entities); + }); + }, + ); + }); +} + +#[derive(Clone)] +struct AssetBrowserStateSnapshot { + search: String, + view: AssetBrowserView, + sort: AssetSort, + kind_filter: AssetKindFilter, + thumbnail_size: f32, + recursive: bool, + show_details: bool, +} + +fn state_snapshot(state: &AssetBrowserUiState) -> AssetBrowserStateSnapshot { + AssetBrowserStateSnapshot { + search: state.search.clone(), + view: state.view, + sort: state.sort, + kind_filter: state.kind_filter, + thumbnail_size: state.thumbnail_size, + recursive: state.recursive, + show_details: state.show_details, + } +} + +fn asset_toolbar(world: &mut World, ui: &mut egui::Ui) { + ui.horizontal_wrapped(|ui| { + if icon_button_small(ui, icons::ARROWS_CLOCKWISE, "Refresh").clicked() { + world.resource_mut::().refresh(); + invalidate_on_catalog_refresh(world); + } + if icon_button_small(ui, icons::UPLOAD, "Import assets").clicked() { + world.resource_mut::().request = Some(SceneIoRequest::ImportAssets); + } + if icon_button_small(ui, icons::ARROW_UP, "Parent folder").clicked() { + navigate_to_parent(world); + } + + ui.separator(); + + let mut state = world.resource_mut::(); + ui.add( + egui::TextEdit::singleline(&mut state.search) + .hint_text("Search assets...") + .desired_width(fit_width(ui, 120.0, 190.0)), + ); + ui.checkbox(&mut state.recursive, "Subfolders"); + + egui::ComboBox::from_id_salt("asset_kind_filter") + .selected_text(kind_filter_label(state.kind_filter)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut state.kind_filter, AssetKindFilter::All, "All"); + ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Model, "Models"); + ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Texture, "Textures"); + ui.selectable_value( + &mut state.kind_filter, + AssetKindFilter::Material, + "Materials", + ); + ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Level, "Levels"); + ui.selectable_value(&mut state.kind_filter, AssetKindFilter::Prefab, "Prefabs"); + ui.selectable_value( + &mut state.kind_filter, + AssetKindFilter::Builtin, + "Built-ins", + ); + }); + + egui::ComboBox::from_id_salt("asset_sort") + .selected_text(sort_label(state.sort)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut state.sort, AssetSort::Name, "Name"); + ui.selectable_value(&mut state.sort, AssetSort::Kind, "Type"); + ui.selectable_value(&mut state.sort, AssetSort::Modified, "Modified"); + ui.selectable_value(&mut state.sort, AssetSort::Size, "Size"); + }); + + ui.separator(); + if tool_button( + ui, + icons::GRID_FOUR, + state.view == AssetBrowserView::Grid, + "Grid view", + ) + .clicked() + { + state.view = AssetBrowserView::Grid; + } + if tool_button( + ui, + icons::LIST, + state.view == AssetBrowserView::List, + "List view", + ) + .clicked() + { + state.view = AssetBrowserView::List; + } + ui.add_sized( + [fit_width(ui, 72.0, 120.0), 20.0], + egui::Slider::new(&mut state.thumbnail_size, 48.0..=112.0).show_value(false), + ); + ui.checkbox(&mut state.show_details, "Details"); + }); +} + +fn content_header( + world: &mut World, + ui: &mut egui::Ui, + current_folder: &str, + folders: &[FolderSnapshot], + assets: &[AssetRow], +) { + ui.horizontal_wrapped(|ui| { + ui.label(panel_heading("Assets")); + ui.separator(); + breadcrumb(world, ui, current_folder); + }); + let direct_assets = assets + .iter() + .filter(|row| row.asset.folder_path == current_folder) + .count(); + let direct_folders = folders + .iter() + .filter(|folder| folder.parent.as_deref() == Some(current_folder)) + .count(); + ui.small( + egui::RichText::new(format!("{direct_folders} folders, {direct_assets} assets")) + .color(TEXT_DIM), + ); +} + +fn breadcrumb(world: &mut World, ui: &mut egui::Ui, current_folder: &str) { + let mut parts = Vec::new(); + let mut cursor = Some(current_folder.to_string()); + while let Some(path) = cursor { + let label = if path == BUILTINS_FOLDER { + "Built-ins".to_string() + } else { + Path::new(&path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(&path) + .to_string() + }; + cursor = path.rfind('/').map(|index| path[..index].to_string()); + parts.push((path, label)); + } + parts.reverse(); + for (index, (path, label)) in parts.iter().enumerate() { + if index > 0 { + ui.label(egui::RichText::new("/").color(TEXT_DIM)); + } + if ui.link(label).clicked() { + world.resource_mut::().current_folder = path.clone(); + } + } +} + +fn selection_footer(world: &mut World, ui: &mut egui::Ui, selected_entities: &SelectedEntities) { + if let Some(asset) = world.resource::().selected_asset().cloned() { + ui.add(egui::Label::new(format!("Selected: {}", asset_label(&asset))).truncate()); + ui.horizontal_wrapped(|ui| { + if matches!(asset.kind, EditorAssetKind::Texture) + && ui.button("Apply Texture To Selection").clicked() + { + apply_texture_to_selection(world, &asset, selected_entities); + } + if matches!( + asset.kind, + EditorAssetKind::Primitive(_) + | EditorAssetKind::Light(_) + | EditorAssetKind::Model + | EditorAssetKind::Prefab + | EditorAssetKind::Level + ) && ui.button("Place At Origin").clicked() + { + spawn_asset_at(world, &asset, Vec3::ZERO); + } + }); + } else { + ui.label(egui::RichText::new("No asset selected").color(TEXT_DIM)); + } +} + +fn folder_tree_branch( + world: &mut World, + ui: &mut egui::Ui, + folders: &[FolderSnapshot], + node_path: &str, + depth: usize, + current_folder: &str, +) { + let Some(folder) = folders.iter().find(|folder| folder.path == node_path) else { + return; + }; + let path = &folder.path; + let name = &folder.name; + let children: Vec<&FolderSnapshot> = folders + .iter() + .filter(|child| child.parent.as_deref() == Some(path.as_str())) + .collect(); + let expanded = children.is_empty() + || world + .resource::() + .expanded_folders + .contains(path); + + let selected = current_folder == *path; + ui.horizontal(|ui| { + ui.add_space(depth as f32 * 12.0); + if children.is_empty() { + ui.add_sized( + [18.0, 20.0], + egui::Label::new(icon_text(icons::DOT_OUTLINE, 12.0).color(TEXT_DIM)), + ); + } else { + let icon = if expanded { + icons::CARET_DOWN + } else { + icons::CARET_RIGHT + }; + if ui + .add_sized( + [18.0, 18.0], + egui::Button::new(icon_text(icon, 12.0)).frame(false), + ) + .clicked() + { + let mut state = world.resource_mut::(); + if expanded { + state.expanded_folders.remove(path); + } else { + state.expanded_folders.insert(path.clone()); + } + } + } + ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM)); + if ui + .add_sized( + [fit_width(ui, 72.0, f32::INFINITY), 20.0], + egui::Button::selectable(selected, name.as_str()), + ) + .clicked() + { + world.resource_mut::().current_folder = path.clone(); + } + }); + + if expanded { + for child in children { + folder_tree_branch(world, ui, folders, &child.path, depth + 1, current_folder); + } + } +} + +fn child_folders_for_content( + folders: &[FolderSnapshot], + current_folder: &str, + search: &str, +) -> Vec { + if !search.trim().is_empty() { + return Vec::new(); + } + let mut children: Vec = folders + .iter() + .filter(|folder| folder.parent.as_deref() == Some(current_folder)) + .cloned() + .collect(); + children.sort_by(|a, b| natural_cmp(&a.name, &b.name)); + children +} + +fn visible_assets( + assets: &[AssetRow], + current_folder: &str, + state: &AssetBrowserStateSnapshot, +) -> Vec { + let search = state.search.trim().to_ascii_lowercase(); + let mut rows: Vec = assets + .iter() + .filter(|row| { + if state.recursive { + row.asset.folder_path == current_folder + || row + .asset + .folder_path + .strip_prefix(current_folder) + .is_some_and(|rest| rest.starts_with('/')) + } else { + row.asset.folder_path == current_folder + } + }) + .filter(|row| asset_matches_kind_filter(&row.asset, state.kind_filter)) + .filter(|row| { + search.is_empty() + || row.asset.label.to_ascii_lowercase().contains(&search) + || row + .asset + .path + .as_deref() + .unwrap_or_default() + .to_ascii_lowercase() + .contains(&search) + }) + .cloned() + .collect(); + + rows.sort_by(|a, b| match state.sort { + AssetSort::Name => natural_cmp(&a.asset.label, &b.asset.label), + AssetSort::Kind => kind_label(&a.asset.kind).cmp(kind_label(&b.asset.kind)), + AssetSort::Modified => b.modified.cmp(&a.modified), + AssetSort::Size => b.file_size.cmp(&a.file_size), + }); + rows +} + +fn asset_grid( + world: &mut World, + ui: &mut egui::Ui, + selected_entities: &SelectedEntities, + selected: &Option, + folders: &[FolderSnapshot], + assets: &[AssetRow], + cache_snapshot: &ThumbnailCacheSnapshot, + thumbnail_size: f32, +) { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = egui::vec2(6.0, 6.0); + for folder in folders { + let response = draw_folder_cell(ui, folder, thumbnail_size); + if response.double_clicked() || response.clicked() { + world.resource_mut::().current_folder = folder.path.clone(); + } + } + for row in assets { + let selection = &row.selection; + let asset = &row.asset; + let is_selected = selected.as_ref() == Some(selection); + let failed = cache_snapshot.is_failed(asset); + let response = draw_asset_cell_with( + ui, + asset, + cache_snapshot.texture_for(asset), + cache_snapshot.is_pending(asset), + failed.then_some("failed"), + is_selected, + thumbnail_size, + ); + if failed && response.double_clicked() { + let key = crate::assets::asset_cache_key(asset); + world + .resource_mut::() + .retry(&key); + } + + if response.clicked() { + world + .resource_mut::() + .select(selection.clone()); + } + if response.drag_started() { + world + .resource_mut::() + .start_drag(selection.clone()); + } + response.context_menu(|ui| { + asset_context_menu(world, ui, asset, selected_entities); + }); + } + }); +} + +fn asset_list( + world: &mut World, + ui: &mut egui::Ui, + selected_entities: &SelectedEntities, + selected: &Option, + folders: &[FolderSnapshot], + assets: &[AssetRow], +) { + if ui.available_width() < COMPACT_LIST_WIDTH { + compact_asset_list(world, ui, selected_entities, selected, folders, assets); + return; + } + + egui::Grid::new("asset_browser_list") + .num_columns(5) + .striped(true) + .spacing(egui::vec2(16.0, 4.0)) + .show(ui, |ui| { + ui.strong("Name"); + ui.strong("Type"); + ui.strong("Size"); + ui.strong("Modified"); + ui.strong("Path"); + ui.end_row(); + + for folder in folders { + let mut clicked = false; + ui.horizontal(|ui| { + ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM)); + clicked = ui.selectable_label(false, folder.name.as_str()).clicked(); + }); + ui.label("Folder"); + ui.label("-"); + ui.label("-"); + ui.label(folder.path.as_str()); + ui.end_row(); + if clicked { + world.resource_mut::().current_folder = folder.path.clone(); + } + } + + for row in assets { + let is_selected = selected.as_ref() == Some(&row.selection); + let mut response = None; + ui.horizontal(|ui| { + ui.label(icon_text(kind_icon(&row.asset.kind), 14.0).color(TEXT_DIM)); + response = Some(ui.selectable_label(is_selected, row.asset.label.as_str())); + }); + let response = response.expect("asset list response"); + ui.label(kind_label(&row.asset.kind)); + ui.label( + row.file_size + .map(format_bytes) + .unwrap_or_else(|| "-".to_string()), + ); + ui.label( + row.modified + .map(format_modified) + .unwrap_or_else(|| "-".to_string()), + ); + ui.label(row.asset.path.as_deref().unwrap_or("Built-in")); + ui.end_row(); + + if response.clicked() { + world + .resource_mut::() + .select(row.selection.clone()); + } + if response.drag_started() { + world + .resource_mut::() + .start_drag(row.selection.clone()); + } + response.context_menu(|ui| { + asset_context_menu(world, ui, &row.asset, selected_entities); + }); + } + }); +} + +fn compact_asset_list( + world: &mut World, + ui: &mut egui::Ui, + selected_entities: &SelectedEntities, + selected: &Option, + folders: &[FolderSnapshot], + assets: &[AssetRow], +) { + for folder in folders { + let mut clicked = false; + ui.horizontal(|ui| { + ui.label(icon_text(icons::FOLDER, 14.0).color(TEXT_DIM)); + clicked = ui + .add_sized( + [fit_width(ui, 120.0, f32::INFINITY), 22.0], + egui::Button::selectable(false, folder.name.as_str()), + ) + .clicked(); + }); + if clicked { + world.resource_mut::().current_folder = folder.path.clone(); + } + ui.small(egui::RichText::new("Folder").color(TEXT_DIM)); + ui.add_space(4.0); + } + + for row in assets { + let is_selected = selected.as_ref() == Some(&row.selection); + let mut response = None; + ui.horizontal(|ui| { + ui.label(icon_text(kind_icon(&row.asset.kind), 14.0).color(TEXT_DIM)); + response = Some(ui.add_sized( + [fit_width(ui, 120.0, f32::INFINITY), 22.0], + egui::Button::selectable(is_selected, row.asset.label.as_str()), + )); + }); + let response = response.expect("compact asset list response"); + let size = row + .file_size + .map(format_bytes) + .unwrap_or_else(|| "-".to_string()); + let modified = row + .modified + .map(format_modified) + .unwrap_or_else(|| "-".to_string()); + let path = row.asset.path.as_deref().unwrap_or("Built-in"); + ui.add( + egui::Label::new( + egui::RichText::new(format!( + "{} | {} | {} | {}", + kind_label(&row.asset.kind), + size, + modified, + path + )) + .color(TEXT_DIM), + ) + .truncate(), + ); + ui.add_space(4.0); + + if response.clicked() { + world + .resource_mut::() + .select(row.selection.clone()); + } + if response.drag_started() { + world + .resource_mut::() + .start_drag(row.selection.clone()); + } + response.context_menu(|ui| { + asset_context_menu(world, ui, &row.asset, selected_entities); + }); + } +} + +fn draw_folder_cell( + ui: &mut egui::Ui, + folder: &FolderSnapshot, + thumbnail_size: f32, +) -> egui::Response { + let thumb_size = thumbnail_size.clamp(48.0, 112.0); + let cell_size = egui::vec2(thumb_size + 20.0, thumb_size + 30.0); + let (rect, response) = ui.allocate_exact_size(cell_size, egui::Sense::click()); + let fill = if response.hovered() { + ELEVATED_BG + } else { + WIDGET_BG + }; + ui.painter().rect( + rect, + 4.0, + fill, + egui::Stroke::new(1.0, BORDER), + egui::StrokeKind::Inside, + ); + ui.painter().text( + rect.center_top() + egui::vec2(0.0, 10.0 + thumb_size * 0.45), + egui::Align2::CENTER_CENTER, + icons::FOLDER_SIMPLE.as_str(), + egui::FontId::new( + (thumb_size * 0.48).max(24.0), + egui::FontFamily::Name("phosphor-regular".into()), + ), + TEXT, + ); + ui.painter().text( + egui::pos2(rect.center().x, rect.max.y - 6.0), + egui::Align2::CENTER_BOTTOM, + folder.name.as_str(), + egui::FontId::new(11.0, egui::FontFamily::Proportional), + TEXT, + ); + response +} + +fn details_panel(world: &mut World, ui: &mut egui::Ui, selected_entities: &SelectedEntities) { + ui.label(panel_heading("Details")); + ui.separator(); + if let Some(asset) = world.resource::().selected_asset().cloned() { + ui.horizontal_wrapped(|ui| { + ui.label( + egui::RichText::new(kind_icon(&asset.kind).as_str()) + .font(egui::FontId::new( + 30.0, + egui::FontFamily::Name("phosphor-regular".into()), + )) + .color(ACCENT), + ); + ui.vertical(|ui| { + ui.strong(asset.label.as_str()); + ui.small(egui::RichText::new(kind_label(&asset.kind)).color(TEXT_DIM)); + }); + }); + ui.add_space(8.0); + detail_row(ui, "Path", asset.path.as_deref().unwrap_or("Built-in")); + detail_row(ui, "Folder", asset.folder_path.as_str()); + if let Some(path) = asset.path.as_deref() { + if let Some(record) = world + .get_resource::() + .and_then(|registry| crate::asset_db::find_asset_by_path(registry, path)) + { + detail_row(ui, "Asset ID", &record.id.as_string()); + ui.separator(); + ui.label(panel_heading("Import Settings")); + let mut settings = record.import_settings.clone(); + let mut dirty = false; + if ui + .add(egui::Slider::new(&mut settings.scale, 0.01..=10.0).text("Scale")) + .changed() + { + dirty = true; + } + if ui + .checkbox(&mut settings.generate_collider, "Generate collider") + .changed() + { + dirty = true; + } + if ui.checkbox(&mut settings.lod0_only, "LOD0 only").changed() { + dirty = true; + } + if matches!(asset.kind, EditorAssetKind::Model) { + egui::ComboBox::from_id_salt("model_placement_mode") + .selected_text(match settings.placement_mode { + ModelPlacementMode::StaticAsset => "Static Asset", + ModelPlacementMode::SceneInstance => "Scene Instance", + }) + .show_ui(ui, |ui| { + dirty |= ui + .selectable_value( + &mut settings.placement_mode, + ModelPlacementMode::StaticAsset, + "Static Asset", + ) + .changed(); + dirty |= ui + .selectable_value( + &mut settings.placement_mode, + ModelPlacementMode::SceneInstance, + "Scene Instance", + ) + .changed(); + }); + egui::ComboBox::from_id_salt("model_hierarchy_mode") + .selected_text(match settings.hierarchy_mode { + ModelHierarchyMode::SingleActor => "One Actor", + ModelHierarchyMode::SourceHierarchy => "Source Hierarchy", + }) + .show_ui(ui, |ui| { + dirty |= ui + .selectable_value( + &mut settings.hierarchy_mode, + ModelHierarchyMode::SingleActor, + "One Actor", + ) + .changed(); + dirty |= ui + .selectable_value( + &mut settings.hierarchy_mode, + ModelHierarchyMode::SourceHierarchy, + "Source Hierarchy", + ) + .changed(); + }); + egui::ComboBox::from_id_salt("model_material_policy") + .selected_text(match settings.material_policy { + MaterialImportPolicy::SourceMaterials => "Source Materials", + MaterialImportPolicy::AuthoringOverride => "Authoring Override", + }) + .show_ui(ui, |ui| { + dirty |= ui + .selectable_value( + &mut settings.material_policy, + MaterialImportPolicy::SourceMaterials, + "Source Materials", + ) + .changed(); + dirty |= ui + .selectable_value( + &mut settings.material_policy, + MaterialImportPolicy::AuthoringOverride, + "Authoring Override", + ) + .changed(); + }); + if let Some(manifest) = settings.static_mesh_manifest_path.as_deref() { + detail_row(ui, "Static mesh artifact", manifest); + } + } + if dirty { + if let Some(mut registry) = + world.get_resource_mut::() + { + crate::asset_db::update_import_settings(&mut registry, path, settings); + if matches!(asset.kind, EditorAssetKind::Model) { + if let Some(record) = + crate::asset_db::find_asset_mut_by_path(&mut registry, path) + { + if let Err(error) = + crate::assets::static_mesh::refresh_static_mesh_artifact(record) + { + warn!("Static mesh import settings refresh failed: {error}"); + } + } + } + let _ = crate::asset_db::save_registry(®istry); + registry.index_dirty = false; + } + } + if !record.dependencies.is_empty() { + ui.separator(); + ui.label(panel_heading("Dependencies")); + for dep in &record.dependencies { + ui.small(dep); + } + } + } + detail_row( + ui, + "Size", + file_size(path).map(format_bytes).as_deref().unwrap_or("-"), + ); + detail_row( + ui, + "Modified", + modified_time(path) + .map(format_modified) + .as_deref() + .unwrap_or("-"), + ); + } + ui.separator(); + if matches!(asset.kind, EditorAssetKind::Texture) + && ui.button("Apply Texture To Selection").clicked() + { + apply_texture_to_selection(world, &asset, selected_entities); + } + if matches!(asset.kind, EditorAssetKind::Material) + && ui.button("Apply Material To Selection").clicked() + { + apply_material_asset_to_selection(world, &asset, selected_entities); + } + if matches!( + asset.kind, + EditorAssetKind::Primitive(_) + | EditorAssetKind::Light(_) + | EditorAssetKind::Model + | EditorAssetKind::Prefab + | EditorAssetKind::Level + ) && ui.button("Place At Origin").clicked() + { + spawn_asset_at(world, &asset, Vec3::ZERO); + } + } else { + ui.label(egui::RichText::new("No asset selected").color(TEXT_DIM)); + } +} + +fn detail_row(ui: &mut egui::Ui, label: &str, value: &str) { + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new(label).color(TEXT_DIM)); + ui.add(egui::Label::new(value).wrap().selectable(false)); + }); +} + +fn empty_content(ui: &mut egui::Ui, search: &str) { + ui.add_space(24.0); + ui.vertical_centered(|ui| { + ui.label( + egui::RichText::new(icons::FOLDER_OPEN.as_str()).font(egui::FontId::new( + 28.0, + egui::FontFamily::Name("phosphor-regular".into()), + )), + ); + if search.trim().is_empty() { + ui.label(egui::RichText::new("Folder is empty").color(TEXT_DIM)); + } else { + ui.label(egui::RichText::new("No matching assets").color(TEXT_DIM)); + } + }); +} + +fn navigate_to_parent(world: &mut World) { + let current = world.resource::().current_folder.clone(); + if let Some(parent) = current.rfind('/').map(|index| current[..index].to_string()) { + world.resource_mut::().current_folder = parent; + } +} + +fn asset_matches_kind_filter(asset: &EditorAsset, filter: AssetKindFilter) -> bool { + match filter { + AssetKindFilter::All => true, + AssetKindFilter::Model => matches!(asset.kind, EditorAssetKind::Model), + AssetKindFilter::Texture => matches!(asset.kind, EditorAssetKind::Texture), + AssetKindFilter::Material => matches!(asset.kind, EditorAssetKind::Material), + AssetKindFilter::Level => matches!(asset.kind, EditorAssetKind::Level), + AssetKindFilter::Prefab => matches!(asset.kind, EditorAssetKind::Prefab), + AssetKindFilter::Builtin => asset.path.is_none(), + } +} + +fn kind_filter_label(filter: AssetKindFilter) -> &'static str { + match filter { + AssetKindFilter::All => "All", + AssetKindFilter::Model => "Models", + AssetKindFilter::Texture => "Textures", + AssetKindFilter::Material => "Materials", + AssetKindFilter::Level => "Levels", + AssetKindFilter::Prefab => "Prefabs", + AssetKindFilter::Builtin => "Built-ins", + } +} + +fn sort_label(sort: AssetSort) -> &'static str { + match sort { + AssetSort::Name => "Name", + AssetSort::Kind => "Type", + AssetSort::Modified => "Modified", + AssetSort::Size => "Size", + } +} + +fn kind_label(kind: &EditorAssetKind) -> &'static str { + match kind { + EditorAssetKind::Primitive(_) => "Primitive", + EditorAssetKind::Light(_) => "Light", + EditorAssetKind::Model => "Model", + EditorAssetKind::Texture => "Texture", + EditorAssetKind::Material => "Material", + EditorAssetKind::Level => "Level", + EditorAssetKind::Prefab => "Prefab", + EditorAssetKind::PostProcessVolume => "Post Process Volume", + EditorAssetKind::PostProcessEffect => "Post FX", + EditorAssetKind::RenderingProfile => "Rendering Profile", + EditorAssetKind::ShaderSchema => "Shader Schema", + } +} + +fn file_size(path: &str) -> Option { + fs::metadata(path).ok().map(|metadata| metadata.len()) +} + +fn modified_time(path: &str) -> Option { + fs::metadata(path) + .ok() + .and_then(|metadata| metadata.modified().ok()) +} + +fn format_bytes(bytes: u64) -> String { + const KIB: f64 = 1024.0; + const MIB: f64 = KIB * 1024.0; + const GIB: f64 = MIB * 1024.0; + let bytes = bytes as f64; + if bytes >= GIB { + format!("{:.1} GB", bytes / GIB) + } else if bytes >= MIB { + format!("{:.1} MB", bytes / MIB) + } else if bytes >= KIB { + format!("{:.1} KB", bytes / KIB) + } else { + format!("{bytes:.0} B") + } +} + +fn format_modified(time: SystemTime) -> String { + let Ok(duration) = SystemTime::now().duration_since(time) else { + return "Just now".to_string(); + }; + let days = duration.as_secs() / 86_400; + if days == 0 { + "Today".to_string() + } else if days == 1 { + "Yesterday".to_string() + } else if days < 30 { + format!("{days} days ago") + } else if days < 365 { + format!("{} months ago", days / 30) + } else { + format!("{} years ago", days / 365) + } +} + +fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { + a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) +} + +fn asset_context_menu( + world: &mut World, + ui: &mut egui::Ui, + asset: &EditorAsset, + selected_entities: &SelectedEntities, +) { + if matches!( + asset.kind, + EditorAssetKind::Primitive(_) + | EditorAssetKind::Light(_) + | EditorAssetKind::Model + | EditorAssetKind::Prefab + | EditorAssetKind::Level + ) && ui.button("Place at Origin").clicked() + { + spawn_asset_at(world, asset, Vec3::ZERO); + ui.close(); + } + if matches!(asset.kind, EditorAssetKind::Texture) + && ui.button("Apply as Base Color Texture").clicked() + { + apply_texture_to_selection(world, asset, selected_entities); + ui.close(); + } + if matches!(asset.kind, EditorAssetKind::Material) + && ui.button("Apply Material To Selection").clicked() + { + apply_material_asset_to_selection(world, asset, selected_entities); + ui.close(); + } +} diff --git a/crates/editor/src/ui/asset_browser/state.rs b/crates/editor/src/ui/asset_browser/state.rs new file mode 100644 index 0000000..b34468c --- /dev/null +++ b/crates/editor/src/ui/asset_browser/state.rs @@ -0,0 +1,62 @@ +//! Asset browser UI state and view enums. + +use std::collections::HashSet; + +use bevy::prelude::*; + +use crate::assets::{ASSETS_ROOT, BUILTINS_FOLDER}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AssetBrowserView { + Grid, + List, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AssetSort { + Name, + Kind, + Modified, + Size, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AssetKindFilter { + All, + Model, + Texture, + Material, + Level, + Prefab, + Builtin, +} + +#[derive(Resource, Debug)] +pub struct AssetBrowserUiState { + pub(crate) search: String, + pub(crate) view: AssetBrowserView, + pub(crate) sort: AssetSort, + pub(crate) kind_filter: AssetKindFilter, + pub(crate) thumbnail_size: f32, + pub(crate) recursive: bool, + pub(crate) show_details: bool, + pub(crate) expanded_folders: HashSet, +} + +impl Default for AssetBrowserUiState { + fn default() -> Self { + let mut expanded_folders = HashSet::new(); + expanded_folders.insert(ASSETS_ROOT.to_string()); + expanded_folders.insert(BUILTINS_FOLDER.to_string()); + Self { + search: String::new(), + view: AssetBrowserView::Grid, + sort: AssetSort::Name, + kind_filter: AssetKindFilter::All, + thumbnail_size: 72.0, + recursive: false, + show_details: true, + expanded_folders, + } + } +} diff --git a/crates/editor/src/ui/component_registry.rs b/crates/editor/src/ui/component_registry.rs new file mode 100644 index 0000000..c36ef85 --- /dev/null +++ b/crates/editor/src/ui/component_registry.rs @@ -0,0 +1,158 @@ +//! Editor-visible component registry for actor inspector palettes. + +use bevy::prelude::*; +use egui_phosphor_icons::icons; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EditorComponentCategory { + Rendering, + Physics, + Gameplay, + Editor, +} + +#[derive(Debug, Clone)] +pub struct EditorComponentDescriptor { + pub type_name: &'static str, + pub display_name: &'static str, + pub category: EditorComponentCategory, + pub addable: bool, + pub removable: bool, + pub reorderable: bool, + pub icon: &'static str, + pub search_terms: &'static [&'static str], +} + +#[derive(Resource, Debug, Clone)] +pub struct EditorComponentRegistry { + pub descriptors: Vec, +} + +impl Default for EditorComponentRegistry { + fn default() -> Self { + Self { + descriptors: vec![ + EditorComponentDescriptor { + type_name: "shared::components::StaticMeshRenderer", + display_name: "Static Mesh Renderer", + category: EditorComponentCategory::Rendering, + addable: true, + removable: true, + reorderable: true, + icon: icons::CUBE.as_str(), + search_terms: &["mesh", "renderer", "rendering", "model"], + }, + EditorComponentDescriptor { + type_name: "shared::components::MaterialDesc", + display_name: "Authoring Material", + category: EditorComponentCategory::Rendering, + addable: true, + removable: true, + reorderable: true, + icon: icons::PALETTE.as_str(), + search_terms: &["material", "shader", "texture"], + }, + EditorComponentDescriptor { + type_name: "shared::components::RigidBodyDesc", + display_name: "Rigid Body", + category: EditorComponentCategory::Physics, + addable: true, + removable: true, + reorderable: true, + icon: icons::SPHERE.as_str(), + search_terms: &["body", "physics", "rigidbody"], + }, + EditorComponentDescriptor { + type_name: "shared::components::ColliderDesc", + display_name: "Collider", + category: EditorComponentCategory::Physics, + addable: true, + removable: true, + reorderable: true, + icon: icons::SELECTION.as_str(), + search_terms: &["collision", "trigger", "physics"], + }, + EditorComponentDescriptor { + type_name: "shared::components::LightDesc", + display_name: "Light", + category: EditorComponentCategory::Rendering, + addable: true, + removable: true, + reorderable: true, + icon: icons::LIGHTBULB.as_str(), + search_terms: &["light", "lamp", "illumination"], + }, + EditorComponentDescriptor { + type_name: "shared::components::Primitive", + display_name: "Primitive", + category: EditorComponentCategory::Rendering, + addable: true, + removable: true, + reorderable: true, + icon: icons::CUBE.as_str(), + search_terms: &["box", "sphere", "ramp", "primitive"], + }, + EditorComponentDescriptor { + type_name: "shared::components::PlayerSpawn", + display_name: "Player Spawn", + category: EditorComponentCategory::Gameplay, + addable: true, + removable: true, + reorderable: true, + icon: icons::PERSON_SIMPLE_RUN.as_str(), + search_terms: &["player", "spawn", "start"], + }, + EditorComponentDescriptor { + type_name: "shared::components::WeaponSpawn", + display_name: "Weapon Spawn", + category: EditorComponentCategory::Gameplay, + addable: true, + removable: true, + reorderable: true, + icon: icons::CROSSHAIR.as_str(), + search_terms: &["weapon", "spawn"], + }, + EditorComponentDescriptor { + type_name: "shared::components::TriggerVolume", + display_name: "Trigger Volume", + category: EditorComponentCategory::Gameplay, + addable: true, + removable: true, + reorderable: true, + icon: icons::SELECTION.as_str(), + search_terms: &["trigger", "volume", "event"], + }, + EditorComponentDescriptor { + type_name: "shared::components::TeamSpawn", + display_name: "Team Spawn", + category: EditorComponentCategory::Gameplay, + addable: true, + removable: true, + reorderable: true, + icon: icons::FLAG.as_str(), + search_terms: &["team", "spawn"], + }, + EditorComponentDescriptor { + type_name: "shared::components::ObjectiveMarker", + display_name: "Objective Marker", + category: EditorComponentCategory::Gameplay, + addable: true, + removable: true, + reorderable: true, + icon: icons::FLAG.as_str(), + search_terms: &["objective", "marker", "gameplay"], + }, + EditorComponentDescriptor { + type_name: "shared::components::InspectorOrder", + display_name: "Inspector Order", + category: EditorComponentCategory::Editor, + addable: false, + removable: false, + reorderable: false, + icon: icons::LIST_BULLETS.as_str(), + search_terms: &["editor", "order"], + }, + ], + } + } +} diff --git a/crates/editor/src/ui/diagnostics.rs b/crates/editor/src/ui/diagnostics.rs new file mode 100644 index 0000000..90f7501 --- /dev/null +++ b/crates/editor/src/ui/diagnostics.rs @@ -0,0 +1,65 @@ +//! Detailed diagnostics (relocated from Status dock tab). + +use bevy::prelude::*; +use bevy_egui::egui; +use shared::LevelObject; + +use crate::assets::EditorAssets; +use crate::history::EditorHistory; +use crate::project_io::ProjectWorkspace; +use crate::scene_io::SceneIo; + +#[derive(Resource, Default)] +pub struct DiagnosticsPanel { + pub open: bool, +} + +pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) { + let scene_io = world.resource::(); + let dirty = if scene_io.dirty { " (dirty)" } else { "" }; + ui.label(format!("Scene: {}{dirty}", scene_io.active_path_label())); + ui.label(format!("Status: {}", scene_io.status)); + ui.label(format!( + "History: {}", + world.resource::().status + )); + ui.label(format!( + "Assets: {}", + world.resource::().status + )); + + let workspace = world.resource::(); + ui.label(format!("Project root: {}", workspace.root)); + ui.label(format!( + "Project settings: {}{}", + workspace.settings_path, + if workspace.settings_dirty { + " (dirty)" + } else { + "" + } + )); + if workspace.layout_dirty { + ui.small("Dock layout modified (unsaved)"); + } + + let mut level_query = world.query_filtered::>(); + let count = level_query.iter(world).count(); + ui.label(format!("Level objects: {count}")); + + ui.separator(); + ui.small("Native Bevy scenes are saved to assets/levels/*.scn.ron"); + ui.small("BRP is enabled on the editor for external tooling."); +} + +pub fn diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) { + if !*open { + return; + } + egui::Window::new("Diagnostics") + .open(open) + .default_width(420.0) + .show(ctx, |ui| { + diagnostics_ui(world, ui); + }); +} diff --git a/crates/editor/src/ui/dock_tabs.rs b/crates/editor/src/ui/dock_tabs.rs new file mode 100644 index 0000000..67afb8f --- /dev/null +++ b/crates/editor/src/ui/dock_tabs.rs @@ -0,0 +1,158 @@ +//! Dock tab open/focus helpers and default panel node placement. + +use egui_dock::{DockState, Node, NodeIndex, SurfaceIndex, TabIndex, Tree}; +use serde::{Deserialize, Serialize}; + +use super::EditorTab; + +/// Preferred leaf nodes for re-opening closed panels (updated when layout is built or loaded). +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct PanelNodes { + pub main_view: usize, + pub hierarchy: usize, + pub inspector: usize, + pub bottom: usize, +} + +impl PanelNodes { + pub fn main_view(&self) -> NodeIndex { + NodeIndex::from(self.main_view) + } + + pub fn hierarchy(&self) -> NodeIndex { + NodeIndex::from(self.hierarchy) + } + + pub fn inspector(&self) -> NodeIndex { + NodeIndex::from(self.inspector) + } + + pub fn bottom(&self) -> NodeIndex { + NodeIndex::from(self.bottom) + } + + pub fn preferred_node(&self, tab: &EditorTab) -> NodeIndex { + match tab { + EditorTab::Viewport | EditorTab::GameView => self.main_view(), + EditorTab::Hierarchy => self.hierarchy(), + EditorTab::Inspector => self.inspector(), + EditorTab::AssetBrowser | EditorTab::Toolbar => self.bottom(), + } + } + + /// Scan the main surface for open tabs and refresh node indices (handles user drag/split). + pub fn discover(dock: &DockState, fallback: Self) -> Self { + let mut nodes = fallback; + for ((surface, node_idx), tab) in dock.iter_all_tabs() { + if surface != SurfaceIndex::main() { + continue; + } + match tab { + EditorTab::Viewport | EditorTab::GameView => nodes.main_view = node_idx.0, + EditorTab::Hierarchy => nodes.hierarchy = node_idx.0, + EditorTab::Inspector => nodes.inspector = node_idx.0, + EditorTab::AssetBrowser | EditorTab::Toolbar => nodes.bottom = node_idx.0, + } + } + nodes + } +} + +pub const PANEL_TABS: [EditorTab; 4] = [ + EditorTab::Viewport, + EditorTab::Hierarchy, + EditorTab::Inspector, + EditorTab::AssetBrowser, +]; + +pub fn tab_label(tab: &EditorTab) -> &'static str { + match tab { + EditorTab::Viewport | EditorTab::GameView => "Viewport", + EditorTab::Hierarchy => "Hierarchy", + EditorTab::Inspector => "Inspector", + EditorTab::AssetBrowser => "Asset Browser", + EditorTab::Toolbar => "Toolbar (legacy)", + } +} + +pub fn tab_is_open(dock: &DockState, tab: &EditorTab) -> bool { + dock.find_main_surface_tab(tab).is_some() +} + +/// Opens `tab` on the main surface if missing, then focuses it. +pub fn open_and_focus_tab( + dock: &mut DockState, + tab: EditorTab, + panels: &PanelNodes, +) -> (NodeIndex, TabIndex) { + let (node, tab_idx) = if let Some(loc) = dock.find_main_surface_tab(&tab) { + loc + } else { + let tree = dock.main_surface_mut(); + let node = panels.preferred_node(&tab); + let tab_idx = push_tab_to_node(tree, node, tab); + (node, tab_idx) + }; + dock.set_active_tab((SurfaceIndex::main(), node, tab_idx)); + dock.set_focused_node_and_surface((SurfaceIndex::main(), node)); + (node, tab_idx) +} + +pub fn ensure_viewport_for_play(dock: &mut DockState, panels: &PanelNodes) { + open_and_focus_tab(dock, EditorTab::Viewport, panels); +} + +/// Migrates old saved layouts by replacing/removing legacy Game View tabs. +pub fn normalize_unified_viewport_tabs(dock: &mut DockState) { + let has_viewport = dock + .iter_all_tabs() + .any(|(_, tab)| matches!(tab, EditorTab::Viewport)); + + if !has_viewport { + if let Some((_, tab)) = dock + .iter_all_tabs_mut() + .find(|(_, tab)| matches!(tab, EditorTab::GameView)) + { + *tab = EditorTab::Viewport; + } + } + + let mut seen_viewport = false; + dock.retain_tabs(|tab| match tab { + EditorTab::GameView => false, + EditorTab::Viewport => { + if seen_viewport { + false + } else { + seen_viewport = true; + true + } + } + _ => true, + }); + + if !seen_viewport { + dock.main_surface_mut() + .push_to_first_leaf(EditorTab::Viewport); + } +} + +fn push_tab_to_node(tree: &mut Tree, node_index: NodeIndex, tab: EditorTab) -> TabIndex { + match &mut tree[node_index] { + Node::Leaf(leaf) => { + let tab_idx = TabIndex(leaf.tabs.len()); + leaf.tabs.push(tab); + leaf.active = tab_idx; + tab_idx + } + Node::Empty => { + tree[node_index] = Node::leaf(tab); + TabIndex(0) + } + _ => { + let kind = tab.clone(); + tree.push_to_first_leaf(tab); + tree.find_tab(&kind).expect("tab was just pushed").1 + } + } +} diff --git a/crates/editor/src/ui/fonts.rs b/crates/editor/src/ui/fonts.rs new file mode 100644 index 0000000..879a3bd --- /dev/null +++ b/crates/editor/src/ui/fonts.rs @@ -0,0 +1,21 @@ +//! Phosphor icon font registration for the primary egui context. + +use bevy::prelude::*; +use bevy_egui::{EguiContext, PrimaryEguiContext}; + +pub struct EditorFontsPlugin; + +impl Plugin for EditorFontsPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, configure_editor_fonts); + } +} + +fn configure_editor_fonts(mut contexts: Query<&mut EguiContext, Added>) { + let Some(mut ctx) = contexts.iter_mut().next() else { + return; + }; + let mut fonts = bevy_egui::egui::FontDefinitions::default(); + egui_phosphor_icons::add_fonts(&mut fonts); + ctx.get_mut().set_fonts(fonts); +} diff --git a/crates/editor/src/ui/helpers.rs b/crates/editor/src/ui/helpers.rs new file mode 100644 index 0000000..cab9700 --- /dev/null +++ b/crates/editor/src/ui/helpers.rs @@ -0,0 +1,297 @@ +//! Spawn helpers, bookmarks, hierarchy tree, and asset labeling. + +use bevy::prelude::*; +use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities; +use shared::{ + AuthoringLightKind, LevelObject, LightDesc, MaterialDesc, PlayerSpawn, PrimitiveShape, + ProjectSun, +}; +use sim::Player; + +use crate::assets::{snapshot_for_asset, EditorAsset, EditorAssetKind}; +use crate::camera::EditorCamera; +use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace}; +use crate::history::{set_material_with_history, spawn_with_history, EditorEntitySnapshot}; +use crate::play::default_player_spawn; +use crate::scene_io::SceneIo; +use crate::viewport::{recall_camera_bookmark, save_camera_bookmark, CameraBookmarks}; + +pub use super::play_controls::{toggle_play_mode, toggle_play_paused, toggle_possession}; +pub use super::selection_ops::{ + delete_selection, duplicate_selection, entity_name, focus_editor_camera_on_selection, + is_level_object, selected_level_entities, +}; + +pub fn set_gizmo_mode(world: &mut World, mode: EditorGizmoMode) { + *world.resource_mut::() = mode; +} + +pub fn set_gizmo_space(world: &mut World, space: EditorGizmoSpace) { + *world.resource_mut::() = space; +} + +pub fn viewport_save_bookmark(world: &mut World) { + let scene_key = world + .resource::() + .active_path + .as_ref() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "__unsaved__".into()); + let Some(transform) = world + .query_filtered::<&Transform, With>() + .single(world) + .ok() + .copied() + else { + return; + }; + let mut bookmarks = world.resource_mut::(); + save_camera_bookmark(&mut bookmarks, &scene_key, transform); +} + +pub fn viewport_recall_bookmark(world: &mut World) { + let scene_key = world + .resource::() + .active_path + .as_ref() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "__unsaved__".into()); + let saved = { + let bookmarks = world.resource::(); + match recall_camera_bookmark(bookmarks, &scene_key) { + Some(transform) => transform, + None => return, + } + }; + for mut transform in world + .query_filtered::<&mut Transform, With>() + .iter_mut(world) + { + *transform = saved; + } +} + +pub fn primitive_snapshot(name: &str, shape: PrimitiveShape, origin: Vec3) -> EditorEntitySnapshot { + let asset = EditorAsset { + label: name.to_string(), + path: None, + folder_path: crate::assets::BUILTINS_FOLDER.to_string(), + kind: EditorAssetKind::Primitive(shape), + }; + snapshot_for_asset(&asset, origin).expect("primitive assets are spawnable") +} + +pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot { + EditorEntitySnapshot { + actor_id: None, + actor_kind: shared::ActorKind::Light, + actor_name: None, + name: Some(name.to_string()), + transform: Transform::from_translation(translation), + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: Some(LightDesc::default()), + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: 0, + editor_visibility: shared::EditorVisibility::default(), + children: Vec::new(), + } +} + +pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity { + if let Some(entity) = world + .query_filtered::>() + .iter(world) + .next() + { + return entity; + } + + let transform = world + .query_filtered::<&Transform, With>() + .iter(world) + .next() + .copied() + .unwrap_or_else(default_player_spawn); + + spawn_with_history( + world, + EditorEntitySnapshot { + actor_id: None, + actor_kind: shared::ActorKind::PlayerSpawn, + actor_name: None, + name: Some("Player Start".to_string()), + transform, + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: None, + player_spawn: true, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: 0, + editor_visibility: shared::EditorVisibility::default(), + children: Vec::new(), + }, + ) +} + +/// Removes all authored [`LightDesc`] so project sun/ambient drive outdoor lighting. +pub fn reset_scene_lighting_to_project_defaults(world: &mut World) { + let mut query = world.query_filtered::>(); + let with_lights: Vec = query + .iter(world) + .filter(|entity| world.get::(*entity).is_some()) + .collect(); + for entity in with_lights { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.remove::(); + } + } + world + .resource_mut::() + .mark_dirty(); +} + +/// Removes authored directional lights so [`ProjectSun`] drives outdoor lighting. +pub fn use_project_sun(world: &mut World) { + let mut query = world.query_filtered::<(Entity, &LightDesc), With>(); + let directionals: Vec = query + .iter(world) + .filter(|(_, light)| matches!(light.kind, AuthoringLightKind::Directional)) + .map(|(e, _)| e) + .collect(); + for entity in directionals { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.remove::(); + } + } + world + .resource_mut::() + .mark_dirty(); +} + +pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Entity { + let (transform, light) = { + let sun_illuminance = world + .resource::() + .rendering + .sun_illuminance; + let transform = world + .query_filtered::<&Transform, With>() + .iter(world) + .next() + .copied() + .unwrap_or_else(|| { + Transform::from_xyz(0.0, 0.0, 0.0) + .looking_to(Vec3::new(-0.35, -0.85, -0.4), Vec3::Y) + }); + let mut light = LightDesc::for_kind(AuthoringLightKind::Directional); + light.intensity = sun_illuminance; + (transform, light) + }; + + spawn_with_history( + world, + EditorEntitySnapshot { + actor_id: None, + actor_kind: shared::ActorKind::Light, + actor_name: None, + name: Some("Scene Sun".to_string()), + transform, + primitive: None, + static_mesh_renderer: None, + material: None, + material_override: None, + rigid_body: None, + collider: None, + physics: None, + light: Some(light), + player_spawn: false, + model: None, + prefab: None, + prefab_instance: None, + weapon_spawn: None, + trigger_volume: None, + post_process_volume: None, + team_spawn: None, + objective: None, + hierarchy_sibling_index: 0, + editor_visibility: shared::EditorVisibility::default(), + children: Vec::new(), + }, + ) +} + +pub fn asset_label(asset: &EditorAsset) -> String { + let prefix = match asset.kind { + EditorAssetKind::Primitive(_) => "Primitive", + EditorAssetKind::Light(_) => "Light", + EditorAssetKind::Model => "Model", + EditorAssetKind::Texture => "Texture", + EditorAssetKind::Material => "Material", + EditorAssetKind::Level => "Level", + EditorAssetKind::Prefab => "Prefab", + EditorAssetKind::PostProcessVolume => "Post Process Volume", + EditorAssetKind::PostProcessEffect => "Post FX", + EditorAssetKind::RenderingProfile => "Rendering Profile", + EditorAssetKind::ShaderSchema => "Shader Schema", + }; + format!("{prefix}: {}", asset.label) +} + +pub fn apply_texture_to_selection( + world: &mut World, + asset: &EditorAsset, + selected: &SelectedEntities, +) { + let Some(path) = asset.path.clone() else { + return; + }; + for entity in selected_level_entities(world, selected) { + let mut material = world + .get::(entity) + .cloned() + .unwrap_or_default(); + material.base_color_texture = Some(path.clone()); + set_material_with_history(world, entity, material); + } +} + +pub fn apply_material_asset_to_selection( + world: &mut World, + asset: &EditorAsset, + selected: &SelectedEntities, +) { + crate::assets::materials::apply_material_asset_to_selection(world, asset, selected); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HierarchyNodeKind { + Authored, + Runtime, + Generated, +} diff --git a/crates/editor/src/ui/hierarchy.rs b/crates/editor/src/ui/hierarchy.rs new file mode 100644 index 0000000..431f99b --- /dev/null +++ b/crates/editor/src/ui/hierarchy.rs @@ -0,0 +1,718 @@ +//! Hierarchy outliner: tree view, drag-and-drop, visibility, and lock toggles. + +use bevy::prelude::*; +use bevy_egui::egui; +use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities; +use egui::containers::scroll_area::ScrollSource; +use egui_phosphor_icons::icons; +use shared::EditorVisibility; + +use crate::history::{ + create_empty_child_with_history, delete_entities_with_history, duplicate_entities_with_history, + group_selection_with_history, rename_entity_with_history, reorder_siblings_with_history, + reparent_with_history, set_editor_visibility_with_history, +}; +use crate::project_io::UserPreferences; +use crate::selection::SelectedEntity; +use crate::ui::helpers::{ + entity_name, focus_editor_camera_on_selection, is_level_object, HierarchyNodeKind, +}; +use crate::ui::hierarchy_ops::{ + authored_children, editor_visibility, entity_hierarchy_path, entity_matches_filter, + hierarchy_label_for_entity, is_entity_locked, level_object_roots, sort_siblings, + would_create_cycle, +}; +use crate::ui::hierarchy_state::{HierarchyPanelState, HierarchySort}; +use crate::ui::theme::panel_heading; + +#[derive(Clone)] +struct HierarchyDragPayload { + entities: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DropTarget { + Root, + On(Entity), + Before(Entity), +} + +pub fn hierarchy_panel_ui( + world: &mut World, + ui: &mut egui::Ui, + selected_entities: &mut SelectedEntities, + renaming_entity: &mut Option, + rename_buffer: &mut String, +) { + let (filter, sort_mode, show_generated, show_runtime, _expanded, _locked, prefs_paths) = { + let state = world.resource::(); + ( + state.filter.clone(), + state.sort_mode, + state.show_generated, + state.show_runtime, + state.expanded.clone(), + state.locked.clone(), + world + .resource::() + .hierarchy_expanded_paths + .clone(), + ) + }; + + if _expanded.is_empty() && !prefs_paths.is_empty() { + let synced = crate::ui::hierarchy_state::expanded_entities_for_paths(world, &prefs_paths); + world.resource_mut::().expanded = synced; + } + + let expanded = world.resource::().expanded.clone(); + let locked = world.resource::().locked.clone(); + + let mut filter = filter; + ui.horizontal(|ui| { + ui.label(icons::MAGNIFYING_GLASS.regular()); + if ui + .add( + egui::TextEdit::singleline(&mut filter) + .hint_text("Search hierarchy...") + .desired_width(f32::INFINITY), + ) + .changed() + { + world.resource_mut::().filter = filter.clone(); + world.resource_mut::().mark_dirty(); + } + }); + + ui.horizontal(|ui| { + ui.label("Sort:"); + let mut sort = sort_mode; + egui::ComboBox::from_id_salt("hierarchy_sort") + .selected_text(sort_label(sort)) + .show_ui(ui, |ui| { + for candidate in [ + HierarchySort::Manual, + HierarchySort::Name, + HierarchySort::Type, + ] { + if ui + .selectable_value(&mut sort, candidate, sort_label(candidate)) + .clicked() + { + world.resource_mut::().sort_mode = candidate; + world.resource_mut::().mark_dirty(); + } + } + }); + let mut show_gen = show_generated; + if ui.checkbox(&mut show_gen, "Generated").changed() { + world.resource_mut::().show_generated = show_gen; + world.resource_mut::().mark_dirty(); + } + let mut show_rt = show_runtime; + if ui.checkbox(&mut show_rt, "Runtime").changed() { + world.resource_mut::().show_runtime = show_rt; + world.resource_mut::().mark_dirty(); + } + }); + + ui.add_space(4.0); + ui.label(panel_heading("Scene")); + + if ui.input(|input| input.key_pressed(egui::Key::F2)) { + if let Some(entity) = selected_entities.as_slice().first().copied() { + if is_level_object(world, entity) { + *renaming_entity = Some(entity); + *rename_buffer = entity_name(world, entity); + } + } + } + + let filter_lower = filter.trim().to_lowercase(); + let mut drop_target = None; + let mut invalid_drop = false; + + let dragging = world + .resource::() + .active_drag + .is_some(); + + egui::ScrollArea::vertical() + .scroll_source(ScrollSource { + scroll_bar: true, + drag: !dragging, + mouse_wheel: true, + }) + .show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 2.0; + + if dragging { + draw_root_drop_zone(ui, &mut drop_target); + } + + let mut roots = level_object_roots(world); + sort_siblings(world, &mut roots, sort_mode); + + let visible_roots: Vec = roots + .into_iter() + .filter(|entity| subtree_matches_filter(world, *entity, &filter_lower, sort_mode)) + .collect(); + + for entity in visible_roots { + draw_authored_node( + world, + ui, + entity, + 0, + selected_entities, + renaming_entity, + rename_buffer, + &filter_lower, + sort_mode, + show_generated, + dragging, + &expanded, + &locked, + &prefs_paths, + &mut drop_target, + &mut invalid_drop, + ); + } + + if show_runtime { + let runtime_roots = runtime_roots(world); + if !runtime_roots.is_empty() { + ui.add_space(6.0); + ui.small(egui::RichText::new("Runtime").weak()); + for entity in runtime_roots { + draw_runtime_node(world, ui, entity, 0, selected_entities, show_runtime); + } + } + } + }); + + if ui.ctx().input(|input| input.pointer.any_released()) { + if let Some(target) = drop_target { + let drag_entities = world + .resource_mut::() + .active_drag + .take(); + if let Some(entities) = drag_entities { + let payload = HierarchyDragPayload { entities }; + apply_drop(world, &payload, target, invalid_drop); + } + } else { + world.resource_mut::().active_drag = None; + } + } + + if selected_entities.as_slice().first().copied() != world.resource::().0 { + world.resource_mut::().0 = selected_entities.as_slice().first().copied(); + } +} + +fn sort_label(sort: HierarchySort) -> &'static str { + match sort { + HierarchySort::Manual => "Manual", + HierarchySort::Name => "Name", + HierarchySort::Type => "Type", + } +} + +fn draw_root_drop_zone(ui: &mut egui::Ui, drop_target: &mut Option) { + let (rect, response) = + ui.allocate_exact_size(egui::vec2(ui.available_width(), 2.0), egui::Sense::hover()); + if let Some(hover) = response.dnd_hover_payload::() { + ui.painter() + .rect_filled(rect, 1.0, egui::Color32::from_rgb(80, 120, 200)); + *drop_target = Some(DropTarget::Root); + let _ = hover; + } +} + +fn draw_authored_node( + world: &mut World, + ui: &mut egui::Ui, + entity: Entity, + depth: usize, + selected_entities: &mut SelectedEntities, + renaming_entity: &mut Option, + rename_buffer: &mut String, + filter_lower: &str, + sort_mode: HierarchySort, + show_generated: bool, + drag_active: bool, + expanded: &std::collections::HashSet, + locked: &std::collections::HashSet, + prefs_paths: &[String], + drop_target: &mut Option, + invalid_drop: &mut bool, +) { + let path = entity_hierarchy_path(world, entity); + let mut is_expanded = expanded.contains(&entity) || prefs_paths.iter().any(|p| p == &path); + let has_children = authored_children(world, entity) + .iter() + .any(|child| subtree_matches_filter(world, *child, filter_lower, sort_mode)) + || (show_generated + && world.get::(entity).is_some_and(|children| { + children + .iter() + .any(|child| is_hierarchy_generated_child(world, child)) + })); + + draw_drop_before( + world, + ui, + entity, + depth, + drag_active, + drop_target, + invalid_drop, + ); + + ui.horizontal(|ui| { + ui.add_space(depth as f32 * 14.0); + if has_children { + let twistie = if is_expanded { + icons::CARET_DOWN + } else { + icons::CARET_RIGHT + }; + if ui.small_button(twistie.as_str()).clicked() { + let path = path.clone(); + let expanding = !world + .resource::() + .expanded + .contains(&entity); + { + let mut state = world.resource_mut::(); + if expanding { + state.expanded.insert(entity); + } else { + state.expanded.remove(&entity); + } + state.mark_dirty(); + } + let mut prefs = world.resource_mut::(); + if expanding { + if !prefs + .hierarchy_expanded_paths + .iter() + .any(|entry| entry == &path) + { + prefs.hierarchy_expanded_paths.push(path); + } + } else { + prefs + .hierarchy_expanded_paths + .retain(|entry| entry != &path); + } + is_expanded = expanding; + } + } else { + ui.add_space(18.0); + } + + let visible = editor_visibility(world, entity).visible; + let eye_icon = if visible { + icons::EYE.regular() + } else { + icons::EYE_SLASH.regular() + }; + if ui.small_button(eye_icon).clicked() { + set_editor_visibility_with_history( + world, + entity, + EditorVisibility { visible: !visible }, + ); + } + + let is_locked = is_entity_locked(locked, entity); + let lock_icon = if is_locked { + icons::LOCK.regular() + } else { + icons::LOCK_OPEN.regular() + }; + if ui.small_button(lock_icon).clicked() { + let mut state = world.resource_mut::(); + if is_locked { + state.locked.remove(&entity); + } else { + state.locked.insert(entity); + } + } + + if *renaming_entity == Some(entity) { + let response = ui.text_edit_singleline(rename_buffer); + let commit = + response.lost_focus() || ui.input(|input| input.key_pressed(egui::Key::Enter)); + if commit { + rename_entity_with_history(world, entity, rename_buffer.clone()); + *renaming_entity = None; + } + } else { + let selected = selected_entities.contains(entity); + let label = hierarchy_label_for_entity(world, entity, HierarchyNodeKind::Authored); + let response = ui.selectable_label(selected, label); + if response.clicked() && !is_locked { + let additive = ui.input(|input| { + input.modifiers.shift || input.modifiers.command || input.modifiers.ctrl + }); + if additive { + selected_entities.select_maybe_add(entity, true); + } else { + selected_entities.select_replace(entity); + } + world.resource_mut::().0 = Some(entity); + } + if response.double_clicked() && is_level_object(world, entity) { + *renaming_entity = Some(entity); + *rename_buffer = entity_name(world, entity); + } + + if response.drag_started() { + let drag_entities: Vec = if selected_entities.contains(entity) { + selected_entities + .iter() + .filter(|e| is_level_object(world, *e)) + .collect() + } else { + vec![entity] + }; + response.dnd_set_drag_payload(HierarchyDragPayload { + entities: drag_entities.clone(), + }); + world.resource_mut::().active_drag = Some(drag_entities); + } + + if let Some(hover) = response.dnd_hover_payload::() { + let invalid = would_create_cycle(world, &hover.entities, Some(entity)); + *invalid_drop = invalid; + *drop_target = Some(DropTarget::On(entity)); + if invalid { + ui.painter().rect_stroke( + response.rect, + 2.0, + egui::Stroke::new(2.0, egui::Color32::RED), + egui::StrokeKind::Outside, + ); + } + } + + response.context_menu(|ui| { + authored_context_menu( + world, + ui, + entity, + selected_entities, + renaming_entity, + rename_buffer, + ) + }); + } + }); + + if !is_expanded { + return; + } + + let mut children = authored_children(world, entity); + sort_siblings(world, &mut children, sort_mode); + for child in children { + if subtree_matches_filter(world, child, filter_lower, sort_mode) { + draw_authored_node( + world, + ui, + child, + depth + 1, + selected_entities, + renaming_entity, + rename_buffer, + filter_lower, + sort_mode, + show_generated, + drag_active, + expanded, + locked, + prefs_paths, + drop_target, + invalid_drop, + ); + } + } + + if show_generated { + if let Some(children) = world.get::(entity) { + for child in children.iter() { + if is_hierarchy_generated_child(world, child) { + draw_generated_node(world, ui, child, depth + 1, expanded, show_generated); + } + } + } + } +} + +fn draw_drop_before( + world: &World, + ui: &mut egui::Ui, + entity: Entity, + depth: usize, + drag_active: bool, + drop_target: &mut Option, + invalid_drop: &mut bool, +) { + if !drag_active { + return; + } + + ui.horizontal(|ui| { + ui.add_space(depth as f32 * 14.0 + 18.0); + let (rect, response) = + ui.allocate_exact_size(egui::vec2(ui.available_width(), 2.0), egui::Sense::hover()); + if let Some(hover) = response.dnd_hover_payload::() { + *invalid_drop = would_create_cycle( + world, + &hover.entities, + world + .get::(entity) + .map(|child_of| child_of.parent()), + ); + *drop_target = Some(DropTarget::Before(entity)); + let color = if *invalid_drop { + egui::Color32::RED + } else { + egui::Color32::from_rgb(80, 120, 200) + }; + ui.painter().rect_filled(rect, 1.0, color); + let _ = hover; + } + }); +} + +fn draw_generated_node( + world: &World, + ui: &mut egui::Ui, + entity: Entity, + depth: usize, + expanded: &std::collections::HashSet, + show_generated: bool, +) { + let _ = show_generated; + let has_children = world.get::(entity).is_some_and(|children| { + children + .iter() + .any(|child| is_hierarchy_generated_child(world, child)) + }); + let mut is_expanded = expanded.contains(&entity); + ui.horizontal(|ui| { + ui.add_space(depth as f32 * 14.0); + if has_children { + let twistie = if is_expanded { + icons::CARET_DOWN + } else { + icons::CARET_RIGHT + }; + if ui.small_button(twistie.as_str()).clicked() { + is_expanded = !is_expanded; + } + } else { + ui.add_space(18.0); + } + ui.add_enabled( + false, + egui::Label::new( + egui::RichText::new(hierarchy_label_for_entity( + world, + entity, + HierarchyNodeKind::Generated, + )) + .weak(), + ), + ); + }); + if !is_expanded { + return; + } + if let Some(children) = world.get::(entity) { + for child in children.iter() { + if is_hierarchy_generated_child(world, child) { + draw_generated_node(world, ui, child, depth + 1, expanded, show_generated); + } + } + } +} + +fn draw_runtime_node( + world: &World, + ui: &mut egui::Ui, + entity: Entity, + depth: usize, + selected_entities: &mut SelectedEntities, + show_runtime: bool, +) { + let _ = show_runtime; + ui.horizontal(|ui| { + ui.add_space(depth as f32 * 14.0); + ui.add_space(18.0); + let label = hierarchy_label_for_entity(world, entity, HierarchyNodeKind::Runtime); + let response = ui.selectable_label(selected_entities.contains(entity), label); + if response.clicked() { + selected_entities.select_replace(entity); + } + response.context_menu(|ui| { + if ui.button("Focus").clicked() { + selected_entities.select_replace(entity); + ui.close(); + } + }); + }); + if let Some(children) = world.get::(entity) { + for child in children.iter() { + if is_runtime_hierarchy_entity(world, child) { + draw_runtime_node(world, ui, child, depth + 1, selected_entities, show_runtime); + } + } + } +} + +fn authored_context_menu( + world: &mut World, + ui: &mut egui::Ui, + entity: Entity, + selected_entities: &mut SelectedEntities, + renaming_entity: &mut Option, + rename_buffer: &mut String, +) { + if ui.button("Rename").clicked() { + *renaming_entity = Some(entity); + *rename_buffer = entity_name(world, entity); + ui.close(); + } + if ui.button("Duplicate").clicked() { + duplicate_entities_with_history(world, &[entity]); + ui.close(); + } + if ui.button("Delete").clicked() { + delete_entities_with_history(world, &[entity]); + ui.close(); + } + if selected_entities.len() >= 2 && selected_entities.contains(entity) { + if ui.button("Group").clicked() { + let entities: Vec = selected_entities + .iter() + .filter(|e| is_level_object(world, *e)) + .collect(); + group_selection_with_history(world, &entities); + ui.close(); + } + } + if ui.button("Unparent").clicked() { + reparent_with_history(world, entity, None); + ui.close(); + } + if ui.button("Create child Empty").clicked() { + create_empty_child_with_history(world, entity); + ui.close(); + } + if ui.button("Focus").clicked() { + selected_entities.select_replace(entity); + world.resource_mut::().0 = Some(entity); + focus_editor_camera_on_selection(world); + ui.close(); + } +} + +fn apply_drop(world: &mut World, drag: &HierarchyDragPayload, target: DropTarget, invalid: bool) { + if invalid || drag.entities.is_empty() { + return; + } + match target { + DropTarget::Root => { + reorder_siblings_with_history(world, &drag.entities, None, i32::MAX); + } + DropTarget::On(parent) => { + reorder_siblings_with_history(world, &drag.entities, Some(parent), i32::MAX); + } + DropTarget::Before(before) => { + let parent = world + .get::(before) + .map(|child_of| child_of.parent()); + let siblings = match parent { + Some(parent) => authored_children(world, parent), + None => level_object_roots(world), + }; + let insert_index = siblings + .iter() + .position(|entity| *entity == before) + .unwrap_or(siblings.len()) as i32; + reorder_siblings_with_history(world, &drag.entities, parent, insert_index); + } + } +} + +fn subtree_matches_filter( + world: &World, + entity: Entity, + filter_lower: &str, + sort_mode: HierarchySort, +) -> bool { + if filter_lower.is_empty() { + return true; + } + if entity_matches_filter(world, entity, filter_lower) { + return true; + } + let mut children = authored_children(world, entity); + sort_siblings(world, &mut children, sort_mode); + children + .iter() + .any(|child| subtree_matches_filter(world, *child, filter_lower, sort_mode)) +} + +fn runtime_roots(world: &mut World) -> Vec { + use shared::ProjectSun; + use sim::{Player, PlayerCamera}; + + let mut query = world.query_filtered::< + (Entity, Option<&ChildOf>), + Or<(With, With, With)>, + >(); + query + .iter(world) + .filter_map(|(entity, parent)| { + if parent + .map(|parent| is_runtime_hierarchy_entity(world, parent.parent())) + .unwrap_or(false) + { + None + } else { + Some(entity) + } + }) + .collect() +} + +pub(crate) fn is_hierarchy_generated_child(world: &World, entity: Entity) -> bool { + use shared::LevelObject; + world.get_entity(entity).is_ok_and(|entity_ref| { + !entity_ref.contains::() + && !entity_ref.contains::() + && !entity_ref.contains::() + && (entity_ref.contains::() || entity_ref.contains::()) + && (entity_ref.contains::() + || entity_ref.contains::() + || entity_ref.contains::()) + }) +} + +pub(crate) fn is_runtime_hierarchy_entity(world: &World, entity: Entity) -> bool { + use shared::ProjectSun; + use sim::{Player, PlayerCamera}; + + world.get_entity(entity).is_ok_and(|entity_ref| { + !entity_ref.contains::() + && (entity_ref.contains::() + || entity_ref.contains::() + || entity_ref.contains::()) + }) +} diff --git a/crates/editor/src/ui/hierarchy_ops.rs b/crates/editor/src/ui/hierarchy_ops.rs new file mode 100644 index 0000000..86cd84d --- /dev/null +++ b/crates/editor/src/ui/hierarchy_ops.rs @@ -0,0 +1,445 @@ +//! Hierarchy tree building, sibling ordering, and reparent helpers. + +use bevy::prelude::*; +use shared::{ + ActorKind, AuthoringLightKind, EditorVisibility, HierarchySiblingIndex, LevelObject, LightDesc, + ModelRef, PhysicsBody, PlayerSpawn, +}; + +use super::helpers::{entity_name, is_level_object, HierarchyNodeKind}; +use super::hierarchy_state::HierarchySort; +use crate::history::SiblingChange; + +pub fn sibling_index(world: &World, entity: Entity) -> i32 { + world + .get::(entity) + .map(|index| index.0) + .unwrap_or(0) +} + +pub fn is_ancestor(world: &World, ancestor: Entity, descendant: Entity) -> bool { + if ancestor == descendant { + return true; + } + let mut current = world + .get::(descendant) + .map(|child_of| child_of.parent()); + while let Some(entity) = current { + if entity == ancestor { + return true; + } + current = world + .get::(entity) + .map(|child_of| child_of.parent()); + } + false +} + +pub fn would_create_cycle(world: &World, targets: &[Entity], new_parent: Option) -> bool { + let Some(parent) = new_parent else { + return false; + }; + targets + .iter() + .any(|entity| is_ancestor(world, *entity, parent)) +} + +pub fn level_object_roots(world: &mut World) -> Vec { + let mut query = world.query_filtered::<(Entity, Option<&ChildOf>), With>(); + query + .iter(world) + .filter_map(|(entity, parent)| match parent { + Some(parent) if is_level_object(world, parent.parent()) => None, + _ => Some(entity), + }) + .collect() +} + +pub fn authored_children(world: &World, parent: Entity) -> Vec { + world + .get::(parent) + .map(|children| { + children + .iter() + .filter(|child| is_level_object(world, *child)) + .collect() + }) + .unwrap_or_default() +} + +fn entity_tie_break(a: Entity, b: Entity) -> std::cmp::Ordering { + a.to_bits().cmp(&b.to_bits()) +} + +pub fn sort_siblings(world: &World, entities: &mut [Entity], sort_mode: HierarchySort) { + match sort_mode { + HierarchySort::Manual => { + entities.sort_by(|a, b| { + sibling_index(world, *a) + .cmp(&sibling_index(world, *b)) + .then_with(|| entity_tie_break(*a, *b)) + }); + } + HierarchySort::Name => { + entities.sort_by(|a, b| { + entity_name(world, *a) + .cmp(&entity_name(world, *b)) + .then_with(|| sibling_index(world, *a).cmp(&sibling_index(world, *b))) + .then_with(|| entity_tie_break(*a, *b)) + }); + } + HierarchySort::Type => { + entities.sort_by(|a, b| { + actor_kind_sort_key(world, *a) + .cmp(&actor_kind_sort_key(world, *b)) + .then_with(|| entity_name(world, *a).cmp(&entity_name(world, *b))) + .then_with(|| sibling_index(world, *a).cmp(&sibling_index(world, *b))) + .then_with(|| entity_tie_break(*a, *b)) + }); + } + } +} + +fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 { + world + .get::(entity) + .copied() + .map(|kind| match kind { + ActorKind::Empty => 0, + ActorKind::StaticMesh | ActorKind::ImportedModel => 1, + ActorKind::Light => 2, + ActorKind::PrefabAnchor => 3, + ActorKind::PlayerSpawn => 4, + ActorKind::WeaponSpawn => 5, + ActorKind::TriggerVolume => 6, + ActorKind::PostProcessVolume => 6, + ActorKind::TeamSpawn => 7, + ActorKind::Objective => 8, + }) + .unwrap_or(9) +} + +pub fn entity_hierarchy_path(world: &World, entity: Entity) -> String { + let mut parts = vec![entity_name(world, entity)]; + let mut current = world + .get::(entity) + .map(|child_of| child_of.parent()); + while let Some(parent) = current { + if !is_level_object(world, parent) { + break; + } + parts.push(entity_name(world, parent)); + current = world + .get::(parent) + .map(|child_of| child_of.parent()); + } + parts.reverse(); + parts.join("/") +} + +pub fn next_sibling_index(world: &mut World, parent: Option) -> i32 { + let siblings = match parent { + Some(parent) => authored_children(world, parent), + None => level_object_roots(world), + }; + siblings + .iter() + .map(|entity| sibling_index(world, *entity)) + .max() + .map(|max| max + 1) + .unwrap_or(0) +} + +pub fn renumber_siblings(world: &mut World, parent: Option, ordered: &[Entity]) { + for (index, entity) in ordered.iter().enumerate() { + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + entity_mut.insert(HierarchySiblingIndex(index as i32)); + } + } + let _ = parent; +} + +pub fn capture_sibling_state(world: &World, entity: Entity) -> SiblingChange { + SiblingChange { + entity, + old_parent: world + .get::(entity) + .map(|child_of| child_of.parent()), + new_parent: world + .get::(entity) + .map(|child_of| child_of.parent()), + old_index: sibling_index(world, entity), + new_index: sibling_index(world, entity), + } +} + +pub fn apply_sibling_change(world: &mut World, change: &SiblingChange) { + if let Ok(mut entity_mut) = world.get_entity_mut(change.entity) { + if let Some(parent) = change.old_parent { + entity_mut.insert(ChildOf(parent)); + } else { + entity_mut.remove::(); + } + entity_mut.insert(HierarchySiblingIndex(change.old_index)); + } +} + +pub fn apply_sibling_change_new(world: &mut World, change: &SiblingChange) { + if let Ok(mut entity_mut) = world.get_entity_mut(change.entity) { + if let Some(parent) = change.new_parent { + entity_mut.insert(ChildOf(parent)); + } else { + entity_mut.remove::(); + } + entity_mut.insert(HierarchySiblingIndex(change.new_index)); + } +} + +pub fn reorder_entities_under_parent( + world: &mut World, + moving: &[Entity], + new_parent: Option, + insert_index: i32, +) -> Vec { + let mut changes = Vec::new(); + let old_parents: std::collections::HashSet> = moving + .iter() + .map(|entity| { + world + .get::(*entity) + .map(|child_of| child_of.parent()) + }) + .collect(); + + changes.extend(renumber_inserted_siblings( + world, + moving, + new_parent, + insert_index, + )); + + for old_parent in old_parents { + if old_parent == new_parent { + continue; + } + changes.extend(renumber_existing_siblings(world, old_parent, moving)); + } + + changes +} + +fn renumber_inserted_siblings( + world: &mut World, + moving: &[Entity], + new_parent: Option, + insert_index: i32, +) -> Vec { + let mut siblings = match new_parent { + Some(parent) => authored_children(world, parent), + None => level_object_roots(world), + }; + siblings.retain(|entity| !moving.contains(entity)); + let insert = insert_index.clamp(0, siblings.len() as i32) as usize; + for (offset, entity) in moving.iter().enumerate() { + siblings.insert(insert + offset, *entity); + } + apply_sibling_order(world, new_parent, &siblings) +} + +fn renumber_existing_siblings( + world: &mut World, + parent: Option, + exclude: &[Entity], +) -> Vec { + let mut siblings = match parent { + Some(parent) => authored_children(world, parent), + None => level_object_roots(world), + }; + siblings.retain(|entity| !exclude.contains(entity)); + sort_siblings(world, &mut siblings, HierarchySort::Manual); + apply_sibling_order(world, parent, &siblings) +} + +fn apply_sibling_order( + world: &mut World, + parent: Option, + siblings: &[Entity], +) -> Vec { + let mut changes = Vec::new(); + for (index, entity) in siblings.iter().enumerate() { + let old_parent = world + .get::(*entity) + .map(|child_of| child_of.parent()); + let old_index = sibling_index(world, *entity); + let new_index = index as i32; + if old_parent != parent || old_index != new_index { + changes.push(SiblingChange { + entity: *entity, + old_parent, + new_parent: parent, + old_index, + new_index, + }); + } + if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { + if let Some(parent) = parent { + entity_mut.insert(ChildOf(parent)); + } else { + entity_mut.remove::(); + } + entity_mut.insert(HierarchySiblingIndex(new_index)); + } + } + changes +} + +pub fn backfill_missing_editor_visibility(world: &mut World) { + let mut query = + world.query_filtered::, Without)>(); + let entities: Vec = query.iter(world).collect(); + for entity in entities { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert(EditorVisibility::default()); + } + } +} + +pub fn backfill_missing_sibling_indices(world: &mut World) { + let mut roots = level_object_roots(world); + if roots + .iter() + .any(|entity| world.get::(*entity).is_none()) + { + sort_siblings(world, &mut roots, HierarchySort::Manual); + renumber_siblings(world, None, &roots); + } + for root in roots { + backfill_children(world, root); + } +} + +fn backfill_children(world: &mut World, parent: Entity) { + let mut children = authored_children(world, parent); + if children + .iter() + .any(|entity| world.get::(*entity).is_none()) + { + sort_siblings(world, &mut children, HierarchySort::Manual); + renumber_siblings(world, Some(parent), &children); + } + for child in children { + backfill_children(world, child); + } +} + +pub fn actor_kind_icon(kind: ActorKind) -> &'static str { + use egui_phosphor_icons::icons; + match kind { + ActorKind::Empty => icons::FOLDER.as_str(), + ActorKind::StaticMesh => icons::CUBE.as_str(), + ActorKind::ImportedModel => icons::CUBE_TRANSPARENT.as_str(), + ActorKind::Light => icons::LIGHTBULB.as_str(), + ActorKind::PrefabAnchor => icons::PACKAGE.as_str(), + ActorKind::PlayerSpawn => icons::USER_CIRCLE.as_str(), + ActorKind::WeaponSpawn => icons::CROSSHAIR.as_str(), + ActorKind::TriggerVolume => icons::BROADCAST.as_str(), + ActorKind::PostProcessVolume => icons::CAMERA.as_str(), + ActorKind::TeamSpawn => icons::FLAG.as_str(), + ActorKind::Objective => icons::TARGET.as_str(), + } +} + +pub fn hierarchy_label_for_entity( + world: &World, + entity: Entity, + kind: HierarchyNodeKind, +) -> String { + use egui_phosphor_icons::icons; + let icon = world + .get::(entity) + .copied() + .map(actor_kind_icon) + .unwrap_or_else(|| match kind { + HierarchyNodeKind::Authored => icons::CUBE.as_str(), + HierarchyNodeKind::Generated => icons::STACK.as_str(), + HierarchyNodeKind::Runtime => icons::CPU.as_str(), + }); + let mut label = format!("{} {}", icon, entity_name(world, entity)); + if let Some(light) = world.get::(entity) { + if matches!(light.kind, AuthoringLightKind::Directional) { + label.push_str(" ☀"); + } + } + match kind { + HierarchyNodeKind::Authored => {} + HierarchyNodeKind::Generated => label.push_str(" (generated)"), + HierarchyNodeKind::Runtime => label.push_str(" (runtime)"), + } + label +} + +pub fn entity_matches_filter(world: &World, entity: Entity, filter_lower: &str) -> bool { + if filter_lower.is_empty() { + return true; + } + if entity_name(world, entity) + .to_lowercase() + .contains(filter_lower) + { + return true; + } + if let Some(kind) = world.get::(entity) { + if format!("{kind:?}").to_lowercase().contains(filter_lower) { + return true; + } + } + let tags = [ + world.get::(entity).is_some().then_some("light"), + world + .get::(entity) + .is_some() + .then_some("spawn"), + world.get::(entity).is_some().then_some("model"), + world + .get::(entity) + .is_some() + .then_some("physics"), + world + .get::(entity) + .is_some() + .then_some("postprocess volume"), + ]; + tags.into_iter() + .flatten() + .any(|tag| tag.contains(filter_lower)) +} + +pub fn editor_visibility(world: &World, entity: Entity) -> EditorVisibility { + world + .get::(entity) + .copied() + .unwrap_or_default() +} + +pub fn is_entity_locked(locked: &std::collections::HashSet, entity: Entity) -> bool { + locked.contains(&entity) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sort_by_name_is_stable_for_duplicate_names() { + let mut world = World::new(); + let a = world.spawn((LevelObject, Name::new("Pillar"))).id(); + let b = world.spawn((LevelObject, Name::new("Pillar"))).id(); + let mut entities = vec![a, b]; + + sort_siblings(&world, &mut entities, HierarchySort::Name); + let first = entities.clone(); + + sort_siblings(&world, &mut entities, HierarchySort::Name); + assert_eq!(entities, first, "duplicate-name sort must be deterministic"); + } +} diff --git a/crates/editor/src/ui/hierarchy_state.rs b/crates/editor/src/ui/hierarchy_state.rs new file mode 100644 index 0000000..f21896f --- /dev/null +++ b/crates/editor/src/ui/hierarchy_state.rs @@ -0,0 +1,142 @@ +//! Hierarchy panel UI state and user-preference persistence. + +use std::collections::HashSet; + +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::project_io::{write_user_preferences, UserPreferences}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum HierarchySort { + #[default] + Manual, + Name, + Type, +} + +#[derive(Resource, Debug)] +pub struct HierarchyPanelState { + pub expanded: HashSet, + pub filter: String, + pub show_generated: bool, + pub show_runtime: bool, + pub sort_mode: HierarchySort, + /// Editor-session lock: prevents selection and gizmo manipulation. + pub locked: HashSet, + /// Active hierarchy drag (release payload for egui 0.33+). + pub active_drag: Option>, + save_timer: f32, + dirty: bool, +} + +impl Default for HierarchyPanelState { + fn default() -> Self { + Self { + expanded: HashSet::new(), + filter: String::new(), + show_generated: true, + show_runtime: false, + sort_mode: HierarchySort::default(), + locked: HashSet::new(), + active_drag: None, + save_timer: 0.0, + dirty: false, + } + } +} + +impl HierarchyPanelState { + pub fn from_prefs(prefs: &UserPreferences) -> Self { + let mut state = Self::default(); + state.filter = prefs.hierarchy_filter.clone(); + state.show_generated = prefs.hierarchy_show_generated; + state.show_runtime = prefs.hierarchy_show_runtime; + state.sort_mode = prefs.hierarchy_sort_mode; + state + } + + pub fn mark_dirty(&mut self) { + self.dirty = true; + self.save_timer = 1.0; + } + + pub fn toggle_expanded(&mut self, entity: Entity, path: &str, prefs: &mut UserPreferences) { + if self.expanded.contains(&entity) { + self.expanded.remove(&entity); + prefs.hierarchy_expanded_paths.retain(|entry| entry != path); + } else { + self.expanded.insert(entity); + if !prefs + .hierarchy_expanded_paths + .iter() + .any(|entry| entry == path) + { + prefs.hierarchy_expanded_paths.push(path.to_string()); + } + } + self.mark_dirty(); + } + + pub fn sync_expanded_from_paths(&mut self, world: &mut World, paths: &[String]) { + self.expanded = expanded_entities_for_paths(world, paths); + } + + fn flush_prefs(&mut self, prefs: &mut UserPreferences) { + prefs.hierarchy_filter = self.filter.clone(); + prefs.hierarchy_show_generated = self.show_generated; + prefs.hierarchy_show_runtime = self.show_runtime; + prefs.hierarchy_sort_mode = self.sort_mode; + } +} + +pub fn expanded_entities_for_paths(world: &mut World, paths: &[String]) -> HashSet { + use super::hierarchy_ops::entity_hierarchy_path; + + let mut expanded = HashSet::new(); + if paths.is_empty() { + return expanded; + } + let mut query = world.query_filtered::>(); + for entity in query.iter(world) { + let path = entity_hierarchy_path(world, entity); + if paths.iter().any(|entry| entry == &path) { + expanded.insert(entity); + } + } + expanded +} + +pub fn tick_hierarchy_prefs_save( + mut state: ResMut, + mut prefs: ResMut, + time: Res