[
  {
    "path": ".cargo/config.toml",
    "content": "[alias]\nexample2d = \"run --example example2d --features _example2d_full\"\nexample3d = \"run --example example3d --features _example3d_full\"\ncustom_camera3d = \"run --example custom_camera3d --features _example3d_full\"\ndoors_to_other_levels = \"run --example doors_to_other_levels --features _doors_to_other_levels_full\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\r\non:\r\n  pull_request:\r\n  push:\r\n    branches: [main]\r\n\r\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\r\npermissions:\r\n  contents: read\r\n  pages: write\r\n  id-token: write\r\n  checks: write\r\n\r\njobs:\r\n  ci:\r\n    name: CI\r\n    needs: [test, clippy, docs]\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Done\r\n        run: exit 0\r\n  test:\r\n    name: Tests\r\n    strategy:\r\n      fail-fast: false\r\n      matrix:\r\n        os: [ubuntu-latest]\r\n        rust: [1.92.0, nightly]\r\n    runs-on: ${{ matrix.os }}\r\n    steps:\r\n      - uses: actions/checkout@v3\r\n      - name: Install rust\r\n        uses: dtolnay/rust-toolchain@master\r\n        with:\r\n          toolchain: ${{ matrix.rust }}\r\n      - name: Ready cache\r\n        if: matrix.os == 'ubuntu-latest'\r\n        run: sudo chown -R $(whoami):$(id -ng) ~/.cargo/\r\n      - name: Install dependencies\r\n        run: sudo apt-get update; sudo apt-get install --no-install-recommends libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libudev-dev\r\n      - name: Cache cargo\r\n        uses: actions/cache@v4\r\n        id: cache\r\n        with:\r\n          path: ~/.cargo\r\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\r\n      - name: Test\r\n        run: cargo test --verbose --all-features --features bevy/bevy_gltf -- --nocapture\r\n  fmt:\r\n   name: Rustfmt\r\n   runs-on: ubuntu-latest\r\n   steps:\r\n     - uses: actions/checkout@v3\r\n     - uses: dtolnay/rust-toolchain@master\r\n       with:\r\n         toolchain: 1.92.0\r\n         components: rustfmt\r\n     - name: Run fmt --all -- --check\r\n       run: cargo fmt --all -- --check\r\n\r\n  clippy:\r\n    name: Clippy\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - uses: actions/checkout@v3\r\n      - uses: dtolnay/rust-toolchain@master\r\n        with:\r\n          toolchain: 1.92.0\r\n          components: clippy\r\n      - name: Install dependencies\r\n        run: sudo apt-get update; sudo apt-get install --no-install-recommends libudev-dev\r\n      - name: Cache cargo\r\n        uses: actions/cache@v4\r\n        id: cache\r\n        with:\r\n          path: ~/.cargo\r\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\r\n      - name: Run clippy --all-targets --\r\n        uses: actions-rs/clippy-check@v1\r\n        with:\r\n          token: ${{ secrets.GITHUB_TOKEN }}\r\n          args: --all-targets --\r\n  docs:\r\n    name: Docs\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - uses: actions/checkout@v3\r\n      - uses: dtolnay/rust-toolchain@master\r\n        with:\r\n          toolchain: 1.92.0\r\n      - name: Install dependencies\r\n        run: sudo apt-get update; sudo apt-get install --no-install-recommends libudev-dev\r\n      - name: Cache cargo\r\n        uses: actions/cache@v4\r\n        id: cache\r\n        with:\r\n          path: ~/.cargo\r\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\r\n      - name: Run doc tests\r\n        run: cargo test --doc --all-features --features bevy/bevy_gltf\r\n      - name: Check docs\r\n        run: cargo doc --no-deps --all-features --features bevy/x11\r\n  docs-and-demos-ghpages:\r\n    name: Update Docs and Demos in GitHub Pages\r\n    runs-on: ubuntu-latest\r\n    if: github.ref == 'refs/heads/main'\r\n    steps:\r\n      - uses: actions/checkout@v3\r\n      - uses: jetli/wasm-bindgen-action@v0.2.0\r\n        with:\r\n          version: 'latest'\r\n      - uses: dtolnay/rust-toolchain@master\r\n        with:\r\n          targets: wasm32-unknown-unknown\r\n          toolchain: 1.92.0\r\n      - name: Build docs\r\n        env:\r\n          GITHUB_REPO: ${{ github.repository }}\r\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n        run: |-\r\n          cargo doc --no-deps --verbose --all-features --features bevy/webgl2,bevy/x11 &&\r\n          echo \"<meta http-equiv=refresh content=0;url=bevy_yoleck/index.html>\" > target/doc/index.html\r\n          required_features=$(\r\n            (\r\n              cargo metadata --no-deps --format-version 1 \\\r\n                  | jq '.packages[].targets[] | select(.kind == [\"example\"]) | .[\"required-features\"][]' -r\r\n              echo bevy/webgl2\r\n              echo bevy/x11\r\n            ) | tr '\\n' ' '\r\n          )\r\n          RUSTFLAGS='--cfg getrandom_backend=\"wasm_js\"' cargo build --examples --release --features \"$required_features\" --target wasm32-unknown-unknown\r\n          for demowasm in $(cd target/wasm32-unknown-unknown/release/examples; ls *.wasm | grep -v -); do\r\n              wasm-bindgen target/wasm32-unknown-unknown/release/examples/$demowasm --out-dir target/doc/demos/ --target web\r\n              cat > target/doc/demos/${demowasm%.*}.html <<EOF\r\n          <html lang=\"en-us\">\r\n              <head>\r\n                  <script type=\"module\">\r\n                      import init from './${demowasm%.*}.js';\r\n                      var res = await init();\r\n                      res.start();\r\n                  </script>\r\n              </head>\r\n              <body>\r\n                  <script>\r\n                      document.body.addEventListener(\"contextmenu\", (e) => {\r\n                          e.preventDefault();\r\n                          e.stopPropagation();\r\n                      });\r\n                  </script>\r\n              </body>\r\n          </html>\r\n          EOF\r\n          done\r\n          cp -R assets/ target/doc/demos/\r\n      - name: Add read permissions\r\n        run: |-\r\n          chmod --recursive +r target/doc\r\n      - name: Upload artifact\r\n        uses: actions/upload-pages-artifact@v3\r\n        with:\r\n          path: target/doc\r\n  deploy-ghpages:\r\n    environment:\r\n      name: github-pages\r\n      url: ${{ steps.deployment.outputs.page_url }}\r\n    runs-on: ubuntu-latest\r\n    needs: docs-and-demos-ghpages\r\n    if: github.ref == 'refs/heads/main'\r\n    steps:\r\n      - name: Deploy to GitHub Pages\r\n        id: deployment\r\n        uses: actions/deploy-pages@v4\r\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\nCargo.lock\n\nassets/levels2d/*.yol\nassets/levels2d/*.yoli\n!assets/levels2d/example.yol\n\nassets/levels3d/*.yol\nassets/levels3d/*.yoli\n!assets/levels3d/example.yol\n\n#IDES\n/.vscode\n\n# No ignored level files in levels_doors\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## 0.31.0 - 2026-01-15\n### Changed\n- Upgrade Bevy to 0.18\n\n## 0.30.0 - 2026-01-15\n### Added\n- Automatic UI generation for components using reflection and attributes.\n  - Supported numeric, boolean, string, vector, color, enum, option, list,\n    asset, and entity fields.\n- `YoleckEntityRef` type with automatic UI, filtering, and runtime UUID\n  resolution.\n  - Drag and drop support for entity references: entities with UUID can now be\n    dragged from the entity list and dropped onto `YoleckEntityRef` fields in\n    the properties panel.\n  - Entity type filtering is automatically applied when dropping entities onto\n    entity reference fields with type constraints.\n- `console_layer_factory` for routing Bevy logs into the Yoleck console.\n- `YoleckConsoleLogHistory` for storing up to 1000 recent log entries.\n- `YoleckConsoleState` for managing console UI state.\n- `YoleckEditorBottomPanelSections` for extensible bottom-panel tabs.\n- `Vpeol3dCameraMode` enum with variants: `Fps`, `Sidescroller`, `Topdown`,\n  `Custom(u32)`.\n- `YoleckCameraChoices` resource for customizing available camera modes in 3D\n  editor.\n- Support for custom camera modes with user-defined movement logic.\n  - Camera mode selector dropdown in editor top panel for switching between\n    camera modes.\n  - `Vpeol3dCameraControl::fps()` preset for FPS-style camera with full\n    rotation freedom.\n- Scene gizmo for camera orientation.\n- Translation gizmo for `vpeol_3d`.\n  - For all 3 world axes (X, Y, Z) by default.\n  - World/Local mode toggle for the translation gizmo in 3D editor top panel.\n- Keyboard shortcut to delete selected entities: Press `Delete` key to remove\n  selected entity from the level.\n- Copy/paste support for entities: Use `Ctrl+C` to copy and `Ctrl+V` to paste\n  entities with all their components and values.\n  - Cross-editor entity copying: Entities can be copied between different level\n    editors through system clipboard (opt in via the `arboard` feature)\n  - If `arboard` is not enabled the copy/paste will not be cross-editor\n- Entity deletion via keyboard in addition to the existing UI button.\n- Auto-selection of pasted entities: Newly pasted entities are automatically\n  selected for immediate editing.\n- UI editing support for `Vpeol3dRotation` using Euler angles (X, Y, Z in\n  degrees)\n- UI editing support for `Vpeol3dScale` with separate X, Y, Z drag values.\n- UI editing support for `Vpeol2dRotatation` using degrees.\n- UI editing support for `Vpeol2dScale` with separate X, Y drag values.\n- `Vpeol3dSnapToPlane` to force an entity on a specific plane.\n\n### Changed\n- Improved editor ergonomics with better organized workspace instead of single\n  cluttered panel.\n- Move camera with keyboard (WASD, with Q and E too for FPS camera) insted of\n  mouse dragging.\n- Use registered systems instead of `YoleckEditorSection`.\n- `YoleckRawEntry::data` is now a `Map<String, Value>` instead of a `Value`.\n- Upgrade Rust edition to 2024.\n- Update bevy_egui version to 0.38.\n\n### Removed\n- [**BREAKING**] Removed `Vpeol3dThirdAxisWithKnob` component (no longer needed\n  as all axes have knobs by default)\n- Removed `plane_origin` field from `Vpeol3dCameraControl` (it was used for\n  movement by mouse-drag, which was dropped)\n- Removed `YoleckEditorSection`.\n\n## 0.29.0 - 2025-10-03\n### Changed\n- Upgrade Bevy to 0.17\n- Rename:\n  - `YoleckSystemSet` -> `YoleckSystems`\n  - `VpeolSystemSet` -> `VpeolSystems`\n\n## 0.28.0 - 2025-08-05\n### Changed\n- Update bevy_egui version to 0.36.\n\n## 0.27.0 - 2025-07-03\n### Changed\n- Update bevy_egui version to 0.35.\n\n## 0.26.1 - 2025-06-04\n### Fixed\n- Don't fail when adding `VpeolRouteClickTo` to a non-existing child.\n\n## 0.26.0 - 2025-04-26\n### Changed\n- Upgrade Bevy to 0.16\n- [**BREAKING**] Rename `YoleckEdit`'s method `get_single` and `get_single_mut`\n  to `single` and `single_mut` (to mirror a similar change in Bevy itself)\n- Replace anyhow usage with `BevyError`.\n\n## 0.25.0 - 2025-02-19\n### Changed\n- Update bevy_egui version to 0.33.\n\n## 0.24.0 - 2025-01-09\n### Changed\n- Update bevy_egui version to 0.32.\n\n## 0.23.0 - 2024-12-02\n### Changed\n- Update Bevy version to 0.15 and bevy_egui version to 0.31.\n\n## 0.22.0 - 2024-07-06\n### Changed\n- Upgrade Bevy to 0.14 (and bevy_egui to 0.28)\n\n## 0.21.0 - 2024-05-08\n### Changed\n- Update bevy_egui version to 0.27.\n\n## 0.20.1 - 2024-04-01\n### Fixed\n- Enable bevy_egui's `render` feature, so that users won't need to load it\n  explicitly and can use the one reexported from Yoleck (fixes\n  https://github.com/idanarye/bevy-yoleck/issues/39)\n\n## 0.20.0 - 2024-03-19\n### Changed\n- Update bevy_egui version to 0.26.\n\n## 0.19.0 - 2024-02-27\n### Changed\n- Update Bevy version to 0.13 and bevy_egui version to 0.25.\n- [**BREAKING**] Changed some API types to use Bevy's new math types. See the\n  [migration guide](MIGRATION-GUIDES.md#migrating-to-yoleck-019).\n\n## 0.18.0 - 2024-02-18\n### Changed\n- Upgrade bevy_egui to 0.24.\n\n### Fixed\n- [**BREAKING**] Typo - `Rotatation` -> `Rotation` in Vpeol.\n\n## 0.17.1 - 2024-01-14\n### Fixed\n- Use a proper `OR` syntax for the dual license.\n\n## 0.17.0 - 2023-11-25\n### Removed\n- [**BREAKING**] The `YoleckLoadingCommand` resource is removed, in favor of a\n  `YoleckLoadLevel` component. See the [migration\n  guide](MIGRATION-GUIDES.md#migrating-to-yoleck-017).\n  - Note that unlike `YoleckLoadingCommand` that could load the level from\n    either an asset or a value, `YoleckLoadLevel` can only load from an asset.\n    If it is necessary to load a level from memory, add it to\n    `ResMut<Assets<YoleckRawLevel>>` first and pass the handle to\n    `YoleckLoadLevel`.\n- [**BREAKING**] The `yoleck_populate_schedule_mut` method (which Yoleck was\n  adding as an extension on Bevy's `App`) is removed in favor of just using\n  `YoleckSchedule::Populate` directly.\n\n### Added\n- `YoleckEditableLevels` resource (accessible only from edit systems) that\n  provides the list of level file names.\n- Entity reference with `YoleckEntityUuid` and `YoleckUuidRegistry`.\n  - Some picking helpers for handling entity references in the editor:\n    `vpeol_read_click_on_entity`, `yoleck_map_entity_to_uuid` and\n    `yoleck_exclusive_system_cancellable`.\n- Load multiple levels with `YoleckLoadLevel` (which is a component, that can\n  be placed on multiple entities)\n- Unload levels by removing the `YoleckKeepLevel` component from the entity\n  that was used to load the level - or by despawning that entity entirely.\n- `YoleckSchedule::LevelLoaded` schedule for interfering with levels before\n  populating their entities.\n- `VpeolRepositionLevel` component.\n\n### Change\n- `YoleckBelongsToLevel` now points to a level entity.\n- `YoleckDirective::spawn_entity` needs to know which level entity to create\n  the component on.\n\n## 0.16.0 - 2023-09-06\n### Changed\n- Upgrade Bevy to 0.12 (and bevy_egui to 0.23)\n\n## 0.15.0 - 2023-10-15\n### Changed\n- Upgrade bevy_egui to 0.22\n- [**BREAKING**] `YoleckUi` is a regular resource again. See the [migration\n  guide](MIGRATION-GUIDES.md#migrating-to-yoleck-013).\n\n## 0.14.1 - 2023-07-30\n### Fixed\n- `Vpeol2dCameraControl` reversing the Y axis when panning and zooming.\n\n  Note that the implementation of this fix requires that the camera entity will\n  have the `VpeolCameraState` component - without it `Vpeol2dCameraControl`\n  will not work at all. I do not consider it a breaking change though, because:\n  1. The documentation do imply that you need `VpeolCameraState`.\n  2. `Vpeol3dCameraControl` was already requiring `VpeolCameraState`, and they\n     are supposed to be equivalent.\n  3. `vpeol_2d` is useless without `VpeolCameraState`, so I'm not expecting\n     anyone to not be using it just so that they can use the camera controls.\n\n  So having `Vpeol2dCameraControl` work without `VpeolCameraState` was an\n  undocumented feature, and I'm going to release this as a bugfix version, not\n  a minor version.\n\n## 0.14.0 - 2023-07-18\n### Changed\n### Added\n- `#[derive(Reflect)]` to several components. Requires the `bevy_reflect` flag.\n\n- [**BREAKING**] Rename `YoleckRouteClickTo` to `VpeolRouteClickTo`.\n\n## 0.13.0 - 2023-07-11\n### Changed\n- Upgrade Bevy to 0.11 (and bevy_egui to 0.21)\n- [**BREAKING**] `YoleckUi` is now a non-`Send` resource. See the [migration\n  guide](MIGRATION-GUIDES.md#migrating-to-yoleck-013).\n\n## 0.12.0 - 2023-06-20\n### Added\n- An exclusive systems mechanism for edit systems that operate alone and can\n  thus assume control over the input (e.g. mouse motion and clicks)\n- Multiple selection with the Shift key, and `YoleckEdit` methods for editing\n  multiple entities.\n\n### Changed\n- When creating a new entity that uses `Vpeol*dPosition`, an exclusive system\n  will kick in to allow placing the entity with the mouse (instead of just\n  placing it in the origin and letting the user drag it from there)\n\n## 0.11.0 - 2023-04-06\n### Added\n- `YoleckBelongsToLevel` for deciding which entities to despawn when the level\n  unloads/restarts. This is added automatically by Yoleck, but should also be\n  added to entities created by the game.\n\n## 0.10.0 - 2023-03-28\n### Changed\n- Model detection now raycasts against the meshes in addition to the AABB.\n\n### Added\n- Supported for 2D meshes in vpeol_2d.\n\n## 0.9.0 - 2023-03-27\n### Changed\n- [**BREAKING**] This entire release is a huge breaking change. See the\n  [migration guide](MIGRATION-GUIDES.md#migrating-to-yoleck-09).\n- [**BREAKING**] Move to a new model, where each Yoleck entity can be composed\n  of multiple `YoleckComponent`s.\n- [**BREAKING**] The syntax of edit systems and populate systems has\n  drastically changed.\n\n### Added\n- A mechanism for upgrading entity's data when their layout changes. See\n  `YoleckEntityUpgradingPlugin`. This can be used to upgrade old games to use\n  the new semantics introduced in this version.\n- `vpeol_3d` is back in, without the dependencies and with better dragging.\n- `yoleck::prelude`\n- `yoleck::vpeol::prelude`\n\n### Removed\n- `vpeol_position_edit_adapter` and `VpeolTransform2dProjection`. Use `Vpeol2dPosition` instead.\n\n## 0.8.0 - 2023-03-14\n### Changed\n- Add scroll area to editor window.\n\n### Fixed\n- Panic that happens sometimes when dragging an entity with children.\n\n### Added\n- `YoleckDirective::spawn_entity` for spawning entities from user code (e.g.\n  for creating entity duplication buttons)\n\n## 0.7.0 - 2023-03-09\n### Changed\n- Upgrade Bevy to 0.10 (and bevy_egui to 0.20)\n- [**BREAKING**] `VpeolSystemLabel` becomes `VpeolSystems`, and uses Bevy's\n  new system set semantics instead of the removed system label semantics. All\n  sets of that system are configured to run during the `EditorActive` state.\n\n### Added\n- `Anchor` is taken into account when vpeol_2d checks clicks on text (previous to\n  Bevy 0.10 it did not have an `Anchor` component, and just used top-left)\n\n## 0.6.0 - 2023-03-06\n### Changed\n- [**BREAKING**] Vpeol names no longer container the \"yoleck\" prefix - so\n  `YoleckVpeolXYZ` becomes `VpeolXYZ` and `yoleck_vpeol_xyz` becomes\n  `vpeol_xyz`. Vpeol is enough to avoid conflicts.\n- [**BREAKING**] `vpeol_2d` sends drag coordinates as `Vec3`, not `Vec2`.\n- [**BREAKING**] `YoleckWillContainClickableChildren` is renamed to\n  `VpeolWillContainClickableChildren` and is no longer reexported by\n  `vpeol_2d`.\n\n### Added\n- [**BREAKING**] `VpeolCameraState` - must be placed on a camera in order for\n  vpoel to work.\n- [**BREAKING**] `Vpeol2dCameraControl` - must be placed on a camera in order\n  for vpoel_2d to apply camera panning and scrolling.\n\n## 0.5.0 - 2023-02-22\n### Changed\n- Update bevy-egui version to 0.19.\n\n## 0.4.0 - 2022-11-14\n### Changed\n- Update Bevy version to 0.9 and bevy-egui version to 0.17.\n\n### Added\n- Ability to revert levels to their initial state:\n  - `Wipe Level` button for ne` levels.\n  - `REVERT` button for existing levels\n  - This is important because otherwise the only ways to select a different\n    level are to save the changes or restart the editor.\n\n### Fixed\n- Knobs remaining during playtest.\n\n## 0.3.0 - 2022-08-18\n### Changed\n- Update Bevy version to 0.8 and bevy-egui version to 0.15.\n\n### Removed\n- **REGRESSION**: Removed `vpeol_3d` and `example3d`. They were depending on\n  crates that were slow to migrate to Bevy 0.8 (one of then has still not\n  released its Bevy 0.8 version when this changelog entry was written). Since\n  `vpeol_3d` was barely usable to begin with (the gizmo is not a good way to\n  move objects around - we need proper dragging! - and `bevy_mod_pickling`\n  required lots of hacks to play nice with Yoleck) it has been removed for now\n  and will be re-added in the future with less dependencies and better\n  interface.\n\n### Fixed\n- Use the correct transform when dragging child entities (#11)\n\n### Added\n- Knobs!\n\n## 0.2.0 - 2022-06-09\n### Added\n- `YoleckVpeolSelectionCuePlugin` for adding a pulse effect to show the\n  selected entity in the viewport.\n\n## 0.1.1 - 2022-06-02\n### Fixed\n- `vpeol_3d`: Entities sometimes getting deselected when cursor leaves egui area.\n- `vpeol_3d`: Freshly created entities getting selected in Yoleck but Gizmo is not shown.\n\n## 0.1.0 - 2022-06-01\n### Added\n- Building `YoleckTypeHandler`s to define the entity types.\n- Editing entity structs with egui.\n- Populating entities with components based on entity structs.\n- Editor file manager.\n- Level loading from files.\n- Level index loading.\n- `vpeol_2d` and `vpeol_3d`.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\"macros\"]\n\n[workspace.package]\nedition = \"2024\"\nauthors = [\"IdanArye <idanarye@gmail.com>\", \"dexsper <dexsperpro@gmail.com>\"]\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/idanarye/bevy-yoleck\"\n\n[package]\nname = \"bevy-yoleck\"\ndescription = \"Your Own Level Editor Creation Kit\"\nversion = \"0.31.0\"\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\ndocumentation = \"https://docs.rs/bevy-yoleck\"\nreadme = \"README.md\"\ncategories = [\"game-development\"]\nkeywords = [\"bevy\", \"gamedev\", \"level-editor\"]\nexclude = [\"assets\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nbevy-yoleck-macros = { version = \"0.10.0\", path = \"macros\" }\nbevy = { version = \"^0.18\", default-features = false, features = [\n    \"bevy_state\",\n    \"bevy_window\",\n    \"bevy_asset\",\n    \"bevy_log\",\n    \"bevy_camera\",\n] }\nbevy_egui = { version = \"^0.39\", default-features = false, features = [\n    \"default_fonts\",\n    \"render\",\n] }\nserde = \"^1\"\nserde_json = \"^1\"\nthiserror = \"^2\"\nuuid = \"1.9.1\"\narboard = {version = \"3.4\", optional = true}\n\n[features]\nbevy_reflect = []\nvpeol = []\nvpeol_2d = [\"vpeol\", \"bevy/bevy_text\", \"bevy/bevy_sprite\", \"bevy/png\"]\nvpeol_3d = [\"vpeol\", \"bevy/bevy_pbr\"]\n# Support clipboard with the Arboard crate. Otherwise the clipboard will be internal.\narboard = [\"dep:arboard\"]\n# Enable Wayland support in Arboard.\narboard_wayland = [\"arboard\", \"arboard/wayland-data-control\"]\n_example2d_full = [\"vpeol_2d\", \"bevy/bevy_gizmos\", \"bevy/bevy_sprite_render\"]\n_example3d_full = [\n    \"vpeol_3d\",\n    \"bevy/bevy_scene\",\n    \"bevy/bevy_gltf\",\n    \"bevy/bevy_animation\",\n    \"bevy/ktx2\",\n    \"bevy/zstd_rust\",\n    \"bevy/tonemapping_luts\",\n    \"bevy/bevy_gizmos\",\n    \"bevy/reflect_auto_register\",\n    \"bevy/bevy_sprite_render\",\n]\n_doors_to_other_levels_full = [\"vpeol_2d\", \"bevy/bevy_sprite_render\"]\n\n[dev-dependencies]\nbevy = { version = \"^0.18\", default-features = false, features = [\n    \"bevy_sprite\",\n    \"x11\",\n    \"bevy_window\",\n    \"bevy_text\",\n    \"bevy_render\",\n] }\n\n[[example]]\nname = \"example2d\"\nrequired-features = [\"_example2d_full\"]\n\n[[example]]\nname = \"example3d\"\nrequired-features = [\"_example3d_full\"]\n\n[[example]]\nname = \"custom_camera3d\"\nrequired-features = [\"_example3d_full\"]\n\n[[example]]\nname = \"doors_to_other_levels\"\nrequired-features = [\"_doors_to_other_levels_full\"]\n\n[package.metadata.docs.rs]\n\nall-features = true\nfeatures = [\n    \"bevy/x11\",       # required for bevy_egui\n    \"bevy/bevy_gltf\", # required for SceneRoot in vpeol_3d's doctests\n]\n"
  },
  {
    "path": "LICENSE-APACHE",
    "content": "                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don't include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "LICENSE-MIT",
    "content": "Permission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MIGRATION-GUIDES.md",
    "content": "# Migrating to Yoleck 0.19\n\n## Using the new Bevy types\n\nBevy 0.13 introduces types for defining directions and planes, which can be used instead of vectors. Yoleck (mainly Vpeol) uses them now in its API. The conversion should be straightforward.\n\nSpecifically:\n\n- `VpeolDragPlane` now uses `Plane3d`.\n- `Vpeol3dPluginForEditor` also uses `Plane3d`.\n- `Vpeol3dCameraControl` uses `Plane3d` for the camera drag plane, and\n`Direction3d` for configuring the UP direction to maintain while rotating\nthe camera.\n\n## `vpeol_read_click_on_entity`\n\nBevy 0.13 [split `WorldQuery` to `QueryData` and `FilterData`](https://bevyengine.org/learn/migration-guides/0-12-to-0-13/#split-worldquery-into-querydata-and-queryfilter) (though there is still a `WorldQuery` trait with some of that functionality). When you use `vpeol_read_click_on_entity`, the data passed to it is `QueryFilter`, not `QueryData` - which measn that if it's a component (which should usually be the case) you need `vpeol_read_click_on_entity::<Has<MyComponent>>` and not `vpeol_read_click_on_entity::<&MyComponent>` (which would have worked before)\n\n# Migrating to Yoleck 0.17\n\n## Loading levels\n\nInstead of a `YoleckLoadingCommand` resource, level loading is now done via entities. This means that instead of loading a level like this:\n```rust\nfn load_level(\n    mut yoleck_loading_command: ResMut<YoleckLoadingCommand>,\n    asset_server: Res<AssetServer>,\n) {\n    *yoleck_loading_command = YoleckLoadingCommand::FromAsset(asset_server.load(\"levels/my-level.yol\"));\n}\n```\n\nYou should do it like this:\n```rust\nfn load_level(\n    mut commands: Commands,\n    asset_server: Res<AssetServer>,\n) {\n    commands.spawn(YoleckLoadLevel(asset_server.load(\"levels/my-level.yol\")));\n}\n```\n\nNote that `YoleckLoadLevel` does not provide an equivalent for `YoleckLoadingCommand::FromData`. If you need to load a level from a value, put that value in `Assets<YoleckRawLevel>` first.\n\n## Clearing levels\n\nInstead of despawning all the entities marked with `YoleckBelongsToLevel`:\n\n```rust\nfn unload_level(\n    query: Query<Entity, With<YoleckBelongsToLevel>>,\n    mut commands: Commands,\n) {\n    for entity in query.iter() {\n        commands.entity(entity).despawn_recursive();\n    }\n}\n```\n\nYou should despawn the entities that represent the levels - the ones marked with `YoleckKeepLevel`:\n\n```rust\nfn unload_old_levels(\n    query: Query<Entity, With<YoleckKeepLevel>>,\n    mut commands: Commands,\n) {\n    for entity in query.iter() {\n        commands.entity(entity).despawn_recursive();\n    }\n}\n```\n\nYoleck will automatically despawn (with `despawn_recursive`) all the entities that belong to these levels.\n\nNote that it is also possible, if needed, to just remove the `YoleckKeepLevel` component from these entities to despawn their entities without despawning the level entities themselves.\n\n## Changes in `YoleckBelongsToLevel`\n\n`YoleckBelongsToLevel` now has a `pub level: Entity` field that specifies which level the entity belongs to. When unloading a level (by despawning a `YoleckKeepLevel` entity, or removing the `YoleckKeepLevel` component from it), the entities that will be despawned are the ones who's `YoleckBelongsToLevel` points at that level.\n\nAs before, if you create a component from a system and want it to be despawned when switching a level or restarting/finishing a playtest in the editor, it still needs the `YoleckBelongsToLevel` component. Except now you have to provide a level entity for it. Where should the level entity come from? Two options:\n\n* It can be attached to an existing level, so that its lifetime will be bound to it. This is useful for entities that need to exist in the level's space - when despawning the level, we don't want these entities to remain.\n\n  The easiest way to achieve this is to use the `YoleckBelongsToLevel` of another component in that level. For example - say you have a treasure chest, and when the player shoots at it it opens up and a powerup pops from it for the player to pick up. Since the chest should already have a `YoleckBelongsToLevel` component, and since the system that spawns the powerup should already need to use some components of the chest entity, it should be easy to just clone the chest's `YoleckBelongsToLevel` and add it to the powerup spawning command.\n\n* You can create a faux level and attach the entities to it. This is useful, for example, for a player character entity that can travel between levels. Just create a new entity with a `YoleckKeepLevel` component and add its `Entity` to the roaming entity inside a `YoleckBelongsToLevel` component.\n\n  Note that you can freely set an existing `YoleckBelongsToLevel` to point to different levels. So it might make more sense to switch the player character entity to different level as it travels between them than to associate it to some faux level. Both options are available.\n\n## Adding populate systems\n\n\n`yoleck_populate_schedule_mut` is removed - this no longer works:\n\n```rust\napp.yoleck_populate_schedule_mut().add_systems(my_populate_system);\n```\n\nInstead, just add the system on the `YoleckSchedule::Populate` schedule:\n```rust\napp.add_systems(YoleckSchedule::Populate, my_populate_system);\n```\n\n`yoleck_populate_schedule_mut` made ergonomic sense in Bevy 0.10, but since starting Bevy 0.11 one has to always specify the schedule, it is no longer that ergonomic to have this helper method.\n\n# Migrating to Yoleck 0.15\n\n## Accessing the YoleckUi\n\nNow that https://github.com/emilk/egui/pull/3233 got in to egui 0.23, and bevy_egui 0.22 was released with that new version of egui, `YoleckUi` can be made a regular resource again.\n\n`YoleckUi` can no longer be accessed with `NonSend`/`NonSendMut`, and must be accessed with the regular `Res`/`ResMut`.\n\n# Migrating to Yoleck 0.13\n\n## Accessing the YoleckUi\n\n`YoleckUi` is now a non-`Send` resource, which means it can no longer be accessed as a regular `Res`/`ResMut`. It must now be accessed as `NonSend`/`NonSendMut`.\n\nHopefully once https://github.com/emilk/egui/issues/3148 is fixed (and gets in to bevy_egui) this can be changed back.\n\n# Migrating to Yoleck 0.9\n\n## Importing\n\nMost of the commonly used stuff can be imported from the new prelude module:\n\n```rust\nuse bevy_yoleck::prelude::*;\n```\n\n## Entity type definition and registration\n\nPreviously entity types were declared as struct:\n\n```rust\n#[derive(Clone, PartialEq, Serialize, Deserialize)]\nstruct Foo {\n    #[serde(default)]\n    bar: Bar,\n    #[serde(default)]\n    baz: Baz,\n}\n```\n\nAnd registered with:\n\n```rust\napp.add_yoleck_handler({\n    YoleckTypeHandler::<Foo>::new(\"Foo\")\n        .populate_with(populate_foo)\n        .edit_with(edit_foo)\n});\n```\n\nStarting from 0.9, entities can be broken to multiple components:\n```rust\n#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\nstruct Bar {\n    // ...\n}\n\n#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\nstruct Baz {\n    // ...\n}\n```\n\nYou can still create one big component per entity type, but if there are data\nfields that are shared between different entity types it's better to split them\nout so that they can be edited with the same edit systems.\n\nInstead of registering type handlers, register entity types:\n\n```rust\napp.add_yoleck_entity_type({\n    YoleckEntityType::new(\"Foo\")\n        .with::<Bar>()\n        .with::<Baz>()\n});\n```\n\nUnlike `YoleckTypeHandler`, that specifies the one data structure used by the\nentity and all the edit and populate systems it'll have, `YoleckEntityType` can\nspecify multiple components and no systems. Systems are registered separately,\nand are not bound to a single entity type:\n\n```rust\napp.add_yoleck_edit_system(edit_bar);\napp.add_yoleck_edit_system(edit_baz);\napp.yoleck_populate_schedule_mut().add_systems((\n    populate_bar,\n    populate_baz,\n));\n```\n\n## Edit systems\n\nIn addition to the different method of registreation specified in the previous\nsection, the semantics of edit systems has also changed.\n\nPreviously, edit systems would use a closure:\n\n```rust\nfn edit_foo(mut edit: YoleckEdit<Foo>) {\n    edit.edit(|ctx, data, ui| {\n        // ...\n    });\n}\n```\n\nNow they use something that acts like a query:\n\n```rust\nfn edit_foo(mut ui: ResMut<YoleckUi>, mut edit: YoleckEdit<&mut Foo>) {\n    let Ok(mut foo) = edit.get_single_mut() else { return };\n    // ...\n}\n```\n\nThe differences:\n\n* Instead of a closure, we use `get_single_mut` to get the single entity. If no\n  entity is being edited, or if the edited entity does not match, we use\n  `return` to skip the rest of the edit system.\n  * In the future, when Yoleck will have multi-entity editing, `YoleckEdit`\n    will have `iter` and `iter_mut` for edit systems that can edit multiple\n    entities.\n* Instead of getting the entity type directly as a generic parameter (`Foo`),\n  `YoleckEdit` gets it like Bevy `Query`s would (`&mut Foo`). In fact,\n  `YoleckEdit` can accept anything a Bevy query would accept, including filters\n  as a second parameter.\n* Instead of getting the UI handle via a closure argument, we get it as a\n  resource in a separate `SystemParam` argument for the edit system function.\n\n## Populate systems\n\nIn addition to the different method of registreation specified in an earlier\nsection, the semantics of populate systems has also changed.\n\nPreviously, populate systems would look like this:\n\n```rust\nfn populate_foo(mut populate: YoleckPopulate<Foo>) {\n    populate.populate(|ctx, data, &mut cmd| {\n        // ...\n    });\n}\n```\n\nPopulate systems still use closures, but they look different:\n\n```rust\nfn populate_foo(mut populate: YoleckPopulate<&Foo>) {\n    populate.populate(|ctx, &mut cmd, foo| {\n        // ...\n    });\n}\n```\n\nThe differences:\n\n* Like `YoleckEdit`, `YoleckPopulate` also accepts query-like generic parameters.\n* The command and data arguments to the closure switch places. Now the command\n  is the second argument and the data is the third.\n* The data argument is actually what a Bevy query with the same generic\n  parameters as what the `YoleckPopulate` got would have yielded.\n\n## Child entities\n\nPreviously, a populate system could freely use `cmd.despawn_descendants();`.\nNow that there are multiple edit systems and their order is determined by a\nscheduler, this should not be used, so instead populate systems should mark\nchild entities they create so that they can despawn them later (usually when\nthey replace them with freshly spawned ones):\n\n```rust\nfn populate_system(mut populate: YoleckPopulate<&MyComponent>, marking: YoleckMarking) {\n    populate.populate(|_ctx, mut cmd, my_component| {\n        marking.despawn_marked(&mut cmd);\n        cmd.with_children(|commands| {\n            let mut child = commands.spawn(marking.marker());\n            child.insert((\n                // relevant Bevy components\n            ));\n        });\n    });\n}\n```\n\n## Passed data\n\nPreviously, passed data would be accessed from the context argument of an edit system's closure:\n\n## Knobs\n\nPreviously, knobs would be accessed from the context argument of an edit system's closure:\n\n```rust\nfn edit_foo(mut edit: YoleckEdit<Foo>, mut commands: Commands) {\n    edit.edit(|ctx, data, ui| {\n        let mut knob = ctx.knob(&mut commands, \"knob-ident\");\n    });\n}\n```\n\nStarting from 0.9 knobs are accessed with a new `SystemParam` named `YoleckKnobs`:\n\n```rust\nfn edit_foo(mut edit: YoleckEdit<&mut Foo>, mut knobs: YoleckKnobs) {\n    let Ok(mut foo) = edit.get_single_mut() else { return };\n    let mut knob = knobs.knob(\"knob-ident\");\n}\n```\n\nThe actual usage of the knob handle is unchained.\n\nNote that knobs are not associated to a specific edited entity (although they\ndo reset when the selection changes). This was also true before 0.9, but is\nmore visible now that they are not accessed from the edit closure's `ctx`.\n\n## Position manipulation with vpeol_2d\n\n* Instead of `vpeol_position_edit_adapter`, use `Vpeol2dPosition` as a Yoleck component.\n* Don't set the translation by yourself - let vpeol_2d do it.\n* If you need to also set rotation and scale, use `Vpeol2dRotatation` and\n  `Vpeol2dScale`. vpeol_2d does not currently offer edit systems for them (it\n  only takes them into account in the populate system), so you'll still have to\n  write them yourself.\n* `Vpeol2dPlugin` is split into two - `Vpeol2dPluginForEditor` and\n  `Vpeol2dPluginForGame`. Use the appropriate one based on how the process\n  started, just like you'd use the appropriate\n  `YoleckPluginForEditor`/`YoleckPluginForGame`.\n"
  },
  {
    "path": "README.md",
    "content": "[![Build Status](https://github.com/idanarye/bevy-yoleck/workflows/CI/badge.svg)](https://github.com/idanarye/bevy-yoleck/actions)\n[![Latest Version](https://img.shields.io/crates/v/bevy-yoleck.svg)](https://crates.io/crates/bevy-yoleck)\n[![Rust Documentation](https://img.shields.io/badge/nightly-rustdoc-blue.svg)](https://idanarye.github.io/bevy-yoleck/)\n[![Rust Documentation](https://img.shields.io/badge/stable-rustdoc-purple.svg)](https://docs.rs/bevy-yoleck/)\n\n# Bevy YOLECK - Your Own Level Editor Creation Kit\n\nYoleck is a crate for having a game built with the Bevy game engine act as its\nown level editor.\n\n## Features\n\n* Same executable can launch in either game mode or editor mode, depending on\n  the plugins added to the app.\n* Write systems that create entities based on serializable structs - use same\n  systems for both loading the levels and visualizing them in the editor.\n* Entity editing is done with egui widgets that edit these structs.\n| * Automatic UI generation for components with support for numeric, boolean, string, vector, color, enum, option, list, asset, and entity fields.\n| * Entity linking system with automatic UI, filtering, and runtime UUID resolution. Supports drag-and-drop.\n  * Visual scene gizmo for camera orientation and control.\n* Support for external plugins that offer more visual editing.\n  * One simple such plugin - Vpeol is included in the crate. It provides basic\n    entity selection, positioning with mouse dragging, and basic camera\n    control. It has two variants behind feature flags - `vpeol_2d` and\n    `vpeol_3d`.\n* A knobs mechanism for more visual editing.\n* Playtest the levels inside the editor.\n* Multiple entity selection in the editor with the Shift key.\n* Optional console system for displaying logs in the UI |\n\n## Examples:\n\n```bash\ngit clone https://github.com/dexsper/bevy-yoleck-fork\ncd bevy-yoleck\n```\n\nThen you can run the examples:\n\n* 2D example:\n  ```bash\n  cargo example2d\n  ```\n  Or check out the WASM version: https://idanarye.github.io/bevy-yoleck/demos/example2d\n\n  https://user-images.githubusercontent.com/1149255/228007948-31a37b3f-7bd3-4a36-a3bc-4617d359c7c2.mp4\n\n* 3D example:\n  ```bash\n  cargo example3d\n  ```\n  Or check out the WASM version: https://idanarye.github.io/bevy-yoleck/demos/example3d\n\n  https://user-images.githubusercontent.com/1149255/228008014-825ef02e-2edc-49f5-a15c-1fa6044f84de.mp4\n\n* Multi-level example:\n  ```bash\n  cargo doors_to_other_levels\n  ```\n  Or check out the WASM version (gameplay only): https://idanarye.github.io/bevy-yoleck/demos/doors_to_other_levels\n\n  https://github.com/idanarye/bevy-yoleck/assets/1149255/590beba4-2ca5-4218-af52-143321bb5946\n\n## File Format\n\nYoleck saves the levels in JSON files that have the `.yol` extension. A `.yol`\nfile's top level is a tuple (actually JSON array) of three values:\n\n* File metadata - e.g. Yoleck version.\n* Level data (placeholder - currently an empty object)\n* List of entities.\n\nEach entity is a tuple of two values:\n\n* Entity metadata - e.g. its type.\n* Entity componments - that's the user defined structs.\n\nThe reason tuples are used instead of objects is to ensure ordering - to\nguarantee the metadata can be read before the data. This is important because\nthe metadata is needed to parse the data.\n\nYoleck generates another JSON file in the same directory as the `.yol` files\ncalled `index.yoli`. The purpose of this file is to let the game know what\nlevel are available to it (in WASM, for example, the asset server cannot look\nat a directory's contents). The index file contains a tuple of two values:\n\n* Index metadata - e.g. Yoleck version.\n* List of objects, each contain a path to a level file relative to the index\n  file.\n\n## Versions\n\n| bevy | bevy-yoleck | bevy_egui |\n|------|-------------|-----------|\n| 0.18 | 0.31        | 0.39      |\n| 0.17 | 0.30        | 0.38      |\n| 0.17 | 0.29        | 0.37      |\n| 0.16 | 0.28        | 0.36      |\n| 0.16 | 0.27        | 0.35      |\n| 0.16 | 0.26        | 0.34      |\n| 0.15 | 0.25        | 0.33      |\n| 0.15 | 0.24        | 0.32      |\n| 0.15 | 0.23        | 0.31      |\n| 0.14 | 0.22        | 0.28      |\n| 0.13 | 0.21        | 0.27      |\n| 0.13 | 0.20        | 0.26      |\n| 0.13 | 0.19        | 0.25      |\n| 0.12 | 0.18        | 0.24      |\n| 0.12 | 0.16, 0.17  | 0.23      |\n| 0.11 | 0.15        | 0.22      |\n| 0.11 | 0.13 - 0.14 | 0.21      |\n| 0.10 | 0.7 - 0.12  | 0.20      |\n| 0.9  | 0.5, 0.6    | 0.19      |\n| 0.9  | 0.4         | 0.17      |\n| 0.8  | 0.3         | 0.15      |\n| 0.7  | 0.1, 0.2    | 0.14      |\n\n## License\n\nLicensed under either of\n\n * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n\nat your option.\n\n### Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally submitted\nfor inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any\nadditional terms or conditions.\n"
  },
  {
    "path": "assets/levels2d/.gitkeep",
    "content": ""
  },
  {
    "path": "assets/levels2d/example.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":1},{},[[{\"type\":\"Player\",\"name\":\"\"},{\"Vpeol2dPosition\":[-309.3874206542969,81.44441223144531],\"Vpeol2dRotatation\":0.0}],[{\"type\":\"Triangle\",\"name\":\"\"},{\"TriangleVertices\":{\"vertices\":[[-12.283805847167969,-141.28375244140625],[211.94580078125,-151.37274169921875],[199.81985473632812,16.863494873046875]]},\"Vpeol2dPosition\":[-459.87017822265625,-29.312294006347656]}],[{\"type\":\"FloatingText\",\"name\":\"\",\"uuid\":\"8acaa597-8c7d-4b82-8fd5-4eda44e24b5f\"},{\"TextContent\":{\"text\":\"Collect the fruit\"},\"TextLaserPointer\":{\"target\":{}},\"Vpeol2dPosition\":[118.0838623046875,306.9156494140625],\"Vpeol2dScale\":[1.0,1.0]}],[{\"type\":\"FloatingText\",\"name\":\"\",\"uuid\":\"14195c3b-e420-4fa4-bc59-7149a7c7f5d4\"},{\"TextContent\":{\"text\":\"Orange\"},\"TextLaserPointer\":{\"target\":{\"uuid\":\"e8f8d516-ca5f-41d2-99a6-1be029fdd0ee\"}},\"Vpeol2dPosition\":[74.51683044433594,-226.31687927246094],\"Vpeol2dScale\":[0.75,0.75]}],[{\"type\":\"Fruit\",\"name\":\"\",\"uuid\":\"e8f8d516-ca5f-41d2-99a6-1be029fdd0ee\"},{\"FruitType\":{\"index\":1},\"Vpeol2dPosition\":[-3.697650909423828,-60.341732025146484]}],[{\"type\":\"Fruit\",\"name\":\"\",\"uuid\":\"eb26f293-7548-4bcc-a268-871c80726fd3\"},{\"FruitType\":{\"index\":2},\"Vpeol2dPosition\":[-265.73712158203125,-226.1751708984375]}],[{\"type\":\"FloatingText\",\"name\":\"\",\"uuid\":\"e9f7c89f-f975-41e0-9ab0-30991e715877\"},{\"TextContent\":{\"text\":\"Grapes\"},\"TextLaserPointer\":{\"target\":{\"uuid\":\"eb26f293-7548-4bcc-a268-871c80726fd3\"}},\"Vpeol2dPosition\":[-440.10369873046875,-230.0904998779297],\"Vpeol2dScale\":[0.75,0.75]}]]]"
  },
  {
    "path": "assets/levels3d/.gitkeep",
    "content": ""
  },
  {
    "path": "assets/levels3d/example.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":0},{},[[{\"type\":\"Planet\",\"name\":\"\",\"uuid\":\"c8aa8fdd-5431-473c-8774-b069c95f794f\"},{\"Vpeol3dPosition\":[-7.047080993652344,6.785710334777832,0.0],\"Vpeol3dRotation\":[0.0,0.0,0.0,1.0],\"Vpeol3dScale\":[1.0,1.0,1.0]}],[{\"type\":\"Planet\",\"name\":\"\",\"uuid\":\"2d676a47-146f-49f0-bba7-cf58a8833b1f\"},{\"Vpeol3dPosition\":[16.688783645629883,-3.0687923431396484,0.0],\"Vpeol3dRotation\":[0.0,0.0,0.0,1.0],\"Vpeol3dScale\":[1.0,1.0,1.0]}],[{\"type\":\"Spaceship\",\"name\":\"\"},{\"SpaceshipSettings\":{\"enabled\":true,\"rotation_speed\":2.0,\"speed\":2.0},\"Vpeol3dPosition\":[1.2451118230819702,-2.86102294921875e-6,21.062850952148438]}],[{\"type\":\"PlanetPointer\",\"name\":\"\"},{\"LaserPointer\":{\"target\":{\"uuid\":\"c8aa8fdd-5431-473c-8774-b069c95f794f\"}},\"Vpeol3dPosition\":[3.227558135986328,2.0,-29.523635864257812]}]]]"
  },
  {
    "path": "assets/levels_doors/entry.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":0},{},[[{\"type\":\"FloatingText\",\"name\":\"\"},{\"TextContent\":{\"text\":\"Start Room\"},\"Vpeol2dPosition\":[28.116905212402344,132.0689239501953],\"Vpeol2dScale\":[1.0,1.0]}],[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"s-1\",\"target_level\":\"room1.yol\"},\"Vpeol2dPosition\":[326.6886291503906,-15.409356117248535],\"Vpeol2dRotatation\":0.0}],[{\"type\":\"Player\",\"name\":\"\"},{\"Vpeol2dPosition\":[0.0,0.0]}]]]"
  },
  {
    "path": "assets/levels_doors/index.yoli",
    "content": "[{\"format_version\":1},[{\"filename\":\"entry.yol\"},{\"filename\":\"room1.yol\"},{\"filename\":\"room2.yol\"},{\"filename\":\"room3.yol\"}]]"
  },
  {
    "path": "assets/levels_doors/room1.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":0},{},[[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"s-1\",\"target_level\":\"entry.yol\"},\"Vpeol2dPosition\":[-341.4138488769531,-77.32525634765625],\"Vpeol2dRotatation\":-3.1367220878601074}],[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"1-2\",\"target_level\":\"room2.yol\"},\"Vpeol2dPosition\":[13.468215942382812,371.9578857421875],\"Vpeol2dRotatation\":1.5801841020584106}],[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"1-3\",\"target_level\":\"room3.yol\"},\"Vpeol2dPosition\":[23.59503173828125,-422.729248046875],\"Vpeol2dRotatation\":-1.5713714361190796}],[{\"type\":\"Player\",\"name\":\"\"},{\"Vpeol2dPosition\":[0.0,0.0]}],[{\"type\":\"FloatingText\",\"name\":\"\"},{\"TextContent\":{\"text\":\"Room 1\"},\"Vpeol2dPosition\":[3.049217939376831,77.6406021118164],\"Vpeol2dScale\":[1.0,1.0]}]]]"
  },
  {
    "path": "assets/levels_doors/room2.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":0},{},[[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"1-2\",\"target_level\":\"room1.yol\"},\"Vpeol2dPosition\":[-5.04287052154541,-384.5793762207031],\"Vpeol2dRotatation\":-1.5634950399398804}],[{\"type\":\"Player\",\"name\":\"\"},{\"Vpeol2dPosition\":[0.0,0.0]}],[{\"type\":\"FloatingText\",\"name\":\"\"},{\"TextContent\":{\"text\":\"Room 2\"},\"Vpeol2dPosition\":[-3.6070098876953125,-112.29154968261719],\"Vpeol2dScale\":[1.0,1.0]}]]]"
  },
  {
    "path": "assets/levels_doors/room3.yol",
    "content": "[{\"format_version\":2,\"app_format_version\":0},{},[[{\"type\":\"Doorway\",\"name\":\"\"},{\"Doorway\":{\"marker\":\"1-3\",\"target_level\":\"room1.yol\"},\"Vpeol2dPosition\":[-4.219451904296875,364.6886291503906],\"Vpeol2dRotatation\":1.5755369663238525}],[{\"type\":\"Player\",\"name\":\"\"},{\"Vpeol2dPosition\":[0.0,0.0]}],[{\"type\":\"FloatingText\",\"name\":\"\"},{\"TextContent\":{\"text\":\"Room 3\"},\"Vpeol2dPosition\":[1.2645721435546875,150.7604217529297],\"Vpeol2dScale\":[1.0,1.0]}]]]"
  },
  {
    "path": "assets/these-assets-are-for-the-examples",
    "content": ""
  },
  {
    "path": "examples/custom_camera3d.rs",
    "content": "use std::path::Path;\n\nuse bevy::color::palettes::css;\nuse bevy::prelude::*;\nuse bevy_egui::{EguiContexts, EguiPlugin};\nuse bevy_yoleck::YoleckEditMarker;\nuse bevy_yoleck::prelude::*;\nuse bevy_yoleck::vpeol::prelude::*;\n\n/// Custom camera mode for isometric view with diagonal movement.\nconst CAMERA_MODE_ISOMETRIC: Vpeol3dCameraMode = Vpeol3dCameraMode::Custom(0);\n/// Custom camera mode for orbital rotation around selected entities.\nconst CAMERA_MODE_ORBITAL: Vpeol3dCameraMode = Vpeol3dCameraMode::Custom(1);\n\nfn main() {\n    let mut app = App::new();\n    app.add_plugins(DefaultPlugins);\n\n    let level = std::env::args().nth(1);\n    if let Some(level) = level {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForGame);\n        app.add_plugins(Vpeol3dPluginForGame);\n        app.add_systems(\n            Startup,\n            move |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(\n                    asset_server.load(Path::new(\"levels3d\").join(&level)),\n                ));\n            },\n        );\n    } else {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForEditor);\n        app.add_plugins(Vpeol3dPluginForEditor::topdown());\n        app.add_plugins(VpeolSelectionCuePlugin::default());\n        app.insert_resource(bevy_yoleck::YoleckEditorLevelsDirectoryPath(\n            Path::new(\".\").join(\"assets\").join(\"levels3d\"),\n        ));\n        #[cfg(target_arch = \"wasm32\")]\n        app.add_systems(\n            Startup,\n            |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(asset_server.load(\"levels3d/example.yol\")));\n            },\n        );\n\n        app.insert_resource(\n            YoleckCameraChoices::default()\n                .choice_with_transform(\n                    \"Isometric\",\n                    {\n                        let mut control = Vpeol3dCameraControl::fps();\n                        control.mode = CAMERA_MODE_ISOMETRIC;\n                        control.allow_rotation_while_maintaining_up = None;\n                        control.wasd_movement_speed = 15.0;\n                        control\n                    },\n                    Vec3::new(10.0, 10.0, 10.0),\n                    Vec3::ZERO,\n                    Vec3::Y,\n                )\n                .choice_with_transform(\n                    \"Orbital\",\n                    {\n                        let mut control = Vpeol3dCameraControl::fps();\n                        control.mode = CAMERA_MODE_ORBITAL;\n                        control.allow_rotation_while_maintaining_up = None;\n                        control.wasd_movement_speed = 0.0;\n                        control.mouse_sensitivity = 0.005;\n                        control\n                    },\n                    Vec3::new(0.0, 5.0, 15.0),\n                    Vec3::ZERO,\n                    Vec3::Y,\n                ),\n        );\n\n        app.add_systems(\n            PostUpdate,\n            isometric_camera_movement.run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_systems(\n            PostUpdate,\n            orbital_camera_movement.run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n    }\n\n    app.add_systems(Startup, (setup_camera, setup_scene));\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Cube\")\n            .with::<Vpeol3dPosition>()\n            .with::<Vpeol3dScale>()\n            .insert_on_init(|| IsCube)\n    });\n    app.add_systems(YoleckSchedule::Populate, populate_cube);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Sphere\")\n            .with::<Vpeol3dPosition>()\n            .insert_on_init(|| IsSphere)\n    });\n    app.add_systems(YoleckSchedule::Populate, populate_sphere);\n\n    app.run();\n}\n\nfn setup_camera(mut commands: Commands) {\n    commands.spawn((\n        Camera3d::default(),\n        Transform::from_xyz(10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),\n        VpeolCameraState::default(),\n        Vpeol3dCameraControl::topdown(),\n    ));\n\n    commands.spawn((\n        DirectionalLight {\n            color: Color::WHITE,\n            illuminance: 10_000.0,\n            shadows_enabled: true,\n            ..Default::default()\n        },\n        Transform::from_xyz(5.0, 10.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),\n    ));\n}\n\nfn setup_scene(\n    mut commands: Commands,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n) {\n    let mesh = mesh_assets.add(Mesh::from(\n        Plane3d {\n            normal: Dir3::Y,\n            half_size: Vec2::new(50.0, 50.0),\n        }\n        .mesh(),\n    ));\n    let material = material_assets.add(Color::from(css::DARK_GRAY));\n    commands.spawn((\n        Mesh3d(mesh),\n        MeshMaterial3d(material),\n        Transform::from_xyz(0.0, -1.0, 0.0),\n    ));\n}\n\nfn isometric_camera_movement(\n    mut egui_context: EguiContexts,\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    time: Res<Time>,\n    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n) -> Result {\n    if egui_context.ctx_mut()?.wants_keyboard_input() {\n        return Ok(());\n    }\n\n    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {\n        if camera_control.mode != CAMERA_MODE_ISOMETRIC {\n            continue;\n        }\n\n        let mut direction = Vec3::ZERO;\n\n        if keyboard_input.pressed(KeyCode::KeyW) {\n            direction += Vec3::new(-1.0, 0.0, -1.0);\n        }\n        if keyboard_input.pressed(KeyCode::KeyS) {\n            direction += Vec3::new(1.0, 0.0, 1.0);\n        }\n        if keyboard_input.pressed(KeyCode::KeyA) {\n            direction += Vec3::new(-1.0, 0.0, 1.0);\n        }\n        if keyboard_input.pressed(KeyCode::KeyD) {\n            direction += Vec3::new(1.0, 0.0, -1.0);\n        }\n        if keyboard_input.pressed(KeyCode::KeyQ) {\n            direction += Vec3::Y;\n        }\n        if keyboard_input.pressed(KeyCode::KeyE) {\n            direction += Vec3::NEG_Y;\n        }\n\n        if direction != Vec3::ZERO {\n            direction = direction.normalize();\n            let speed_multiplier = if keyboard_input.pressed(KeyCode::ShiftLeft) {\n                2.0\n            } else {\n                1.0\n            };\n\n            camera_transform.translation += direction\n                * camera_control.wasd_movement_speed\n                * speed_multiplier\n                * time.delta_secs();\n        }\n    }\n    Ok(())\n}\n\nfn orbital_camera_movement(\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    time: Res<Time>,\n    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n    selected_entities: Query<&GlobalTransform, With<YoleckEditMarker>>,\n) {\n    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {\n        if camera_control.mode != CAMERA_MODE_ORBITAL {\n            continue;\n        }\n\n        let look_at = if let Some(selected_transform) = selected_entities.iter().next() {\n            selected_transform.translation()\n        } else {\n            Vec3::ZERO\n        };\n\n        let mut orbit_speed = 0.0;\n        if keyboard_input.pressed(KeyCode::KeyA) {\n            orbit_speed += 1.0;\n        }\n        if keyboard_input.pressed(KeyCode::KeyD) {\n            orbit_speed -= 1.0;\n        }\n\n        if orbit_speed != 0.0 {\n            let rotation = Quat::from_axis_angle(Vec3::Y, orbit_speed * time.delta_secs());\n            let offset = camera_transform.translation - look_at;\n            camera_transform.translation = look_at + rotation * offset;\n            camera_transform.look_at(look_at, Vec3::Y);\n        }\n\n        let mut zoom = 0.0;\n        if keyboard_input.pressed(KeyCode::KeyW) {\n            zoom -= 1.0;\n        }\n        if keyboard_input.pressed(KeyCode::KeyS) {\n            zoom += 1.0;\n        }\n\n        if zoom != 0.0 {\n            let direction = (camera_transform.translation - look_at).normalize();\n            camera_transform.translation += direction * zoom * 10.0 * time.delta_secs();\n        }\n\n        camera_transform.look_at(look_at, Vec3::Y);\n    }\n}\n\n#[derive(Component)]\nstruct IsCube;\n\nfn populate_cube(\n    mut populate: YoleckPopulate<(), With<IsCube>>,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut mesh: Local<Option<Handle<Mesh>>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n    mut material: Local<Option<Handle<StandardMaterial>>>,\n) {\n    populate.populate(|ctx, mut cmd, ()| {\n        cmd.insert(VpeolWillContainClickableChildren);\n        if ctx.is_first_time() {\n            let mesh = mesh\n                .get_or_insert_with(|| {\n                    mesh_assets.add(Mesh::from(Cuboid::from_size(Vec3::splat(2.0))))\n                })\n                .clone();\n            let material = material\n                .get_or_insert_with(|| material_assets.add(Color::from(css::BLUE)))\n                .clone();\n            cmd.insert((Mesh3d(mesh), MeshMaterial3d(material)));\n        }\n    });\n}\n\n#[derive(Component)]\nstruct IsSphere;\n\nfn populate_sphere(\n    mut populate: YoleckPopulate<(), With<IsSphere>>,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut mesh: Local<Option<Handle<Mesh>>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n    mut material: Local<Option<Handle<StandardMaterial>>>,\n) {\n    populate.populate(|ctx, mut cmd, ()| {\n        cmd.insert(VpeolWillContainClickableChildren);\n        if ctx.is_first_time() {\n            let mesh = mesh\n                .get_or_insert_with(|| mesh_assets.add(Mesh::from(Sphere { radius: 1.0 })))\n                .clone();\n            let material = material\n                .get_or_insert_with(|| material_assets.add(Color::from(css::RED)))\n                .clone();\n            cmd.insert((Mesh3d(mesh), MeshMaterial3d(material)));\n        }\n    });\n}\n"
  },
  {
    "path": "examples/doors_to_other_levels.rs",
    "content": "use std::path::Path;\n\nuse bevy::color::palettes::css;\nuse bevy::math::Affine3A;\nuse bevy::platform::collections::HashSet;\nuse bevy::prelude::*;\nuse bevy_egui::{EguiPlugin, egui};\nuse bevy_yoleck::vpeol::prelude::*;\nuse bevy_yoleck::{YoleckEditableLevels, prelude::*};\nuse serde::{Deserialize, Serialize};\n\nfn main() {\n    let mut app = App::new();\n    app.add_plugins(DefaultPlugins);\n\n    let level = if cfg!(target_arch = \"wasm32\") {\n        Some(\"entry.yol\".to_owned())\n    } else {\n        std::env::args().nth(1)\n    };\n\n    app.add_plugins(EguiPlugin::default());\n\n    if let Some(level) = level {\n        app.add_plugins(YoleckPluginForGame);\n        app.add_plugins(Vpeol2dPluginForGame);\n        app.add_systems(\n            Startup,\n            move |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(\n                    asset_server.load(Path::new(\"levels_doors\").join(&level)),\n                ));\n            },\n        );\n    } else {\n        app.add_plugins(YoleckPluginForEditor);\n        app.add_plugins(Vpeol2dPluginForEditor);\n        app.add_plugins(VpeolSelectionCuePlugin::default());\n\n        app.insert_resource(bevy_yoleck::YoleckEditorLevelsDirectoryPath(\n            Path::new(\".\").join(\"assets\").join(\"levels_doors\"),\n        ));\n    }\n\n    app.add_systems(Startup, setup_camera);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Player\")\n            .with::<Vpeol2dPosition>()\n            .insert_on_init(|| IsPlayer)\n    });\n    app.add_systems(YoleckSchedule::Populate, populate_player);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"FloatingText\")\n            .with::<Vpeol2dPosition>()\n            .with::<Vpeol2dScale>()\n            .with::<TextContent>()\n    });\n    app.add_yoleck_edit_system(edit_text);\n    app.add_systems(YoleckSchedule::Populate, populate_text);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Doorway\")\n            .with::<Vpeol2dPosition>()\n            .with::<Vpeol2dRotatation>()\n            .with::<Doorway>()\n    });\n    app.add_yoleck_edit_system(edit_doorway_rotation);\n    app.add_yoleck_edit_system(edit_doorway);\n    app.add_systems(YoleckSchedule::Populate, populate_doorway);\n    app.add_systems(Update, set_doorways_sprite_index);\n\n    app.add_systems(\n        YoleckSchedule::LevelLoaded,\n        (\n            handle_player_entity_when_level_loads,\n            position_level_from_opened_door,\n        ),\n    );\n\n    app.add_systems(\n        Update,\n        (\n            control_camera,\n            control_player,\n            handle_door_opening,\n            close_old_doors,\n        )\n            .run_if(in_state(YoleckEditorState::GameActive)),\n    );\n\n    app.run();\n}\n\nfn setup_camera(mut commands: Commands) {\n    commands.spawn((\n        Camera2d,\n        Transform::from_xyz(0.0, 0.0, 100.0).with_scale(2.0 * (Vec3::X + Vec3::Y) + Vec3::Z),\n        VpeolCameraState::default(),\n        Vpeol2dCameraControl::default(),\n    ));\n}\n\n#[derive(Component)]\nstruct IsPlayer;\n\nfn populate_player(\n    mut populate: YoleckPopulate<(), With<IsPlayer>>,\n    asset_server: Res<AssetServer>,\n    mut texture_cache: Local<Option<Handle<Image>>>,\n) {\n    populate.populate(|_ctx, mut cmd, ()| {\n        cmd.insert(Sprite {\n            image: texture_cache\n                .get_or_insert_with(|| asset_server.load(\"sprites/player.png\"))\n                .clone(),\n            custom_size: Some(Vec2::new(100.0, 100.0)),\n            ..Default::default()\n        });\n    });\n}\n\nfn control_camera(\n    player_query: Query<&GlobalTransform, With<IsPlayer>>,\n    mut camera_query: Query<&mut Transform, With<Camera>>,\n    time: Res<Time>,\n) {\n    if player_query.is_empty() {\n        return;\n    }\n    let position_to_track = player_query.iter().map(|t| t.translation()).sum::<Vec3>()\n        / player_query.iter().len() as f32;\n    for mut camera_transform in camera_query.iter_mut() {\n        let displacement = position_to_track - camera_transform.translation;\n        if displacement.length_squared() < 10000.0 {\n            camera_transform.translation +=\n                displacement.clamp_length_max(100.0 * time.delta_secs());\n        } else {\n            camera_transform.translation +=\n                displacement.clamp_length_max(800.0 * time.delta_secs());\n        }\n    }\n}\n\nfn control_player(\n    mut player_query: Query<&mut Transform, With<IsPlayer>>,\n    time: Res<Time>,\n    input: Res<ButtonInput<KeyCode>>,\n) {\n    let mut velocity = Vec3::ZERO;\n    if input.pressed(KeyCode::ArrowUp) {\n        velocity += Vec3::Y;\n    }\n    if input.pressed(KeyCode::ArrowDown) {\n        velocity -= Vec3::Y;\n    }\n    if input.pressed(KeyCode::ArrowLeft) {\n        velocity -= Vec3::X;\n    }\n    if input.pressed(KeyCode::ArrowRight) {\n        velocity += Vec3::X;\n    }\n    velocity *= 800.0;\n    for mut player_transform in player_query.iter_mut() {\n        player_transform.translation += velocity * time.delta_secs();\n    }\n}\n\n#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\nstruct TextContent {\n    text: String,\n}\n\nfn edit_text(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<(&mut TextContent, &mut Vpeol2dScale)>,\n) {\n    let Ok((mut content, mut scale)) = edit.single_mut() else {\n        return;\n    };\n    ui.text_edit_multiline(&mut content.text);\n    // TODO: do this in vpeol_2d?\n    ui.add(egui::Slider::new(&mut scale.0.x, 0.5..=5.0).logarithmic(true));\n    scale.0.y = scale.0.x;\n}\n\nfn populate_text(mut populate: YoleckPopulate<&TextContent>, asset_server: Res<AssetServer>) {\n    populate.populate(|ctx, mut cmd, content| {\n        let text;\n        let color;\n        if ctx.is_in_editor() && content.text.chars().all(|c| c.is_whitespace()) {\n            text = \"<TEXT>\".to_owned();\n            color = css::WHITE.with_alpha(0.25);\n        } else {\n            text = content.text.clone();\n            color = css::WHITE;\n        };\n        cmd.insert((\n            Text2d(text),\n            TextFont {\n                font: asset_server.load(\"fonts/FiraSans-Bold.ttf\"),\n                font_size: 72.0,\n                ..Default::default()\n            },\n            TextColor(color.into()),\n        ));\n    });\n}\n\n#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent, Debug)]\nstruct Doorway {\n    target_level: String,\n    marker: String,\n}\n\nfn edit_doorway(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<&mut Doorway>,\n    levels: Res<YoleckEditableLevels>,\n) {\n    let Ok(mut doorway) = edit.single_mut() else {\n        return;\n    };\n\n    ui.horizontal(|ui| {\n        egui::ComboBox::from_id_salt(\"doorway-level\")\n            .selected_text(\n                Some(doorway.target_level.as_str())\n                    .filter(|l| !l.is_empty())\n                    .unwrap_or(\"<target level>\"),\n            )\n            .show_ui(ui, |ui| {\n                for level in levels.names() {\n                    ui.selectable_value(&mut doorway.target_level, level.to_owned(), level);\n                }\n            });\n        egui::TextEdit::singleline(&mut doorway.marker)\n            .hint_text(\"<marker>\")\n            .show(ui);\n    });\n}\n\nfn edit_doorway_rotation(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<(&Vpeol2dPosition, &mut Vpeol2dRotatation), With<Doorway>>,\n    mut knobs: YoleckKnobs,\n) {\n    let Ok((Vpeol2dPosition(position), mut rotation)) = edit.single_mut() else {\n        return;\n    };\n    use std::f32::consts::PI;\n    ui.add(egui::Slider::new(&mut rotation.0, PI..=-PI).prefix(\"Angle: \"));\n    // TODO: do this in vpeol_2d?\n    let mut rotate_knob = knobs.knob(\"rotate\");\n    let knob_position = position.extend(1.0) + Quat::from_rotation_z(rotation.0) * (75.0 * Vec3::X);\n    rotate_knob.cmd.insert((\n        Sprite::from_color(css::PURPLE, Vec2::new(30.0, 30.0)),\n        Transform::from_translation(knob_position),\n        GlobalTransform::from(Transform::from_translation(knob_position)),\n    ));\n    if let Some(rotate_to) = rotate_knob.get_passed_data::<Vec3>() {\n        rotation.0 = Vec2::X.angle_to(rotate_to.truncate() - *position);\n    }\n}\n\nfn populate_doorway(\n    mut populate: YoleckPopulate<(), With<Doorway>>,\n    asset_server: Res<AssetServer>,\n    mut asset_handle_cache: Local<Option<(Handle<Image>, Handle<TextureAtlasLayout>)>>,\n    mut texture_atlas_layout_assets: ResMut<Assets<TextureAtlasLayout>>,\n) {\n    populate.populate(|_ctx, mut cmd, ()| {\n        let (image, texture_atlas_layout) = asset_handle_cache\n            .get_or_insert_with(|| {\n                (\n                    asset_server.load(\"sprites/doorway.png\"),\n                    texture_atlas_layout_assets.add(TextureAtlasLayout::from_grid(\n                        UVec2::new(64, 64),\n                        1,\n                        2,\n                        None,\n                        None,\n                    )),\n                )\n            })\n            .clone();\n        cmd.insert(Sprite {\n            image,\n            custom_size: Some(Vec2::new(100.0, 100.0)),\n            texture_atlas: Some(TextureAtlas {\n                layout: texture_atlas_layout,\n                index: 0,\n            }),\n            ..Default::default()\n        });\n    });\n}\n\nfn set_doorways_sprite_index(mut query: Query<(&mut Sprite, Has<DoorIsOpen>), With<Doorway>>) {\n    for (mut sprite, door_is_open) in query.iter_mut() {\n        if let Some(texture_atlas) = sprite.texture_atlas.as_mut() {\n            texture_atlas.index = if door_is_open { 1 } else { 0 };\n        }\n    }\n}\n\n#[derive(Component)]\nstruct LevelFromOpenedDoor {\n    exit_door: Entity,\n}\n\n#[derive(Component)]\nstruct PlayerHoldingLevel;\n\nfn handle_player_entity_when_level_loads(\n    levels_query: Query<Has<LevelFromOpenedDoor>, With<YoleckLevelJustLoaded>>,\n    mut players_query: Query<(Entity, &mut YoleckBelongsToLevel, &Vpeol2dPosition), With<IsPlayer>>,\n    mut commands: Commands,\n    mut camera_query: Query<&mut Transform, With<Camera>>,\n) {\n    for (player_entity, mut belongs_to_level, player_position) in players_query.iter_mut() {\n        let Ok(is_level_from_opened_door) = levels_query.get(belongs_to_level.level) else {\n            continue;\n        };\n        if is_level_from_opened_door {\n            commands.entity(player_entity).despawn();\n        } else {\n            belongs_to_level.level = commands\n                .spawn((\n                    // So that the player entity will be removed when finishing a playtest in the\n                    // editor:\n                    YoleckKeepLevel,\n                    // So that we won't remove that level when unloading old rooms:\n                    PlayerHoldingLevel,\n                ))\n                .id();\n            let translation_for_camera = player_position.0.extend(100.0);\n            for mut camera_transform in camera_query.iter_mut() {\n                *camera_transform = Transform {\n                    translation: translation_for_camera,\n                    rotation: Quat::IDENTITY,\n                    scale: 2.0 * (Vec3::X + Vec3::Y) + Vec3::Z,\n                };\n            }\n        }\n    }\n}\n\n#[derive(Component)]\nstruct DoorIsOpen;\n\n#[derive(Component)]\nstruct DoorConnectsTo(Entity);\n\nfn handle_door_opening(\n    players_query: Query<&GlobalTransform, With<IsPlayer>>,\n    doors_query: Query<\n        (Entity, &YoleckBelongsToLevel, &GlobalTransform, &Doorway),\n        Without<DoorIsOpen>,\n    >,\n    mut commands: Commands,\n    asset_server: Res<AssetServer>,\n    levels_query: Query<Entity, (With<YoleckKeepLevel>, Without<PlayerHoldingLevel>)>,\n) {\n    for player_transform in players_query.iter() {\n        for (door_entity, belongs_to_level, door_transform, doorway) in doors_query.iter() {\n            let distance_sq = player_transform\n                .translation()\n                .distance_squared(door_transform.translation());\n            if distance_sq < 10000.0 {\n                for level_entity in levels_query.iter() {\n                    if level_entity != belongs_to_level.level {\n                        commands.entity(level_entity).despawn();\n                    }\n                }\n\n                commands.entity(door_entity).insert(DoorIsOpen);\n                commands.spawn((\n                    YoleckLoadLevel(\n                        asset_server.load(format!(\"levels_doors/{}\", doorway.target_level)),\n                    ),\n                    LevelFromOpenedDoor {\n                        exit_door: door_entity,\n                    },\n                ));\n            }\n        }\n    }\n}\n\nfn position_level_from_opened_door(\n    levels_query: Query<(Entity, &LevelFromOpenedDoor), With<YoleckLevelJustLoaded>>,\n    doors_query: Query<(\n        Entity,\n        &YoleckBelongsToLevel,\n        Option<&GlobalTransform>,\n        &Doorway,\n        &Vpeol2dPosition,\n        &Vpeol2dRotatation,\n    )>,\n    mut commands: Commands,\n) {\n    for (level_entity, level_from_opened_door) in levels_query.iter() {\n        let Ok((_, _, Some(exit_door_transform), exit_doorway, _, _)) =\n            doors_query.get(level_from_opened_door.exit_door)\n        else {\n            continue;\n        };\n        let exit_door_affine = exit_door_transform.affine();\n        let (entry_door_entity, _, _, _, entry_door_position, entry_door_rotation) = doors_query\n            .iter()\n            .find(|(_, belongs_to_level, _, entry_doorway, _, _)| {\n                belongs_to_level.level == level_entity\n                    && entry_doorway.marker == exit_doorway.marker\n            })\n            .expect(&format!(\n                \"Cannot find a door marked as {:?} in {:?}\",\n                exit_doorway.marker, exit_doorway.target_level\n            ));\n        let entry_door_affine = Affine3A::from_rotation_translation(\n            Quat::from_rotation_z(entry_door_rotation.0),\n            entry_door_position.0.extend(0.0),\n        );\n        let rotate_door_around = Affine3A::from_rotation_translation(\n            Quat::from_rotation_z(std::f32::consts::PI),\n            100.0 * Vec3::X,\n        );\n        let level_transformation =\n            exit_door_affine * rotate_door_around * entry_door_affine.inverse();\n        let level_transformation = Transform::from_matrix(level_transformation.into());\n\n        commands\n            .entity(level_from_opened_door.exit_door)\n            .insert(DoorConnectsTo(entry_door_entity));\n        commands\n            .entity(entry_door_entity)\n            .insert((DoorIsOpen, DoorConnectsTo(level_from_opened_door.exit_door)));\n        commands\n            .entity(level_entity)\n            .insert(VpeolRepositionLevel(level_transformation));\n    }\n}\n\nfn close_old_doors(\n    mut removed_doors: RemovedComponents<DoorConnectsTo>,\n    doors_query: Query<(Entity, &DoorConnectsTo)>,\n    mut commands: Commands,\n) {\n    if removed_doors.is_empty() {\n        return;\n    }\n    let removed_doors = removed_doors.read().collect::<HashSet<Entity>>();\n\n    for (door_entity, door_connects_to) in doors_query.iter() {\n        if removed_doors.contains(&door_connects_to.0) {\n            commands\n                .entity(door_entity)\n                .remove::<(DoorIsOpen, DoorConnectsTo)>();\n        }\n    }\n}\n"
  },
  {
    "path": "examples/example2d.rs",
    "content": "use std::path::Path;\n\nuse bevy::color::palettes::css;\nuse bevy::ecs::system::SystemState;\nuse bevy::mesh::Indices;\nuse bevy::prelude::*;\nuse bevy::render::render_resource::PrimitiveTopology;\nuse bevy::{asset::RenderAssetUsages, log::LogPlugin};\nuse bevy_egui::{EguiContexts, EguiPlugin, egui};\n\nuse bevy_yoleck::YoleckDirective;\nuse bevy_yoleck::prelude::*;\nuse bevy_yoleck::vpeol::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nfn main() {\n    let mut app = App::new();\n    app.add_plugins(DefaultPlugins.set(LogPlugin {\n        custom_layer: bevy_yoleck::console_layer_factory,\n        ..default()\n    }));\n\n    let level = std::env::args().nth(1);\n    if let Some(level) = level {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForGame);\n        app.add_plugins(bevy_yoleck::vpeol_2d::Vpeol2dPluginForGame);\n        app.add_systems(\n            Startup,\n            move |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(\n                    asset_server.load(Path::new(\"levels2d\").join(&level)),\n                ));\n            },\n        );\n    } else {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForEditor);\n        app.insert_resource(bevy_yoleck::YoleckEditorLevelsDirectoryPath(\n            Path::new(\".\").join(\"assets\").join(\"levels2d\"),\n        ));\n        app.add_plugins(Vpeol2dPluginForEditor);\n        app.add_plugins(VpeolSelectionCuePlugin::default());\n        #[cfg(target_arch = \"wasm32\")]\n        app.add_systems(\n            Startup,\n            |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(asset_server.load(\"levels2d/example.yol\")));\n            },\n        );\n    }\n\n    app.add_plugins(YoleckEntityUpgradingPlugin {\n        app_format_version: 1,\n    });\n\n    app.add_systems(Startup, (setup_camera, setup_assets));\n\n    // ========================================================================\n    // Player\n    // ========================================================================\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Player\")\n            .with::<Vpeol2dPosition>()\n            .with::<Vpeol2dRotatation>()\n            .insert_on_init(|| IsPlayer)\n    });\n    app.add_yoleck_edit_system(edit_player);\n    app.add_systems(YoleckSchedule::Populate, populate_player);\n    app.add_yoleck_entity_upgrade_for(1, \"Player\", |data| {\n        let mut old_data = data.remove(\"Player\").unwrap();\n        data[\"Vpeol2dPosition\"] = old_data.get_mut(\"position\").unwrap().take();\n    });\n\n    // ========================================================================\n    // Fruit\n    // ========================================================================\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Fruit\")\n            .with_uuid()\n            .with::<Vpeol2dPosition>()\n            .with::<FruitType>()\n    });\n    app.add_yoleck_edit_system(duplicate_fruit);\n    app.add_yoleck_edit_system(edit_fruit_type);\n    app.add_systems(YoleckSchedule::Populate, populate_fruit);\n    app.add_yoleck_entity_upgrade(1, |type_name, data| {\n        if type_name != \"Fruit\" {\n            return;\n        }\n        let mut old_data = data.remove(\"Fruit\").unwrap();\n        data[\"Vpeol2dPosition\"] = old_data.get_mut(\"position\").unwrap().take();\n        data[\"FruitType\"] = serde_json::json!({\n            \"index\": old_data.get_mut(\"fruit_index\").unwrap().take(),\n        });\n    });\n\n    // ========================================================================\n    // FloatingText\n    // ========================================================================\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"FloatingText\")\n            .with_uuid()\n            .with::<Vpeol2dPosition>()\n            .with::<Vpeol2dScale>()\n            .with::<TextContent>()\n            .with::<TextLaserPointer>()\n    });\n    app.add_yoleck_auto_edit::<TextContent>();\n    app.add_yoleck_auto_edit::<TextLaserPointer>();\n    app.add_systems(YoleckSchedule::Populate, populate_text);\n    app.add_yoleck_entity_upgrade(1, |type_name, data| {\n        if type_name != \"FloatingText\" {\n            return;\n        }\n        let mut old_data = data.remove(\"FloatingText\").unwrap();\n        data[\"Vpeol2dPosition\"] = old_data.get_mut(\"position\").unwrap().take();\n        data[\"TextContent\"] = serde_json::json!({\n            \"text\": old_data.get_mut(\"text\").unwrap().take(),\n        });\n        data[\"Vpeol2dScale\"] = serde_json::to_value(\n            Vec2::ONE * old_data.get_mut(\"scale\").unwrap().take().as_f64().unwrap() as f32,\n        )\n        .unwrap();\n    });\n\n    // ========================================================================\n    // Triangle\n    // ========================================================================\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Triangle\")\n            .with::<Vpeol2dPosition>()\n            .with::<TriangleVertices>()\n    });\n    app.add_yoleck_edit_system(edit_triangle);\n    app.add_systems(YoleckSchedule::Populate, populate_triangle);\n\n    // ========================================================================\n    // Common systems\n    // ========================================================================\n\n    app.add_systems(Update, draw_laser_pointers);\n    app.add_systems(\n        Update,\n        (control_player, eat_fruits).run_if(in_state(YoleckEditorState::GameActive)),\n    );\n\n    app.run();\n}\n\n// ============================================================================\n// Setup\n// ============================================================================\n\nfn setup_camera(mut commands: Commands) {\n    commands.spawn((\n        Camera2d,\n        Transform::from_xyz(0.0, 0.0, 100.0),\n        VpeolCameraState::default(),\n        Vpeol2dCameraControl::default(),\n    ));\n}\n\nfn setup_assets(world: &mut World) {\n    world.init_resource::<GameAssets>();\n}\n\n#[derive(Resource)]\nstruct GameAssets {\n    fruits_sprite_sheet_texture: Handle<Image>,\n    fruits_sprite_sheet_layout: Handle<TextureAtlasLayout>,\n    fruits_sprite_sheet_egui: (egui::TextureId, Vec<egui::Rect>),\n    font: Handle<Font>,\n}\n\nimpl FromWorld for GameAssets {\n    fn from_world(world: &mut World) -> Self {\n        let mut system_state = SystemState::<(\n            Res<AssetServer>,\n            ResMut<Assets<TextureAtlasLayout>>,\n            EguiContexts,\n        )>::new(world);\n        let (asset_server, mut texture_atlas_layout_assets, mut egui_context) =\n            system_state.get_mut(world);\n        let fruits_atlas_texture = asset_server.load(\"sprites/fruits.png\");\n        let fruits_atlas_layout =\n            TextureAtlasLayout::from_grid(UVec2::new(64, 64), 3, 1, None, None);\n        let fruits_egui = {\n            (\n                egui_context.add_image(bevy_egui::EguiTextureHandle::Strong(\n                    fruits_atlas_texture.clone(),\n                )),\n                fruits_atlas_layout\n                    .textures\n                    .iter()\n                    .map(|rect| {\n                        [\n                            [\n                                rect.min.x as f32 / fruits_atlas_layout.size.x as f32,\n                                rect.min.y as f32 / fruits_atlas_layout.size.y as f32,\n                            ]\n                            .into(),\n                            [\n                                rect.max.x as f32 / fruits_atlas_layout.size.x as f32,\n                                rect.max.y as f32 / fruits_atlas_layout.size.y as f32,\n                            ]\n                            .into(),\n                        ]\n                        .into()\n                    })\n                    .collect(),\n            )\n        };\n        Self {\n            fruits_sprite_sheet_texture: fruits_atlas_texture,\n            fruits_sprite_sheet_layout: texture_atlas_layout_assets.add(fruits_atlas_layout),\n            fruits_sprite_sheet_egui: fruits_egui,\n            font: asset_server.load(\"fonts/FiraSans-Bold.ttf\"),\n        }\n    }\n}\n\n// ============================================================================\n// Player\n// ============================================================================\n\n#[derive(Component)]\nstruct IsPlayer;\n\nfn populate_player(\n    mut populate: YoleckPopulate<(), With<IsPlayer>>,\n    asset_server: Res<AssetServer>,\n    mut texture_cache: Local<Option<Handle<Image>>>,\n) {\n    populate.populate(|_ctx, mut cmd, ()| {\n        cmd.insert(Sprite {\n            image: texture_cache\n                .get_or_insert_with(|| asset_server.load(\"sprites/player.png\"))\n                .clone(),\n            custom_size: Some(Vec2::new(100.0, 100.0)),\n            ..Default::default()\n        });\n    });\n}\n\nfn edit_player(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<(&IsPlayer, &Vpeol2dPosition, &mut Vpeol2dRotatation)>,\n    mut knobs: YoleckKnobs,\n) {\n    let Ok((_, Vpeol2dPosition(position), mut rotation)) = edit.single_mut() else {\n        return;\n    };\n    use std::f32::consts::PI;\n    ui.add(egui::Slider::new(&mut rotation.0, PI..=-PI).prefix(\"Angle: \"));\n    let mut rotate_knob = knobs.knob(\"rotate\");\n    let knob_position = position.extend(1.0) + Quat::from_rotation_z(rotation.0) * (50.0 * Vec3::Y);\n    rotate_knob.cmd.insert((\n        Sprite::from_color(css::PURPLE, Vec2::new(30.0, 30.0)),\n        Transform::from_translation(knob_position),\n        GlobalTransform::from(Transform::from_translation(knob_position)),\n    ));\n    if let Some(rotate_to) = rotate_knob.get_passed_data::<Vec3>() {\n        rotation.0 = Vec2::Y.angle_to(rotate_to.truncate() - *position);\n    }\n}\n\nfn control_player(\n    mut player_query: Query<&mut Transform, With<IsPlayer>>,\n    time: Res<Time>,\n    input: Res<ButtonInput<KeyCode>>,\n) {\n    let mut velocity = Vec3::ZERO;\n    if input.pressed(KeyCode::ArrowUp) {\n        velocity += Vec3::Y;\n    }\n    if input.pressed(KeyCode::ArrowDown) {\n        velocity -= Vec3::Y;\n    }\n    if input.pressed(KeyCode::ArrowLeft) {\n        velocity -= Vec3::X;\n    }\n    if input.pressed(KeyCode::ArrowRight) {\n        velocity += Vec3::X;\n    }\n    velocity *= 400.0;\n    for mut player_transform in player_query.iter_mut() {\n        player_transform.translation += velocity * time.delta_secs();\n    }\n}\n\n// ============================================================================\n// Fruit\n// ============================================================================\n\n#[derive(Component)]\nstruct IsFruit;\n\n#[derive(\n    Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Component, YoleckComponent, Debug,\n)]\nstruct FruitType {\n    index: usize,\n}\n\nfn duplicate_fruit(\n    mut ui: ResMut<YoleckUi>,\n    edit: YoleckEdit<(&YoleckBelongsToLevel, &FruitType, &Vpeol2dPosition)>,\n    mut writer: MessageWriter<YoleckDirective>,\n) {\n    let Ok((belongs_to_level, fruit_type, Vpeol2dPosition(position))) = edit.single() else {\n        return;\n    };\n    if ui.button(\"Duplicate\").clicked() {\n        writer.write(\n            YoleckDirective::spawn_entity(belongs_to_level.level, \"Fruit\", true)\n                .with(Vpeol2dPosition(*position - 100.0 * Vec2::Y))\n                .with(FruitType {\n                    index: fruit_type.index,\n                })\n                .modify_exclusive_systems(|queue| queue.clear())\n                .into(),\n        );\n    }\n}\n\nfn edit_fruit_type(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<(Entity, &mut FruitType, &Vpeol2dPosition)>,\n    assets: Res<GameAssets>,\n    mut knobs: YoleckKnobs,\n) {\n    if edit.is_empty() {\n        return;\n    }\n\n    let (texture_id, rects) = &assets.fruits_sprite_sheet_egui;\n    let mut selected_fruit_types = vec![false; rects.len()];\n    for (entity, mut fruit_type, Vpeol2dPosition(position)) in edit.iter_matching_mut() {\n        selected_fruit_types[fruit_type.index] = true;\n        for index in 0..rects.len() {\n            if index != fruit_type.index {\n                let mut knob = knobs.knob((entity, \"select\", index));\n                let knob_position =\n                    (*position + Vec2::new(-30.0 + index as f32 * 30.0, 50.0)).extend(1.0);\n                knob.cmd.insert((\n                    Sprite {\n                        image: assets.fruits_sprite_sheet_texture.clone(),\n                        texture_atlas: Some(TextureAtlas {\n                            layout: assets.fruits_sprite_sheet_layout.clone(),\n                            index,\n                        }),\n                        custom_size: Some(Vec2::new(20.0, 20.0)),\n                        ..Default::default()\n                    },\n                    Transform::from_translation(knob_position),\n                    GlobalTransform::from(Transform::from_translation(knob_position)),\n                ));\n                if knob.get_passed_data::<YoleckKnobClick>().is_some() {\n                    fruit_type.index = index;\n                }\n            }\n        }\n    }\n    if edit.has_nonmatching() {\n        return;\n    }\n    let selected_fruit_types = selected_fruit_types;\n    let are_multile_types_selected = 1 < selected_fruit_types\n        .iter()\n        .filter(|is_selected| **is_selected)\n        .count();\n\n    ui.horizontal(|ui| {\n        for (index, rect) in rects.iter().enumerate() {\n            if ui\n                .add_enabled(\n                    are_multile_types_selected || !selected_fruit_types[index],\n                    egui::Button::image(\n                        egui::Image::new(egui::load::SizedTexture {\n                            id: *texture_id,\n                            size: egui::Vec2::new(25.0, 25.0),\n                        })\n                        .uv(*rect),\n                    )\n                    .selected(selected_fruit_types[index]),\n                )\n                .clicked()\n            {\n                for (_, mut fruit_type, _) in edit.iter_matching_mut() {\n                    fruit_type.index = index;\n                }\n            }\n        }\n    });\n}\n\nfn populate_fruit(\n    mut populate: YoleckPopulate<&FruitType>,\n    assets: Res<GameAssets>,\n    marking: YoleckMarking,\n) {\n    populate.populate(|_ctx, mut cmd, fruit| {\n        marking.despawn_marked(&mut cmd);\n        cmd.insert((\n            Visibility::default(),\n            VpeolWillContainClickableChildren,\n            IsFruit,\n        ));\n        cmd.with_children(|commands| {\n            let mut child = commands.spawn(marking.marker());\n            child.insert((Sprite {\n                image: assets.fruits_sprite_sheet_texture.clone(),\n                texture_atlas: Some(TextureAtlas {\n                    layout: assets.fruits_sprite_sheet_layout.clone(),\n                    index: fruit.index,\n                }),\n                custom_size: Some(Vec2::new(100.0, 100.0)),\n                ..Default::default()\n            },));\n        });\n    });\n}\n\nfn eat_fruits(\n    player_query: Query<&Transform, With<IsPlayer>>,\n    fruits_query: Query<(Entity, &Transform), With<IsFruit>>,\n    mut commands: Commands,\n) {\n    for player_transform in player_query.iter() {\n        for (fruit_entity, fruit_transform) in fruits_query.iter() {\n            if player_transform\n                .translation\n                .distance_squared(fruit_transform.translation)\n                < 100.0f32.powi(2)\n            {\n                commands.entity(fruit_entity).despawn();\n            }\n        }\n    }\n}\n\n// ============================================================================\n// FloatingText\n// ============================================================================\n\n#[derive(\n    Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent, YoleckAutoEdit,\n)]\npub struct TextContent {\n    #[yoleck(multiline)]\n    text: String,\n}\n\n#[derive(\n    Default,\n    Clone,\n    PartialEq,\n    Serialize,\n    Deserialize,\n    Component,\n    YoleckComponent,\n    YoleckAutoEdit,\n    Debug,\n)]\nstruct TextLaserPointer {\n    #[yoleck(entity_ref = \"Fruit\")]\n    target: YoleckEntityRef,\n}\n\nfn populate_text(mut populate: YoleckPopulate<&TextContent>, assets: Res<GameAssets>) {\n    populate.populate(|_ctx, mut cmd, content| {\n        cmd.insert((\n            Text2d(content.text.clone()),\n            TextFont {\n                font: assets.font.clone(),\n                font_size: 72.0,\n                ..Default::default()\n            },\n        ));\n    });\n}\n\n// ============================================================================\n// Triangle\n// ============================================================================\n\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\npub struct TriangleVertices {\n    vertices: [Vec2; 3],\n}\n\nimpl Default for TriangleVertices {\n    fn default() -> Self {\n        Self {\n            vertices: [\n                Vec2::new(-50.0, -50.0),\n                Vec2::new(50.0, -50.0),\n                Vec2::new(50.0, 50.0),\n            ],\n        }\n    }\n}\n\nfn edit_triangle(\n    mut edit: YoleckEdit<(&mut TriangleVertices, &GlobalTransform)>,\n    mut knobs: YoleckKnobs,\n) {\n    let Ok((mut triangle, triangle_transform)) = edit.single_mut() else {\n        return;\n    };\n    for (index, vertex) in triangle.vertices.iter_mut().enumerate() {\n        let mut knob = knobs.knob((\"move-vertex\", index));\n        if let Some(move_to) = knob.get_passed_data::<Vec3>() {\n            *vertex = triangle_transform\n                .to_matrix()\n                .inverse()\n                .transform_point3(*move_to)\n                .truncate();\n        }\n        let knob_position = triangle_transform.transform_point(vertex.extend(1.0));\n        knob.cmd.insert((\n            Sprite::from_color(css::RED, Vec2::new(15.0, 15.0)),\n            Transform::from_translation(knob_position),\n            GlobalTransform::from(Transform::from_translation(knob_position)),\n        ));\n    }\n}\n\nfn populate_triangle(\n    mut populate: YoleckPopulate<(&TriangleVertices, Option<&Mesh2d>)>,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut material_assets: ResMut<Assets<ColorMaterial>>,\n) {\n    populate.populate(|_ctx, mut cmd, (triangle, mesh2d)| {\n        let mesh = if let Some(Mesh2d(mesh_handle)) = mesh2d {\n            mesh_assets\n                .get_mut(mesh_handle)\n                .expect(\"mesh inserted by previous invocation of this system\")\n        } else {\n            let mesh_handle = mesh_assets.add(Mesh::new(\n                PrimitiveTopology::TriangleList,\n                RenderAssetUsages::default(),\n            ));\n            let mesh = mesh_assets.get_mut(&mesh_handle);\n            cmd.insert((\n                Mesh2d(mesh_handle),\n                MeshMaterial2d(material_assets.add(Color::from(css::GREEN))),\n            ));\n            mesh.expect(\"mesh was just inserted\")\n        };\n        mesh.insert_attribute(\n            Mesh::ATTRIBUTE_POSITION,\n            triangle\n                .vertices\n                .iter()\n                .map(|point| point.extend(0.0).to_array())\n                .collect::<Vec<_>>(),\n        );\n        let mut indices = Vec::new();\n        for i in 1..(triangle.vertices.len() - 1) {\n            let i = i as u32;\n            indices.extend([0, i, i + 1]);\n        }\n        mesh.insert_indices(Indices::U32(indices));\n    });\n}\n\n// ============================================================================\n// LaserPointer (shared)\n// ============================================================================\n\nfn draw_laser_pointers(\n    query: Query<(&TextLaserPointer, &GlobalTransform)>,\n    targets_query: Query<&GlobalTransform>,\n    mut gizmos: Gizmos,\n) {\n    for (laser_pointer, source_transform) in query.iter() {\n        if let Some(target_entity) = laser_pointer.target.entity()\n            && let Ok(target_transform) = targets_query.get(target_entity)\n        {\n            gizmos.line(\n                source_transform.translation(),\n                target_transform.translation(),\n                css::GREEN,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "examples/example3d.rs",
    "content": "use std::path::Path;\n\nuse bevy::prelude::*;\nuse bevy::{color::palettes::css, log::LogPlugin};\nuse bevy_egui::EguiPlugin;\nuse bevy_yoleck::prelude::*;\nuse bevy_yoleck::vpeol::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nfn main() {\n    let mut app = App::new();\n    app.add_plugins(DefaultPlugins.set(LogPlugin {\n        custom_layer: bevy_yoleck::console_layer_factory,\n        ..default()\n    }));\n\n    let level = std::env::args().nth(1);\n    if let Some(level) = level {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForGame);\n        app.add_plugins(Vpeol3dPluginForGame);\n        app.add_systems(\n            Startup,\n            move |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(\n                    asset_server.load(Path::new(\"levels3d\").join(&level)),\n                ));\n            },\n        );\n    } else {\n        app.add_plugins(EguiPlugin::default());\n        app.add_plugins(YoleckPluginForEditor);\n        app.add_plugins(Vpeol3dPluginForEditor::topdown());\n        app.add_plugins(VpeolSelectionCuePlugin::default());\n        app.insert_resource(bevy_yoleck::YoleckEditorLevelsDirectoryPath(\n            Path::new(\".\").join(\"assets\").join(\"levels3d\"),\n        ));\n        #[cfg(target_arch = \"wasm32\")]\n        app.add_systems(\n            Startup,\n            |asset_server: Res<AssetServer>, mut commands: Commands| {\n                commands.spawn(YoleckLoadLevel(asset_server.load(\"levels3d/example.yol\")));\n            },\n        );\n    }\n\n    app.add_systems(Startup, (setup_camera, setup_arena));\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Spaceship\")\n            .with::<Vpeol3dPosition>()\n            .with::<SpaceshipSettings>()\n            .insert_on_init(|| IsSpaceship)\n    });\n    app.add_yoleck_auto_edit::<SpaceshipSettings>();\n    app.add_systems(YoleckSchedule::Populate, populate_spaceship);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"Planet\")\n            .with_uuid()\n            .with::<Vpeol3dPosition>()\n            .with::<Vpeol3dRotation>()\n            .with::<Vpeol3dScale>()\n            .insert_on_init(|| IsPlanet)\n            .insert_on_init_during_editor(|| VpeolDragPlane::XY)\n    });\n    app.add_systems(YoleckSchedule::Populate, populate_planet);\n\n    app.add_yoleck_entity_type({\n        YoleckEntityType::new(\"PlanetPointer\")\n            .with::<Vpeol3dPosition>()\n            .with::<LaserPointer>()\n            .insert_on_init(|| SimpleSphere)\n            .insert_on_init_during_editor(|| Vpeol3dSnapToPlane {\n                normal: Dir3::Y,\n                offset: 2.0,\n            })\n    });\n    app.add_yoleck_auto_edit::<LaserPointer>();\n    app.add_systems(YoleckSchedule::Populate, populate_simple_sphere);\n    app.add_systems(Update, draw_laser_pointers);\n\n    app.add_systems(\n        Update,\n        (control_spaceship, hit_planets).run_if(in_state(YoleckEditorState::GameActive)),\n    );\n\n    app.run();\n}\n\n// ============================================================================\n// Setup\n// ============================================================================\n\nfn setup_camera(mut commands: Commands) {\n    commands.spawn((\n        Camera3d::default(),\n        Transform::from_xyz(0.0, 16.0, 40.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),\n        VpeolCameraState::default(),\n        Vpeol3dCameraControl::topdown(),\n    ));\n\n    commands.spawn((\n        DirectionalLight {\n            color: Color::WHITE,\n            illuminance: 50_000.0,\n            shadows_enabled: true,\n            ..Default::default()\n        },\n        Transform::from_xyz(0.0, 100.0, 0.0).looking_to(-Vec3::Y, Vec3::Z),\n    ));\n}\n\nfn setup_arena(\n    mut commands: Commands,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n) {\n    let mesh = mesh_assets.add(Mesh::from(\n        Plane3d {\n            normal: Dir3::Y,\n            half_size: Vec2::new(100.0, 100.0),\n        }\n        .mesh(),\n    ));\n    let material = material_assets.add(Color::from(css::GRAY));\n    commands.spawn((\n        Mesh3d(mesh),\n        MeshMaterial3d(material),\n        Transform::from_xyz(0.0, -10.0, 0.0),\n    ));\n}\n\n// ============================================================================\n// Spaceship\n// ============================================================================\n\n#[derive(Component)]\nstruct IsSpaceship;\n\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent, YoleckAutoEdit)]\nstruct SpaceshipSettings {\n    #[yoleck(label = \"Speed\", range(0.5..=10.0))]\n    speed: f32,\n    #[yoleck(label = \"Rotation Speed\", range(0.5..=5.0))]\n    rotation_speed: f32,\n    #[yoleck(label = \"Enabled\")]\n    enabled: bool,\n}\n\nimpl Default for SpaceshipSettings {\n    fn default() -> Self {\n        Self {\n            speed: 2.0,\n            rotation_speed: 2.0,\n            enabled: true,\n        }\n    }\n}\n\nfn populate_spaceship(\n    mut populate: YoleckPopulate<&SpaceshipSettings, With<IsSpaceship>>,\n    asset_server: Res<AssetServer>,\n) {\n    populate.populate(|ctx, mut cmd, _settings| {\n        cmd.insert(VpeolWillContainClickableChildren);\n        if ctx.is_first_time() {\n            cmd.insert(SceneRoot(asset_server.load(\"models/spaceship.glb#Scene0\")));\n        }\n    });\n}\n\nfn control_spaceship(\n    mut query: Query<(&mut Transform, &SpaceshipSettings), With<IsSpaceship>>,\n    time: Res<Time>,\n    input: Res<ButtonInput<KeyCode>>,\n) {\n    let calc_axis = |neg: KeyCode, pos: KeyCode| match (input.pressed(neg), input.pressed(pos)) {\n        (true, true) | (false, false) => 0.0,\n        (true, false) => -1.0,\n        (false, true) => 1.0,\n    };\n\n    let pitch = calc_axis(KeyCode::ArrowUp, KeyCode::ArrowDown);\n    let roll = calc_axis(KeyCode::ArrowLeft, KeyCode::ArrowRight);\n\n    for (mut transform, settings) in query.iter_mut() {\n        if !settings.enabled {\n            continue;\n        }\n        let forward = transform.rotation.mul_vec3(-Vec3::Z);\n        let roll_quat =\n            Quat::from_scaled_axis(settings.rotation_speed * forward * time.delta_secs() * roll);\n        let pitch_axis = transform.rotation.mul_vec3(Vec3::X);\n        let pitch_quat = Quat::from_scaled_axis(\n            settings.rotation_speed * pitch_axis * time.delta_secs() * pitch,\n        );\n        transform.rotation = roll_quat * pitch_quat * transform.rotation;\n        transform.translation += settings.speed * forward * time.delta_secs();\n    }\n}\n\n// ============================================================================\n// Planet\n// ============================================================================\n\n#[derive(Component)]\nstruct IsPlanet;\n\nfn populate_planet(\n    mut populate: YoleckPopulate<(), With<IsPlanet>>,\n    asset_server: Res<AssetServer>,\n) {\n    populate.populate(|ctx, mut cmd, ()| {\n        cmd.insert(VpeolWillContainClickableChildren);\n        if ctx.is_first_time() {\n            cmd.insert(SceneRoot(asset_server.load(\"models/planet.glb#Scene0\")));\n        }\n    });\n}\n\nfn hit_planets(\n    spaceship_query: Query<&Transform, With<IsSpaceship>>,\n    planets_query: Query<(Entity, &Transform), With<IsPlanet>>,\n    mut commands: Commands,\n) {\n    for spaceship_transform in spaceship_query.iter() {\n        for (planet_entity, planet_transform) in planets_query.iter() {\n            let planet_radius = planet_transform.scale.max_element();\n            let hit_distance = planet_radius + 1.0;\n            if spaceship_transform\n                .translation\n                .distance_squared(planet_transform.translation)\n                < hit_distance.powi(2)\n            {\n                commands.entity(planet_entity).despawn();\n            }\n        }\n    }\n}\n\n// ============================================================================\n// LaserPointer (PlanetPointer)\n// ============================================================================\n\n#[derive(Component)]\nstruct SimpleSphere;\n\n#[derive(\n    Default,\n    Clone,\n    PartialEq,\n    Serialize,\n    Deserialize,\n    Component,\n    YoleckComponent,\n    YoleckAutoEdit,\n    Debug,\n)]\nstruct LaserPointer {\n    #[yoleck(entity_ref = \"Planet\")]\n    target: YoleckEntityRef,\n}\n\nfn populate_simple_sphere(\n    mut populate: YoleckPopulate<(), With<SimpleSphere>>,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut mesh: Local<Option<Handle<Mesh>>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n    mut material: Local<Option<Handle<StandardMaterial>>>,\n) {\n    populate.populate(|ctx, mut cmd, ()| {\n        if ctx.is_first_time() {\n            let mesh = mesh\n                .get_or_insert_with(|| mesh_assets.add(Mesh::from(Sphere { radius: 1.0 })))\n                .clone();\n            let material = material\n                .get_or_insert_with(|| material_assets.add(Color::from(css::YELLOW)))\n                .clone();\n            cmd.insert((Mesh3d(mesh), MeshMaterial3d(material)));\n        }\n    });\n}\n\nfn draw_laser_pointers(\n    query: Query<(&LaserPointer, &GlobalTransform)>,\n    targets_query: Query<&GlobalTransform>,\n    mut gizmos: Gizmos,\n) {\n    for (laser_pointer, source_transform) in query.iter() {\n        if let Some(target_entity) = laser_pointer.target.entity()\n            && let Ok(target_transform) = targets_query.get(target_entity)\n        {\n            gizmos.line(\n                source_transform.translation(),\n                target_transform.translation(),\n                css::LIMEGREEN,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "macros/Cargo.toml",
    "content": "[package]\nname = \"bevy-yoleck-macros\"\ndescription = \"Macros for bevy-yoleck\"\nversion = \"0.10.0\"\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\n\n[lib]\nproc-macro = true\n\n[dependencies]\nsyn = { version = \"2\", features = [\"full\", \"extra-traits\"] }\nquote = \"1\"\nproc-macro2 = \"1\"\n"
  },
  {
    "path": "macros/src/lib.rs",
    "content": "use proc_macro2::TokenStream;\n\nuse quote::quote;\nuse syn::{Data, DeriveInput, Error, Field, Fields, LitStr, Token, Type};\n\n#[proc_macro_derive(YoleckComponent)]\npub fn derive_yoleck_component(input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n    let input = syn::parse_macro_input!(input as DeriveInput);\n    match impl_yoleck_component_derive(input) {\n        Ok(output) => output.into(),\n        Err(error) => error.to_compile_error().into(),\n    }\n}\n\nfn impl_yoleck_component_derive(input: DeriveInput) -> Result<TokenStream, Error> {\n    let name = input.ident;\n    let key = name.to_string();\n    let result = quote!(\n        impl YoleckComponent for #name {\n            const KEY: &'static str = #key;\n        }\n    );\n    Ok(result)\n}\n\n#[derive(Default, Debug)]\nstruct YoleckFieldAttrs {\n    range: Option<(f64, f64)>,\n    step: Option<f64>,\n    label: Option<String>,\n    tooltip: Option<String>,\n    readonly: bool,\n    hidden: bool,\n    multiline: bool,\n    color_picker: bool,\n    asset_extensions: Option<Vec<String>>,\n    entity_filter: Option<String>,\n    speed: Option<f64>,\n}\n\nfn parse_number(expr: &syn::Expr) -> syn::Result<f64> {\n    match expr {\n        syn::Expr::Lit(syn::ExprLit {\n            lit: syn::Lit::Int(i),\n            ..\n        }) => Ok(i.base10_parse::<f64>()?),\n        syn::Expr::Lit(syn::ExprLit {\n            lit: syn::Lit::Float(f),\n            ..\n        }) => Ok(f.base10_parse::<f64>()?),\n        syn::Expr::Unary(syn::ExprUnary {\n            op: syn::UnOp::Neg(_),\n            expr: inner,\n            ..\n        }) => Ok(-parse_number(inner)?),\n        _ => Err(syn::Error::new_spanned(expr, \"Expected numeric literal\")),\n    }\n}\n\nfn parse_field_attrs(field: &Field) -> Result<YoleckFieldAttrs, Error> {\n    let mut attrs = YoleckFieldAttrs::default();\n\n    for attr in &field.attrs {\n        if !attr.path().is_ident(\"yoleck\") {\n            continue;\n        }\n\n        attr.parse_nested_meta(|meta| {\n            if meta.path.is_ident(\"readonly\") {\n                attrs.readonly = true;\n                return Ok(());\n            }\n            if meta.path.is_ident(\"hidden\") {\n                attrs.hidden = true;\n                return Ok(());\n            }\n            if meta.path.is_ident(\"multiline\") {\n                attrs.multiline = true;\n                return Ok(());\n            }\n            if meta.path.is_ident(\"color_picker\") {\n                attrs.color_picker = true;\n                return Ok(());\n            }\n\n            if meta.path.is_ident(\"label\") {\n                let value: syn::LitStr = meta.value()?.parse()?;\n                attrs.label = Some(value.value());\n                return Ok(());\n            }\n            if meta.path.is_ident(\"tooltip\") {\n                let value: syn::LitStr = meta.value()?.parse()?;\n                attrs.tooltip = Some(value.value());\n                return Ok(());\n            }\n            if meta.path.is_ident(\"step\") {\n                let value: syn::LitFloat = meta.value()?.parse()?;\n                attrs.step = Some(value.base10_parse()?);\n                return Ok(());\n            }\n            if meta.path.is_ident(\"speed\") {\n                let value: syn::LitFloat = meta.value()?.parse()?;\n                attrs.speed = Some(value.base10_parse()?);\n                return Ok(());\n            }\n            if meta.path.is_ident(\"asset\") {\n                let value: syn::LitStr = meta.value()?.parse()?;\n                attrs.asset_extensions = Some(\n                    value\n                        .value()\n                        .split(',')\n                        .map(|s| s.trim().to_string())\n                        .collect(),\n                );\n                return Ok(());\n            }\n            if meta.path.is_ident(\"entity_ref\") {\n                let value: syn::LitStr = meta.value()?.parse()?;\n                attrs.entity_filter = Some(value.value());\n                return Ok(());\n            }\n            if meta.path.is_ident(\"range\") {\n                let content;\n                syn::parenthesized!(content in meta.input);\n\n                let expr: syn::Expr = content.parse()?;\n                match expr {\n                    syn::Expr::Range(syn::ExprRange {\n                        start: Some(start),\n                        end: Some(end),\n                        limits: syn::RangeLimits::Closed(_),\n                        ..\n                    }) => {\n                        let start_val = parse_number(&start)?;\n                        let end_val = parse_number(&end)?;\n                        attrs.range = Some((start_val, end_val));\n                        return Ok(());\n                    }\n                    _ => {\n                        return Err(syn::Error::new_spanned(\n                            expr,\n                            \"Expected closed numeric range, e.g., `0.5..=10.0`\",\n                        ));\n                    }\n                }\n            }\n\n            Err(meta.error(\"unknown yoleck attribute\"))\n        })?;\n    }\n\n    Ok(attrs)\n}\n\nfn get_type_name(ty: &Type) -> String {\n    match ty {\n        Type::Path(type_path) => type_path\n            .path\n            .segments\n            .last()\n            .map(|s| s.ident.to_string())\n            .unwrap_or_default(),\n        _ => String::new(),\n    }\n}\n\nfn quote_option<T, F>(opt: &Option<T>, f: F) -> TokenStream\nwhere\n    F: FnOnce(&T) -> TokenStream,\n{\n    match opt {\n        Some(value) => {\n            let inner = f(value);\n            quote! { Some(#inner) }\n        }\n        None => quote! { None },\n    }\n}\n\nfn generate_field_ui(field: &Field, attrs: &YoleckFieldAttrs) -> TokenStream {\n    let field_name = field.ident.as_ref().unwrap();\n    let field_name_str = attrs\n        .label\n        .clone()\n        .unwrap_or_else(|| field_name.to_string().replace('_', \" \"));\n\n    let range = quote_option(&attrs.range, |(min, max)| quote! { (#min, #max) });\n    let speed = quote_option(&attrs.speed, |s| quote! { #s });\n    let label_opt = quote_option(&attrs.label, |l| quote! { #l.to_string() });\n    let tooltip = quote_option(&attrs.tooltip, |t| quote! { #t.to_string() });\n    let entity_filter = quote_option(&attrs.entity_filter, |f| quote! { #f.to_string() });\n\n    let readonly = attrs.readonly;\n    let multiline = attrs.multiline;\n\n    quote! {\n        {\n            use bevy_yoleck::auto_edit::{YoleckAutoEdit, FieldAttrs};\n            let attrs = FieldAttrs {\n                label: #label_opt,\n                tooltip: #tooltip,\n                range: #range,\n                speed: #speed,\n                readonly: #readonly,\n                multiline: #multiline,\n                entity_filter: #entity_filter,\n            };\n            YoleckAutoEdit::auto_edit_with_label_and_attrs(\n                &mut value.#field_name,\n                ui,\n                #field_name_str,\n                &attrs,\n            );\n        }\n    }\n}\n\n#[proc_macro_derive(YoleckAutoEdit, attributes(yoleck))]\npub fn derive_yoleck_auto_edit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n    let input = syn::parse_macro_input!(input as DeriveInput);\n    match impl_yoleck_auto_edit_derive(input) {\n        Ok(output) => output.into(),\n        Err(error) => error.to_compile_error().into(),\n    }\n}\n\nfn impl_yoleck_auto_edit_derive(input: DeriveInput) -> Result<TokenStream, Error> {\n    let name = &input.ident;\n    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();\n\n    let fields = if let Data::Struct(data) = &input.data {\n        if let Fields::Named(fields) = &data.fields {\n            &fields.named\n        } else {\n            return Err(Error::new_spanned(\n                &input,\n                \"YoleckAutoEdit only supports structs with named fields\",\n            ));\n        }\n    } else {\n        return Err(Error::new_spanned(\n            &input,\n            \"YoleckAutoEdit only supports structs\",\n        ));\n    };\n\n    let mut field_uis = Vec::new();\n    for field in fields {\n        let attrs = parse_field_attrs(field)?;\n        if attrs.hidden {\n            continue;\n        }\n        field_uis.push(generate_field_ui(field, &attrs));\n    }\n\n    let mut entity_ref_fields = Vec::new();\n    let mut entity_ref_field_names = Vec::new();\n    for field in fields {\n        if let Some(info) = parse_entity_ref_attrs(field)? {\n            entity_ref_fields.push(info);\n            entity_ref_field_names.push(\n                field\n                    .ident\n                    .as_ref()\n                    .expect(\"fields are taken from a named struct variant\"),\n            );\n        }\n    }\n\n    let fields_array: Vec<TokenStream> = entity_ref_fields\n        .iter()\n        .map(|info| {\n            let field_ident = &info.field_ident;\n            let field_ident_str = LitStr::new(&field_ident.to_string(), field_ident.span());\n            let filter = match &info.filter {\n                Some(f) => quote! { Some(#f) },\n                None => quote! { None },\n            };\n\n            quote! { (#field_ident_str, #filter) }\n        })\n        .collect();\n\n    let match_arms: Vec<TokenStream> = entity_ref_fields\n        .iter()\n        .map(|info| {\n            let field_ident = &info.field_ident;\n            let field_ident_str = LitStr::new(&field_ident.to_string(), field_ident.span());\n\n            quote! {\n                #field_ident_str => &mut self.#field_ident\n            }\n        })\n        .collect();\n\n    let fields_count = entity_ref_fields.len();\n\n    let get_entity_ref_mut_body = if entity_ref_fields.is_empty() {\n        quote! {\n            panic!(\"No entity ref fields in {}\", stringify!(#name))\n        }\n    } else {\n        quote! {\n            match field_name {\n                #(#match_arms,)*\n                _ => panic!(\"Unknown entity ref field: {}\", field_name),\n            }\n        }\n    };\n\n    let result = quote! {\n        impl #impl_generics bevy_yoleck::auto_edit::YoleckAutoEdit for #name #ty_generics #where_clause {\n            fn auto_edit(value: &mut Self, ui: &mut bevy_yoleck::egui::Ui) {\n                use bevy_yoleck::egui;\n                #(#field_uis)*\n            }\n        }\n\n        impl #impl_generics bevy_yoleck::entity_ref::YoleckEntityRefAccessor for #name #ty_generics #where_clause {\n            fn entity_ref_fields() -> &'static [(&'static str, Option<&'static str>)] {\n                static FIELDS: [(&'static str, Option<&'static str>); #fields_count] = [\n                    #(#fields_array),*\n                ];\n                &FIELDS\n            }\n\n            fn get_entity_ref_mut(&mut self, field_name: &str) -> &mut bevy_yoleck::entity_ref::YoleckEntityRef {\n                #get_entity_ref_mut_body\n            }\n\n            fn resolve_entity_refs(&mut self, registry: &bevy_yoleck::prelude::YoleckUuidRegistry) {\n                #(\n                    let _ = self.#entity_ref_field_names.resolve(registry);\n                )*\n            }\n        }\n    };\n\n    Ok(result)\n}\n\n#[derive(Debug)]\nstruct EntityRefFieldInfo {\n    field_ident: syn::Ident,\n    filter: Option<String>,\n}\n\nfn parse_entity_ref_attrs(field: &Field) -> Result<Option<EntityRefFieldInfo>, Error> {\n    let type_name = get_type_name(&field.ty);\n\n    if type_name != \"YoleckEntityRef\" {\n        return Ok(None);\n    }\n\n    let field_ident = field\n        .ident\n        .as_ref()\n        .ok_or_else(|| Error::new_spanned(field, \"Expected named field\"))?\n        .clone();\n\n    let mut info = EntityRefFieldInfo {\n        field_ident,\n        filter: None,\n    };\n\n    for attr in &field.attrs {\n        if !attr.path().is_ident(\"yoleck\") {\n            continue;\n        }\n\n        attr.parse_nested_meta(|meta| {\n            if meta.path.is_ident(\"entity_ref\") {\n                if meta.input.peek(Token![=]) {\n                    let value: syn::LitStr = meta.value()?.parse()?;\n                    info.filter = Some(value.value());\n                }\n                return Ok(());\n            }\n            Ok(())\n        })?;\n    }\n\n    Ok(Some(info))\n}\n"
  },
  {
    "path": "run-retrospective-crate-version-tagging.sh",
    "content": "#!/bin/bash\n\n(\n    retrospective-crate-version-tagging detect \\\n        --crate-name bevy-yoleck \\\n        --changelog-path CHANGELOG.md \\\n        --tag-prefix v \\\n) | retrospective-crate-version-tagging create-releases\n"
  },
  {
    "path": "src/auto_edit.rs",
    "content": "use bevy::prelude::*;\nuse bevy_egui::egui;\n\nuse crate::YoleckInternalSchedule;\nuse crate::entity_ref::resolve_entity_refs;\n\nuse crate::entity_ref::validate_entity_ref_requirements_for;\n\nuse crate::entity_ref::YoleckEntityRef;\nuse crate::prelude::YoleckUuidRegistry;\n\nuse std::collections::HashMap;\n\n/// Attributes that can be applied to fields for customizing their UI\n#[derive(Default, Clone)]\npub struct FieldAttrs {\n    pub label: Option<String>,\n    pub tooltip: Option<String>,\n    pub range: Option<(f64, f64)>,\n    pub speed: Option<f64>,\n    pub readonly: bool,\n    pub multiline: bool,\n    pub entity_filter: Option<String>,\n}\n\npub trait YoleckAutoEdit: Send + Sync + 'static {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui);\n\n    /// Auto-edit with field-level attributes (label, tooltip, range, etc.)\n    /// Default implementation wraps auto_edit with label and common decorations\n    fn auto_edit_with_label_and_attrs(\n        value: &mut Self,\n        ui: &mut egui::Ui,\n        label: &str,\n        attrs: &FieldAttrs,\n    ) {\n        if attrs.readonly {\n            ui.add_enabled_ui(false, |ui| {\n                Self::auto_edit_field_impl(value, ui, label, attrs);\n            });\n        } else {\n            Self::auto_edit_field_impl(value, ui, label, attrs);\n        }\n    }\n\n    /// Internal implementation for field rendering with label\n    /// Types can override this to customize behavior based on attributes\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        ui.horizontal(|ui| {\n            ui.label(label);\n            let response = ui\n                .scope(|ui| {\n                    Self::auto_edit(value, ui);\n                })\n                .response;\n\n            if let Some(tooltip) = &attrs.tooltip {\n                response.on_hover_text(tooltip);\n            }\n        });\n    }\n}\n\npub fn render_auto_edit_value<T: YoleckAutoEdit>(ui: &mut egui::Ui, value: &mut T) {\n    T::auto_edit(value, ui);\n}\n\nimpl YoleckAutoEdit for f32 {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.add(egui::DragValue::new(value).speed(0.1));\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        ui.horizontal(|ui| {\n            ui.label(label);\n            let response = if let Some((min, max)) = attrs.range {\n                ui.add(egui::Slider::new(value, min as f32..=max as f32))\n            } else {\n                let speed = attrs.speed.unwrap_or(0.1) as f32;\n                ui.add(egui::DragValue::new(value).speed(speed))\n            };\n\n            if let Some(tooltip) = &attrs.tooltip {\n                response.on_hover_text(tooltip);\n            }\n        });\n    }\n}\n\nimpl YoleckAutoEdit for f64 {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.add(egui::DragValue::new(value).speed(0.1));\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        ui.horizontal(|ui| {\n            ui.label(label);\n            let response = if let Some((min, max)) = attrs.range {\n                ui.add(egui::Slider::new(value, min..=max))\n            } else {\n                let speed = attrs.speed.unwrap_or(0.1);\n                ui.add(egui::DragValue::new(value).speed(speed))\n            };\n\n            if let Some(tooltip) = &attrs.tooltip {\n                response.on_hover_text(tooltip);\n            }\n        });\n    }\n}\n\nmacro_rules! impl_auto_edit_for_integer {\n    ($($ty:ty),*) => {\n        $(\n            impl YoleckAutoEdit for $ty {\n                fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n                    ui.add(egui::DragValue::new(value).speed(1.0));\n                }\n\n                fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n                    ui.horizontal(|ui| {\n                        ui.label(label);\n                        let response = if let Some((min, max)) = attrs.range {\n                            ui.add(egui::Slider::new(value, min as $ty..=max as $ty))\n                        } else {\n                            let speed = attrs.speed.unwrap_or(1.0) as f32;\n                            ui.add(egui::DragValue::new(value).speed(speed))\n                        };\n\n                        if let Some(tooltip) = &attrs.tooltip {\n                            response.on_hover_text(tooltip);\n                        }\n                    });\n                }\n            }\n        )*\n    };\n}\n\nimpl_auto_edit_for_integer!(i32, i64, u32, u64, usize, isize);\n\nimpl YoleckAutoEdit for bool {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.checkbox(value, \"\");\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        ui.horizontal(|ui| {\n            let response = ui.checkbox(value, label);\n\n            if let Some(tooltip) = &attrs.tooltip {\n                response.on_hover_text(tooltip);\n            }\n        });\n    }\n}\n\nimpl YoleckAutoEdit for String {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.text_edit_singleline(value);\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        if attrs.multiline {\n            ui.label(label);\n            let response = ui.text_edit_multiline(value);\n\n            if let Some(tooltip) = &attrs.tooltip {\n                response.on_hover_text(tooltip);\n            }\n        } else {\n            ui.horizontal(|ui| {\n                ui.label(label);\n                let response = ui.text_edit_singleline(value);\n\n                if let Some(tooltip) = &attrs.tooltip {\n                    response.on_hover_text(tooltip);\n                }\n            });\n        }\n    }\n}\n\nimpl YoleckAutoEdit for Vec2 {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.horizontal(|ui| {\n            ui.add(egui::DragValue::new(&mut value.x).prefix(\"x: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.y).prefix(\"y: \").speed(0.1));\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let speed = attrs.speed.unwrap_or(0.1) as f32;\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                ui.add(\n                    egui::DragValue::new(&mut value.x)\n                        .prefix(\"x: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.y)\n                        .prefix(\"y: \")\n                        .speed(speed),\n                );\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl YoleckAutoEdit for Vec3 {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.horizontal(|ui| {\n            ui.add(egui::DragValue::new(&mut value.x).prefix(\"x: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.y).prefix(\"y: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.z).prefix(\"z: \").speed(0.1));\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let speed = attrs.speed.unwrap_or(0.1) as f32;\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                ui.add(\n                    egui::DragValue::new(&mut value.x)\n                        .prefix(\"x: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.y)\n                        .prefix(\"y: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.z)\n                        .prefix(\"z: \")\n                        .speed(speed),\n                );\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl YoleckAutoEdit for Vec4 {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.horizontal(|ui| {\n            ui.add(egui::DragValue::new(&mut value.x).prefix(\"x: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.y).prefix(\"y: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.z).prefix(\"z: \").speed(0.1));\n            ui.add(egui::DragValue::new(&mut value.w).prefix(\"w: \").speed(0.1));\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let speed = attrs.speed.unwrap_or(0.1) as f32;\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                ui.add(\n                    egui::DragValue::new(&mut value.x)\n                        .prefix(\"x: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.y)\n                        .prefix(\"y: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.z)\n                        .prefix(\"z: \")\n                        .speed(speed),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut value.w)\n                        .prefix(\"w: \")\n                        .speed(speed),\n                );\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl YoleckAutoEdit for Quat {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        let (mut yaw, mut pitch, mut roll) = value.to_euler(EulerRot::YXZ);\n        yaw = yaw.to_degrees();\n        pitch = pitch.to_degrees();\n        roll = roll.to_degrees();\n\n        ui.horizontal(|ui| {\n            let mut changed = false;\n            changed |= ui\n                .add(\n                    egui::DragValue::new(&mut yaw)\n                        .prefix(\"yaw: \")\n                        .speed(1.0)\n                        .suffix(\"°\"),\n                )\n                .changed();\n            changed |= ui\n                .add(\n                    egui::DragValue::new(&mut pitch)\n                        .prefix(\"pitch: \")\n                        .speed(1.0)\n                        .suffix(\"°\"),\n                )\n                .changed();\n            changed |= ui\n                .add(\n                    egui::DragValue::new(&mut roll)\n                        .prefix(\"roll: \")\n                        .speed(1.0)\n                        .suffix(\"°\"),\n                )\n                .changed();\n\n            if changed {\n                *value = Quat::from_euler(\n                    EulerRot::YXZ,\n                    yaw.to_radians(),\n                    pitch.to_radians(),\n                    roll.to_radians(),\n                );\n            }\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let speed = attrs.speed.unwrap_or(1.0) as f32;\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                let (mut yaw, mut pitch, mut roll) = value.to_euler(EulerRot::YXZ);\n                yaw = yaw.to_degrees();\n                pitch = pitch.to_degrees();\n                roll = roll.to_degrees();\n\n                let mut changed = false;\n                changed |= ui\n                    .add(\n                        egui::DragValue::new(&mut yaw)\n                            .prefix(\"yaw: \")\n                            .speed(speed)\n                            .suffix(\"°\"),\n                    )\n                    .changed();\n                changed |= ui\n                    .add(\n                        egui::DragValue::new(&mut pitch)\n                            .prefix(\"pitch: \")\n                            .speed(speed)\n                            .suffix(\"°\"),\n                    )\n                    .changed();\n                changed |= ui\n                    .add(\n                        egui::DragValue::new(&mut roll)\n                            .prefix(\"roll: \")\n                            .speed(speed)\n                            .suffix(\"°\"),\n                    )\n                    .changed();\n\n                if changed {\n                    *value = Quat::from_euler(\n                        EulerRot::YXZ,\n                        yaw.to_radians(),\n                        pitch.to_radians(),\n                        roll.to_radians(),\n                    );\n                }\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl YoleckAutoEdit for Color {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        let srgba = value.to_srgba();\n        let mut color_arr = [srgba.red, srgba.green, srgba.blue, srgba.alpha];\n        if ui\n            .color_edit_button_rgba_unmultiplied(&mut color_arr)\n            .changed()\n        {\n            *value = Color::srgba(color_arr[0], color_arr[1], color_arr[2], color_arr[3]);\n        }\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                let srgba = value.to_srgba();\n                let mut color_arr = [srgba.red, srgba.green, srgba.blue, srgba.alpha];\n                if ui\n                    .color_edit_button_rgba_unmultiplied(&mut color_arr)\n                    .changed()\n                {\n                    *value = Color::srgba(color_arr[0], color_arr[1], color_arr[2], color_arr[3]);\n                }\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl<T: YoleckAutoEdit + Default> YoleckAutoEdit for Option<T> {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.horizontal(|ui| {\n            let mut has_value = value.is_some();\n            if ui.checkbox(&mut has_value, \"\").changed() {\n                if has_value {\n                    *value = Some(T::default());\n                } else {\n                    *value = None;\n                }\n            }\n            if let Some(inner) = value.as_mut() {\n                T::auto_edit(inner, ui);\n            }\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n                let mut has_value = value.is_some();\n                if ui.checkbox(&mut has_value, \"\").changed() {\n                    if has_value {\n                        *value = Some(T::default());\n                    } else {\n                        *value = None;\n                    }\n                }\n                if let Some(inner) = value.as_mut() {\n                    T::auto_edit(inner, ui);\n                }\n            })\n            .response;\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl<T: YoleckAutoEdit + Default> YoleckAutoEdit for Vec<T> {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        let mut to_remove = None;\n        for (idx, item) in value.iter_mut().enumerate() {\n            ui.horizontal(|ui| {\n                ui.label(format!(\"[{}]\", idx));\n                T::auto_edit(item, ui);\n                if ui.small_button(\"−\").clicked() {\n                    to_remove = Some(idx);\n                }\n            });\n        }\n        if let Some(idx) = to_remove {\n            value.remove(idx);\n        }\n        if ui.small_button(\"+\").clicked() {\n            value.push(T::default());\n        }\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        let response = ui.collapsing(label, |ui| {\n            let mut to_remove = None;\n            for (idx, item) in value.iter_mut().enumerate() {\n                ui.horizontal(|ui| {\n                    ui.label(format!(\"[{}]\", idx));\n                    T::auto_edit(item, ui);\n                    if ui.small_button(\"−\").clicked() {\n                        to_remove = Some(idx);\n                    }\n                });\n            }\n            if let Some(idx) = to_remove {\n                value.remove(idx);\n            }\n            if ui.small_button(\"+\").clicked() {\n                value.push(T::default());\n            }\n        });\n\n        if let Some(tooltip) = &attrs.tooltip {\n            response.header_response.on_hover_text(tooltip);\n        }\n    }\n}\n\nimpl<T: YoleckAutoEdit> YoleckAutoEdit for [T] {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        for (idx, item) in value.iter_mut().enumerate() {\n            ui.horizontal(|ui| {\n                ui.label(format!(\"[{}]\", idx));\n                T::auto_edit(item, ui);\n            });\n        }\n    }\n}\n\n#[derive(Clone)]\nstruct EntityRefDisplayInfo {\n    pub type_name: String,\n    pub name: String,\n}\n\nimpl YoleckAutoEdit for YoleckEntityRef {\n    fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {\n        ui.horizontal(|ui| {\n            if let Some(uuid) = value.uuid() {\n                ui.label(uuid.to_string());\n                if ui.small_button(\"✕\").clicked() {\n                    value.clear();\n                }\n            } else {\n                ui.label(\"None\");\n            }\n        });\n    }\n\n    fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {\n        // Get entity info map once for both display and drag&drop validation\n        let entity_info_map = ui.ctx().data(|data| {\n            data.get_temp::<HashMap<uuid::Uuid, EntityRefDisplayInfo>>(egui::Id::new(\n                \"yoleck_entity_ref_display_info\",\n            ))\n        });\n\n        let response = ui\n            .horizontal(|ui| {\n                ui.label(label);\n\n                let display_text = if let Some(uuid) = value.uuid() {\n                    if let Some(ref info_map) = entity_info_map {\n                        if let Some(info) = info_map.get(&uuid) {\n                            if info.name.is_empty() {\n                                let uuid_str = uuid.to_string();\n                                let uuid_short = &uuid_str[..uuid_str.len().min(8)];\n                                format!(\"{} ({})\", info.type_name, uuid_short)\n                            } else {\n                                format!(\"{} - {}\", info.type_name, info.name)\n                            }\n                        } else {\n                            uuid.to_string()\n                        }\n                    } else {\n                        uuid.to_string()\n                    }\n                } else {\n                    \"None\".to_string()\n                };\n\n                ui.add(\n                    egui::Button::new(\n                        egui::RichText::new(display_text)\n                            .text_style(ui.style().drag_value_text_style.clone()),\n                    )\n                    .wrap_mode(egui::TextWrapMode::Extend)\n                    .min_size(ui.spacing().interact_size),\n                );\n\n                if value.is_some() && ui.small_button(\"✕\").clicked() {\n                    value.clear();\n                }\n\n                if let Some(tooltip) = &attrs.tooltip {\n                    ui.label(\"ⓘ\").on_hover_text(tooltip);\n                }\n            })\n            .response;\n\n        // Handle drag & drop\n        if let Some(dropped_uuid) = response.dnd_release_payload::<uuid::Uuid>() {\n            let dropped_uuid = *dropped_uuid;\n\n            let should_accept = if let Some(filter) = &attrs.entity_filter {\n                entity_info_map\n                    .as_ref()\n                    .and_then(|map| map.get(&dropped_uuid))\n                    .is_none_or(|info| &info.type_name == filter)\n            } else {\n                true\n            };\n\n            if should_accept {\n                value.set(dropped_uuid);\n            }\n        }\n    }\n}\n\nuse crate::YoleckExtForApp;\nuse crate::editing::{YoleckEdit, YoleckUi};\nuse crate::specs_registration::YoleckComponent;\n\nuse crate::entity_ref::YoleckEntityRefAccessor;\nuse bevy::ecs::component::Mutable;\n\nuse crate::YoleckManaged;\nuse crate::entity_uuid::YoleckEntityUuid;\n\npub fn auto_edit_system<T: YoleckComponent + YoleckAutoEdit + YoleckEntityRefAccessor>(\n    mut ui: ResMut<YoleckUi>,\n    mut edit: YoleckEdit<&mut T>,\n    entities_query: Query<(&YoleckEntityUuid, &YoleckManaged)>,\n    registry: Res<YoleckUuidRegistry>,\n) {\n    let Ok(mut component) = edit.single_mut() else {\n        return;\n    };\n\n    // Populate entity display info in egui context only if component has entity ref fields\n    if !T::entity_ref_fields().is_empty() {\n        let entity_count = entities_query.iter().len();\n        let mut entity_info_map = HashMap::with_capacity(entity_count);\n\n        for (entity_uuid, managed) in entities_query.iter() {\n            entity_info_map.insert(\n                entity_uuid.get(),\n                EntityRefDisplayInfo {\n                    type_name: managed.type_name.clone(),\n                    name: managed.name.clone(),\n                },\n            );\n        }\n\n        ui.ctx().data_mut(|data| {\n            data.insert_temp(\n                egui::Id::new(\"yoleck_entity_ref_display_info\"),\n                entity_info_map,\n            );\n        });\n    }\n\n    ui.group(|ui| {\n        ui.label(egui::RichText::new(T::KEY).strong());\n        ui.separator();\n        T::auto_edit(&mut component, ui);\n    });\n\n    component.resolve_entity_refs(registry.as_ref());\n}\n\npub trait YoleckAutoEditExt {\n    fn add_yoleck_auto_edit<\n        T: Component<Mutability = Mutable>\n            + YoleckComponent\n            + YoleckAutoEdit\n            + YoleckEntityRefAccessor,\n    >(\n        &mut self,\n    );\n}\n\nimpl YoleckAutoEditExt for App {\n    fn add_yoleck_auto_edit<\n        T: Component<Mutability = Mutable>\n            + YoleckComponent\n            + YoleckAutoEdit\n            + YoleckEntityRefAccessor,\n    >(\n        &mut self,\n    ) {\n        self.add_yoleck_edit_system(auto_edit_system::<T>);\n        self.add_systems(\n            YoleckInternalSchedule::PostLoadResolutions,\n            resolve_entity_refs::<T>,\n        );\n\n        let construction_specs = self\n            .world_mut()\n            .get_resource::<crate::YoleckEntityConstructionSpecs>();\n\n        if let Some(specs) = construction_specs {\n            validate_entity_ref_requirements_for::<T>(specs);\n        }\n    }\n}\n"
  },
  {
    "path": "src/console.rs",
    "content": "use bevy::log::BoxedLayer;\nuse bevy::log::tracing;\nuse bevy::log::tracing_subscriber;\nuse bevy::prelude::*;\nuse bevy_egui::egui;\nuse std::collections::VecDeque;\nuse std::sync::mpsc;\n\nuse crate::editor_panels::YoleckPanelUi;\n\n/// Log level for console messages.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum LogLevel {\n    Debug,\n    Info,\n    Warn,\n    Error,\n}\n\nimpl LogLevel {\n    pub fn color(&self) -> egui::Color32 {\n        match self {\n            LogLevel::Debug => egui::Color32::LIGHT_GRAY,\n            LogLevel::Info => egui::Color32::WHITE,\n            LogLevel::Warn => egui::Color32::from_rgb(255, 200, 0),\n            LogLevel::Error => egui::Color32::from_rgb(255, 100, 100),\n        }\n    }\n\n    pub fn label(&self) -> &str {\n        match self {\n            LogLevel::Debug => \"DEBUG\",\n            LogLevel::Info => \"INFO\",\n            LogLevel::Warn => \"WARN\",\n            LogLevel::Error => \"ERROR\",\n        }\n    }\n}\n\n/// A single log entry captured from the tracing system.\n#[derive(Clone, Debug, Message)]\npub struct LogEntry {\n    pub level: LogLevel,\n    pub message: String,\n    pub target: String,\n}\n\n/// Non-send resource containing the receiver for captured log messages.\npub struct CapturedLogMessages(mpsc::Receiver<LogEntry>);\n\n/// Resource storing the history of log messages displayed in the console.\n#[derive(Resource)]\npub struct YoleckConsoleLogHistory {\n    pub logs: VecDeque<LogEntry>,\n    pub max_logs: usize,\n}\n\nimpl YoleckConsoleLogHistory {\n    pub fn new(max_logs: usize) -> Self {\n        Self {\n            logs: VecDeque::with_capacity(max_logs),\n            max_logs,\n        }\n    }\n\n    pub fn add_log(&mut self, entry: LogEntry) {\n        if self.logs.len() >= self.max_logs {\n            self.logs.pop_front();\n        }\n        self.logs.push_back(entry);\n    }\n\n    pub fn clear(&mut self) {\n        self.logs.clear();\n    }\n}\n\nimpl Default for YoleckConsoleLogHistory {\n    fn default() -> Self {\n        Self::new(1000)\n    }\n}\n\n/// Resource containing the current state of the console UI.\n#[derive(Resource, Default)]\npub struct YoleckConsoleState {\n    pub log_filters: LogFilters,\n}\n\n/// Filters for controlling which log levels are displayed in the console.\n#[derive(Resource)]\npub struct LogFilters {\n    pub show_debug: bool,\n    pub show_info: bool,\n    pub show_warn: bool,\n    pub show_error: bool,\n}\n\nimpl Default for LogFilters {\n    fn default() -> Self {\n        Self {\n            show_debug: false,\n            show_info: true,\n            show_warn: true,\n            show_error: true,\n        }\n    }\n}\n\nimpl LogFilters {\n    pub fn should_show(&self, level: LogLevel) -> bool {\n        match level {\n            LogLevel::Debug => self.show_debug,\n            LogLevel::Info => self.show_info,\n            LogLevel::Warn => self.show_warn,\n            LogLevel::Error => self.show_error,\n        }\n    }\n}\n\n/// Creates a console panel section for displaying log messages in the editor UI.\npub fn console_panel_section(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut console_state: ResMut<YoleckConsoleState>,\n    mut log_history: ResMut<YoleckConsoleLogHistory>,\n) -> Result {\n    ui.horizontal(|ui| {\n        ui.label(\"Filters:\");\n\n        ui.checkbox(&mut console_state.log_filters.show_debug, \"DEBUG\");\n        ui.checkbox(&mut console_state.log_filters.show_info, \"INFO\");\n        ui.checkbox(&mut console_state.log_filters.show_warn, \"WARN\");\n        ui.checkbox(&mut console_state.log_filters.show_error, \"ERROR\");\n\n        ui.separator();\n\n        if ui.button(\"Clear\").clicked() {\n            log_history.clear();\n        }\n    });\n\n    ui.separator();\n\n    egui::ScrollArea::vertical()\n        .auto_shrink([false, false])\n        .stick_to_bottom(true)\n        .show(&mut ui, |ui| {\n            for log in log_history\n                .logs\n                .iter()\n                .filter(|log| console_state.log_filters.should_show(log.level))\n            {\n                ui.horizontal_wrapped(|ui| {\n                    ui.colored_label(log.level.color(), format!(\"[{}]\", log.level.label()));\n                    ui.label(&log.message);\n                });\n            }\n        });\n\n    Ok(())\n}\n\n/// Tracing layer that captures log messages and sends them to the console.\npub struct YoleckConsoleLayer {\n    sender: mpsc::Sender<LogEntry>,\n}\n\nimpl YoleckConsoleLayer {\n    pub fn new(sender: mpsc::Sender<LogEntry>) -> Self {\n        Self { sender }\n    }\n}\n\nimpl<S> tracing_subscriber::Layer<S> for YoleckConsoleLayer\nwhere\n    S: tracing::Subscriber,\n{\n    fn on_event(\n        &self,\n        event: &tracing::Event<'_>,\n        _ctx: tracing_subscriber::layer::Context<'_, S>,\n    ) {\n        let metadata = event.metadata();\n        let level = match *metadata.level() {\n            tracing::Level::TRACE => return,\n            tracing::Level::DEBUG => LogLevel::Debug,\n            tracing::Level::INFO => LogLevel::Info,\n            tracing::Level::WARN => LogLevel::Warn,\n            tracing::Level::ERROR => LogLevel::Error,\n        };\n\n        let mut visitor = MessageVisitor::default();\n        event.record(&mut visitor);\n\n        if let Some(message) = visitor.message {\n            let _ = self.sender.send(LogEntry {\n                level,\n                message,\n                target: metadata.target().to_string(),\n            });\n        }\n    }\n}\n\n#[derive(Default)]\nstruct MessageVisitor {\n    message: Option<String>,\n}\n\nimpl tracing::field::Visit for MessageVisitor {\n    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {\n        if field.name() == \"message\" {\n            self.message = Some(format!(\"{:?}\", value).trim_matches('\"').to_string());\n        }\n    }\n}\n\nfn transfer_log_messages(\n    receiver: NonSend<CapturedLogMessages>,\n    mut message_writer: MessageWriter<LogEntry>,\n) {\n    message_writer.write_batch(receiver.0.try_iter());\n}\n\nfn store_log_messages(\n    mut log_reader: MessageReader<LogEntry>,\n    log_history: Option<ResMut<YoleckConsoleLogHistory>>,\n) {\n    let Some(mut log_history) = log_history else {\n        return;\n    };\n    for log in log_reader.read() {\n        log_history.add_log(log.clone());\n    }\n}\n\n/// Factory function that creates and configures the console logging layer.\n///\n/// This function should be used with Bevy's `LogPlugin` to capture log messages\n/// and display them in the Yoleck editor console.\n///\n/// # Example\n///\n/// ```no_run\n/// # use bevy::{prelude::*, log::LogPlugin};\n/// # use bevy_yoleck::console_layer_factory;\n///\n/// fn main() {\n///     App::new()\n///         .add_plugins(DefaultPlugins.set(LogPlugin {\n///             custom_layer: console_layer_factory,\n///             ..default()\n///         }))\n///         .run();\n/// }\n/// ```\npub fn console_layer_factory(app: &mut App) -> Option<BoxedLayer> {\n    let (sender, receiver) = mpsc::channel();\n\n    let layer = YoleckConsoleLayer::new(sender);\n    let resource = CapturedLogMessages(receiver);\n\n    app.insert_non_send_resource(resource);\n    app.add_message::<LogEntry>();\n    app.add_systems(Update, (transfer_log_messages, store_log_messages).chain());\n\n    Some(Box::new(layer))\n}\n"
  },
  {
    "path": "src/editing.rs",
    "content": "use std::ops::{Deref, DerefMut};\n\nuse bevy::ecs::query::{QueryData, QueryFilter, QueryIter, QuerySingleError};\nuse bevy::ecs::system::SystemParam;\nuse bevy::prelude::*;\nuse bevy_egui::egui;\n\n/// Marks which entities are currently being edited in the level editor.\n#[derive(Component)]\npub struct YoleckEditMarker;\n\n/// Wrapper for writing queries in edit systems.\n///\n/// To future-proof for the multi-entity editing feature, use this instead of\n/// regular queries with `With<YoleckEditMarker>`.\n///\n/// The methods of `YoleckEdit` that have the same name as methods of a regular Bevy `Query`\n/// delegate to them, but if there are edited entities that do not fit the query they will act as\n/// if they found no match.\n#[derive(SystemParam)]\npub struct YoleckEdit<'w, 's, Q: 'static + QueryData, F: 'static + QueryFilter = ()> {\n    query: Query<'w, 's, Q, (With<YoleckEditMarker>, F)>,\n    verification_query: Query<'w, 's, (), With<YoleckEditMarker>>,\n}\n\nimpl<'s, Q: 'static + QueryData, F: 'static + QueryFilter> YoleckEdit<'_, 's, Q, F> {\n    pub fn single(\n        &self,\n    ) -> Result<<<Q as QueryData>::ReadOnly as QueryData>::Item<'_, 's>, QuerySingleError> {\n        let single = self.query.single()?;\n        // This will return an error if multiple entities are selected (but only one fits F and Q)\n        self.verification_query.single()?;\n        Ok(single)\n    }\n\n    pub fn single_mut(&mut self) -> Result<<Q as QueryData>::Item<'_, 's>, QuerySingleError> {\n        let single = self.query.single_mut()?;\n        // This will return an error if multiple entities are selected (but only one fits F and Q)\n        self.verification_query.single()?;\n        Ok(single)\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.query.is_empty()\n    }\n\n    /// Check if some non-matching entities are selected for editing.\n    ///\n    /// Use this, together with [`is_empty`](Self::is_empty) for systems that can edit multiple\n    /// entities but want to not show their UI when some irrelevant entities are selected as well.\n    pub fn has_nonmatching(&self) -> bool {\n        // Note - cannot use len for query.iter() because then `F` would be limited to archetype\n        // filters only.\n        self.query.iter().count() != self.verification_query.iter().len()\n    }\n\n    /// Iterate over all the matching entities, _even_ if some selected entities do not match.\n    ///\n    /// If both matching and non-matching entities are selected, this will iterate over the\n    /// matching entities only. If it is not desired to iterate at all in such cases,\n    /// check [`has_nonmatching`](Self::has_nonmatching) must be checked manually.\n    pub fn iter_matching(\n        &mut self,\n    ) -> QueryIter<'_, '_, <Q as QueryData>::ReadOnly, (bevy::prelude::With<YoleckEditMarker>, F)>\n    {\n        self.query.iter()\n    }\n\n    /// Iterate mutably over all the matching entities, _even_ if some selected entities do not match.\n    ///\n    /// If both matching and non-matching entities are selected, this will iterate over the\n    /// matching entities only. If it is not desired to iterate at all in such cases,\n    /// check [`has_nonmatching`](Self::has_nonmatching) must be checked manually.\n    pub fn iter_matching_mut(&mut self) -> QueryIter<'_, '_, Q, (With<YoleckEditMarker>, F)> {\n        self.query.iter_mut()\n    }\n}\n\n/// An handle for the egui UI frame used in editing systems.\n#[derive(Resource)]\npub struct YoleckUi(pub egui::Ui);\n\nimpl Deref for YoleckUi {\n    type Target = egui::Ui;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl DerefMut for YoleckUi {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        &mut self.0\n    }\n}\n"
  },
  {
    "path": "src/editor.rs",
    "content": "use std::any::TypeId;\nuse std::borrow::Cow;\nuse std::sync::Arc;\n\nuse bevy::ecs::system::SystemState;\nuse bevy::platform::collections::{HashMap, HashSet};\nuse bevy::prelude::*;\nuse bevy::state::state::FreelyMutableState;\nuse bevy_egui::egui;\n\nuse crate::editor_panels::YoleckPanelUi;\nuse crate::entity_management::{YoleckEntryHeader, YoleckRawEntry};\nuse crate::entity_uuid::YoleckEntityUuid;\nuse crate::exclusive_systems::{\n    YoleckActiveExclusiveSystem, YoleckEntityCreationExclusiveSystems,\n    YoleckExclusiveSystemDirective, YoleckExclusiveSystemsQueue,\n};\nuse crate::knobs::YoleckKnobsCache;\nuse crate::prelude::{YoleckComponent, YoleckUi};\n#[cfg(feature = \"vpeol\")]\nuse crate::vpeol;\nuse crate::{\n    BoxedArc, YoleckBelongsToLevel, YoleckEditMarker, YoleckEditSystems,\n    YoleckEntityConstructionSpecs, YoleckInternalSchedule, YoleckManaged, YoleckState,\n};\n\n/// Whether or not the Yoleck editor is active.\n#[derive(States, Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]\npub enum YoleckEditorState {\n    /// Editor mode. The editor is active and can be used to edit entities.\n    #[default]\n    EditorActive,\n    /// Game mode. Either the actual game or playtest from the editor mode.\n    GameActive,\n}\n\n/// Sync the game's state back and forth when the level editor enters and exits playtest mode.\n///\n/// Add this as a plugin. When using it, there is no need to initialize the state with `add_state`\n/// because `YoleckSyncWithEditorState` will initialize it and set its initial value to\n/// `when_editor`. This means that the state's default value should be it's initial value for\n/// non-editor mode (which is not necessarily `when_game`, because the game may start in a menu\n/// state or a loading state)\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # use bevy_yoleck::bevy_egui::EguiPlugin;\n/// #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]\n/// enum GameState {\n///     #[default]\n///     Loading,\n///     Game,\n///     Editor,\n/// }\n///\n/// # let mut app = App::new();\n/// # let executable_started_in_editor_mode = true;\n/// if executable_started_in_editor_mode {\n///     // These two plugins are needed for editor mode:\n///     app.add_plugins(EguiPlugin::default());\n///     app.add_plugins(YoleckPluginForEditor);\n///     app.add_plugins(YoleckSyncWithEditorState {\n///         when_editor: GameState::Editor,\n///         when_game: GameState::Game,\n///     });\n/// } else {\n///     // This plugin is needed for game mode:\n///     app.add_plugins(YoleckPluginForGame);\n///\n///     app.init_state::<GameState>();\n/// }\npub struct YoleckSyncWithEditorState<T>\nwhere\n    T: 'static\n        + States\n        + FreelyMutableState\n        + Sync\n        + Send\n        + std::fmt::Debug\n        + Clone\n        + std::cmp::Eq\n        + std::hash::Hash,\n{\n    pub when_editor: T,\n    pub when_game: T,\n}\n\nimpl<T> Plugin for YoleckSyncWithEditorState<T>\nwhere\n    T: 'static\n        + States\n        + FreelyMutableState\n        + Sync\n        + Send\n        + std::fmt::Debug\n        + Clone\n        + std::cmp::Eq\n        + std::hash::Hash,\n{\n    fn build(&self, app: &mut App) {\n        app.insert_state(self.when_editor.clone());\n        let when_editor = self.when_editor.clone();\n        let when_game = self.when_game.clone();\n        app.add_systems(\n            Update,\n            move |editor_state: Res<State<YoleckEditorState>>,\n                  mut game_state: ResMut<NextState<T>>| {\n                game_state.set(match editor_state.get() {\n                    YoleckEditorState::EditorActive => when_editor.clone(),\n                    YoleckEditorState::GameActive => when_game.clone(),\n                });\n            },\n        );\n    }\n}\n\n/// Events emitted by the Yoleck editor.\n///\n/// Modules that provide editing overlays over the viewport (like [vpeol](crate::vpeol)) can\n/// use these events to update their status to match with the editor.\n#[derive(Debug, Message)]\npub enum YoleckEditorEvent {\n    EntitySelected(Entity),\n    EntityDeselected(Entity),\n    EditedEntityPopulated(Entity),\n}\n\nenum YoleckDirectiveInner {\n    SetSelected(Option<Entity>),\n    ChangeSelectedStatus {\n        entity: Entity,\n        force_to: Option<bool>,\n    },\n    PassToEntity(Entity, TypeId, BoxedArc),\n    SpawnEntity {\n        level: Entity,\n        type_name: String,\n        data: serde_json::Map<String, serde_json::Value>,\n        select_created_entity: bool,\n        #[allow(clippy::type_complexity)]\n        modify_exclusive_systems:\n            Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,\n    },\n}\n\n/// Event that can be sent to control Yoleck's editor.\n#[derive(Message)]\npub struct YoleckDirective(YoleckDirectiveInner);\n\nimpl YoleckDirective {\n    /// Pass data from an external system (usually a [ViewPort Editing OverLay](crate::vpeol)) to an entity.\n    ///\n    /// This data can be received using the [`YoleckPassedData`] resource. If the data is\n    /// passed to a knob, it can also be received using the knob handle's\n    /// [`get_passed_data`](crate::knobs::YoleckKnobHandle::get_passed_data) method.\n    pub fn pass_to_entity<T: 'static + Send + Sync>(entity: Entity, data: T) -> Self {\n        Self(YoleckDirectiveInner::PassToEntity(\n            entity,\n            TypeId::of::<T>(),\n            Arc::new(data),\n        ))\n    }\n\n    /// Set the entity selected in the Yoleck editor.\n    pub fn set_selected(entity: Option<Entity>) -> Self {\n        Self(YoleckDirectiveInner::SetSelected(entity))\n    }\n\n    /// Set the entity selected in the Yoleck editor.\n    pub fn toggle_selected(entity: Entity) -> Self {\n        Self(YoleckDirectiveInner::ChangeSelectedStatus {\n            entity,\n            force_to: None,\n        })\n    }\n\n    /// Spawn a new entity with pre-populated data.\n    ///\n    /// ```no_run\n    /// # use serde::{Deserialize, Serialize};\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;\n    /// # use bevy_yoleck::YoleckDirective;\n    /// # use bevy_yoleck::vpeol_2d::Vpeol2dPosition;\n    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n    /// # struct Example;\n    /// fn duplicate_example(\n    ///     mut ui: ResMut<YoleckUi>,\n    ///     mut edit: YoleckEdit<(&YoleckBelongsToLevel, &Vpeol2dPosition), With<Example>>,\n    ///     mut writer: MessageWriter<YoleckDirective>,\n    /// ) {\n    ///     let Ok((belongs_to_level, position)) = edit.single() else { return };\n    ///     if ui.button(\"Duplicate\").clicked() {\n    ///         writer.write(\n    ///             YoleckDirective::spawn_entity(\n    ///                 belongs_to_level.level,\n    ///                 \"Example\",\n    ///                 // Automatically select the newly created entity:\n    ///                 true,\n    ///             )\n    ///             // Create the new example entity 100 units below the current one:\n    ///             .with(Vpeol2dPosition(position.0 - 100.0 * Vec2::Y))\n    ///             .into(),\n    ///         );\n    ///     }\n    /// }\n    /// ```\n    pub fn spawn_entity(\n        level: Entity,\n        type_name: impl ToString,\n        select_created_entity: bool,\n    ) -> SpawnEntityBuilder {\n        SpawnEntityBuilder {\n            level,\n            type_name: type_name.to_string(),\n            select_created_entity,\n            data: Default::default(),\n            modify_exclusive_systems: None,\n        }\n    }\n}\n\npub struct SpawnEntityBuilder {\n    level: Entity,\n    type_name: String,\n    select_created_entity: bool,\n    data: HashMap<Cow<'static, str>, serde_json::Value>,\n    #[allow(clippy::type_complexity)]\n    modify_exclusive_systems: Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,\n}\n\nimpl SpawnEntityBuilder {\n    /// Override a component of the spawned entity.\n    pub fn with<T: YoleckComponent>(self, component: T) -> Self {\n        self.with_raw(\n            T::KEY,\n            serde_json::to_value(component).expect(\"should always work\"),\n        )\n    }\n\n    pub fn with_raw(\n        mut self,\n        component_name: impl Into<Cow<'static, str>>,\n        component_data: serde_json::Value,\n    ) -> Self {\n        self.data.insert(component_name.into(), component_data);\n        self\n    }\n\n    pub fn extend(\n        mut self,\n        components: impl Iterator<Item = (impl Into<Cow<'static, str>>, serde_json::Value)>,\n    ) -> Self {\n        for (component_name, component_data) in components.into_iter() {\n            self = self.with_raw(component_name, component_data);\n        }\n        self\n    }\n\n    /// Change the exclusive systems that will be running the entity is spawned.\n    pub fn modify_exclusive_systems(\n        mut self,\n        dlg: impl 'static + Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue),\n    ) -> Self {\n        self.modify_exclusive_systems = Some(Box::new(dlg));\n        self\n    }\n}\n\nimpl From<SpawnEntityBuilder> for YoleckDirective {\n    fn from(value: SpawnEntityBuilder) -> Self {\n        YoleckDirective(YoleckDirectiveInner::SpawnEntity {\n            level: value.level,\n            type_name: value.type_name,\n            data: value\n                .data\n                .into_iter()\n                .map(|(k, v)| (k.into_owned(), v))\n                .collect(),\n            select_created_entity: value.select_created_entity,\n            modify_exclusive_systems: value.modify_exclusive_systems,\n        })\n    }\n}\n\n#[derive(Resource)]\npub struct YoleckPassedData(pub(crate) HashMap<Entity, HashMap<TypeId, BoxedArc>>);\n\nimpl YoleckPassedData {\n    /// Get data sent to an entity from external systems (usually from (usually a [ViewPort Editing\n    /// OverLay](crate::vpeol))\n    ///\n    /// The data is sent using [a directive event](crate::YoleckDirective::pass_to_entity).\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;;\n    /// # #[derive(Component)]\n    /// # struct Example {\n    /// #     message: String,\n    /// # }\n    /// fn edit_example(\n    ///     mut edit: YoleckEdit<(Entity, &mut Example)>,\n    ///     passed_data: Res<YoleckPassedData>,\n    /// ) {\n    ///     let Ok((entity, mut example)) = edit.single_mut() else { return };\n    ///     if let Some(message) = passed_data.get::<String>(entity) {\n    ///         example.message = message.clone();\n    ///     }\n    /// }\n    /// ```\n    pub fn get<T: 'static>(&self, entity: Entity) -> Option<&T> {\n        Some(\n            self.0\n                .get(&entity)?\n                .get(&TypeId::of::<T>())?\n                .downcast_ref()\n                .expect(\"Passed data TypeId must be correct\"),\n        )\n    }\n}\n\nfn format_caption(entity: Entity, yoleck_managed: &YoleckManaged) -> String {\n    if yoleck_managed.name.is_empty() {\n        format!(\"{} {:?}\", yoleck_managed.type_name, entity)\n    } else {\n        format!(\n            \"{} ({} {:?})\",\n            yoleck_managed.name, yoleck_managed.type_name, entity\n        )\n    }\n}\n\n/// The UI part for creating new entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).\npub fn new_entity_section(\n    mut ui: ResMut<YoleckPanelUi>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    yoleck: Res<YoleckState>,\n    editor_state: Res<State<YoleckEditorState>>,\n    mut writer: MessageWriter<YoleckDirective>,\n    active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,\n) -> Result {\n    if active_exclusive_system.is_some() {\n        return Ok(());\n    }\n\n    if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {\n        return Ok(());\n    }\n\n    let button_response = ui.button(\"Add New Entity\");\n\n    egui::Popup::menu(&button_response).show(|ui| {\n        for entity_type in construction_specs.entity_types.iter() {\n            if ui.button(&entity_type.name).clicked() {\n                writer.write(YoleckDirective(YoleckDirectiveInner::SpawnEntity {\n                    level: yoleck.level_being_edited,\n                    type_name: entity_type.name.clone(),\n                    data: Default::default(),\n                    select_created_entity: true,\n                    modify_exclusive_systems: None,\n                }));\n            }\n        }\n    });\n    Ok(())\n}\n\n/// The UI part for selecting entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).\n#[allow(clippy::too_many_arguments)]\npub fn entity_selection_section(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut filter_custom_name: Local<String>,\n    mut filter_types: Local<HashSet<String>>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    yoleck_managed_query: Query<(\n        Entity,\n        &YoleckManaged,\n        Option<&YoleckEditMarker>,\n        Option<&YoleckEntityUuid>,\n    )>,\n    editor_state: Res<State<YoleckEditorState>>,\n    mut writer: MessageWriter<YoleckDirective>,\n    active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,\n    #[cfg(feature = \"vpeol\")] vpeol_camera_state_query: Query<&vpeol::VpeolCameraState>,\n) -> Result {\n    if active_exclusive_system.is_some() {\n        return Ok(());\n    }\n\n    if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {\n        return Ok(());\n    }\n\n    egui::CollapsingHeader::new(\"Filter\").show(ui.as_mut(), |ui| {\n        ui.horizontal(|ui| {\n            ui.label(\"By Name:\");\n            ui.text_edit_singleline(&mut *filter_custom_name);\n        });\n        for entity_type in construction_specs.entity_types.iter() {\n            let mut should_show = filter_types.contains(&entity_type.name);\n            if ui.checkbox(&mut should_show, &entity_type.name).changed() {\n                if should_show {\n                    filter_types.insert(entity_type.name.clone());\n                } else {\n                    filter_types.remove(&entity_type.name);\n                }\n            }\n        }\n    });\n\n    #[cfg(not(feature = \"vpeol\"))]\n    let entities_under_cursor: HashSet<Entity> = Default::default();\n    #[cfg(feature = \"vpeol\")]\n    let entities_under_cursor: HashSet<Entity> = vpeol_camera_state_query\n        .iter()\n        .filter_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))\n        .collect();\n\n    for (entity, yoleck_managed, edit_marker, entity_uuid) in yoleck_managed_query.iter() {\n        if !filter_types.is_empty() && !filter_types.contains(&yoleck_managed.type_name) {\n            continue;\n        }\n        if !yoleck_managed.name.contains(filter_custom_name.as_str()) {\n            continue;\n        }\n        let is_selected = edit_marker.is_some();\n\n        let caption = format_caption(entity, yoleck_managed);\n        let mut potentially_highlighted_caption = egui::RichText::new(&caption);\n        if entities_under_cursor.contains(&entity) {\n            potentially_highlighted_caption =\n                potentially_highlighted_caption.background_color(egui::Color32::DARK_RED);\n        }\n\n        if let Some(entity_uuid) = entity_uuid {\n            let uuid = entity_uuid.get();\n            let sense = egui::Sense::click_and_drag();\n            let response = ui\n                .selectable_label(is_selected, potentially_highlighted_caption)\n                .interact(sense);\n\n            if response.drag_started() {\n                egui::DragAndDrop::set_payload(ui.ctx(), uuid);\n            } else if response.clicked() {\n                if ui.input(|input| input.modifiers.shift) {\n                    writer.write(YoleckDirective::toggle_selected(entity));\n                } else if is_selected {\n                    writer.write(YoleckDirective::set_selected(None));\n                } else {\n                    writer.write(YoleckDirective::set_selected(Some(entity)));\n                }\n            }\n\n            if response.dragged() {\n                ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);\n\n                if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {\n                    egui::Area::new(egui::Id::new(\"dragged_entity_preview\"))\n                        .fixed_pos(pointer_pos + egui::vec2(10.0, 10.0))\n                        .order(egui::Order::Tooltip)\n                        .show(ui.ctx(), |ui| {\n                            egui::Frame::popup(ui.style()).show(ui, |ui| {\n                                ui.label(caption);\n                            });\n                        });\n                }\n            }\n        } else if ui\n            .selectable_label(is_selected, potentially_highlighted_caption)\n            .clicked()\n        {\n            if ui.input(|input| input.modifiers.shift) {\n                writer.write(YoleckDirective::toggle_selected(entity));\n            } else if is_selected {\n                writer.write(YoleckDirective::set_selected(None));\n            } else {\n                writer.write(YoleckDirective::set_selected(Some(entity)));\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// The UI part for editing entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).\n#[allow(clippy::type_complexity)]\npub fn entity_editing_section(\n    world: &mut World,\n    mut previously_edited_entity: Local<Option<Entity>>,\n    mut new_entity_created_this_frame: Local<bool>,\n    mut system_state: Local<\n        Option<\n            SystemState<(\n                ResMut<YoleckState>,\n                Query<(Entity, &mut YoleckManaged), With<YoleckEditMarker>>,\n                Query<Entity, With<YoleckEditMarker>>,\n                MessageReader<YoleckDirective>,\n                Commands,\n                Res<State<YoleckEditorState>>,\n                MessageWriter<YoleckEditorEvent>,\n                ResMut<YoleckKnobsCache>,\n                Option<Res<YoleckActiveExclusiveSystem>>,\n                ResMut<YoleckExclusiveSystemsQueue>,\n                Res<YoleckEntityCreationExclusiveSystems>,\n            )>,\n        >,\n    >,\n) -> Result {\n    let system_state = system_state.get_or_insert_with(|| SystemState::new(world));\n\n    world.resource_scope(|world, mut ui: Mut<YoleckPanelUi>| {\n        let ui = &mut **ui;\n        let mut passed_data = YoleckPassedData(Default::default());\n        {\n            let (\n                mut yoleck,\n                mut yoleck_managed_query,\n                yoleck_edited_query,\n                mut directives_reader,\n                mut commands,\n                editor_state,\n                mut writer,\n                mut knobs_cache,\n                active_exclusive_system,\n                mut exclusive_systems_queue,\n                entity_creation_exclusive_systems,\n            ) = system_state.get_mut(world);\n\n            if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {\n                return Ok(());\n            }\n\n            let mut data_passed_to_entities: HashMap<Entity, HashMap<TypeId, BoxedArc>> =\n                Default::default();\n            for directive in directives_reader.read() {\n                match &directive.0 {\n                    YoleckDirectiveInner::PassToEntity(entity, type_id, data) => {\n                        if false {\n                            data_passed_to_entities\n                                .entry(*entity)\n                                .or_default()\n                                .insert(*type_id, data.clone());\n                        }\n                        passed_data\n                            .0\n                            .entry(*entity)\n                            .or_default()\n                            .insert(*type_id, data.clone());\n                    }\n                    YoleckDirectiveInner::SetSelected(entity) => {\n                        if active_exclusive_system.is_some() {\n                            // TODO: pass the selection command to the exclusive system?\n                            continue;\n                        }\n                        if let Some(entity) = entity {\n                            let mut already_selected = false;\n                            for entity_to_deselect in yoleck_edited_query.iter() {\n                                if entity_to_deselect == *entity {\n                                    already_selected = true;\n                                } else {\n                                    commands\n                                        .entity(entity_to_deselect)\n                                        .remove::<YoleckEditMarker>();\n                                    writer.write(YoleckEditorEvent::EntityDeselected(\n                                        entity_to_deselect,\n                                    ));\n                                }\n                            }\n                            if !already_selected {\n                                commands.entity(*entity).insert(YoleckEditMarker);\n                                writer.write(YoleckEditorEvent::EntitySelected(*entity));\n                            }\n                        } else {\n                            for entity_to_deselect in yoleck_edited_query.iter() {\n                                commands\n                                    .entity(entity_to_deselect)\n                                    .remove::<YoleckEditMarker>();\n                                writer\n                                    .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));\n                            }\n                        }\n                    }\n                    YoleckDirectiveInner::ChangeSelectedStatus { entity, force_to } => {\n                        if active_exclusive_system.is_some() {\n                            // TODO: pass the selection command to the exclusive system?\n                            continue;\n                        }\n                        match (force_to, yoleck_edited_query.contains(*entity)) {\n                            (Some(true), true) | (Some(false), false) => {\n                                // Nothing to do\n                            }\n                            (None, false) | (Some(true), false) => {\n                                // Add to selection\n                                commands.entity(*entity).insert(YoleckEditMarker);\n                                writer.write(YoleckEditorEvent::EntitySelected(*entity));\n                            }\n                            (None, true) | (Some(false), true) => {\n                                // Remove from selection\n                                commands.entity(*entity).remove::<YoleckEditMarker>();\n                                writer.write(YoleckEditorEvent::EntityDeselected(*entity));\n                            }\n                        }\n                    }\n                    YoleckDirectiveInner::SpawnEntity {\n                        level,\n                        type_name,\n                        data,\n                        select_created_entity,\n                        modify_exclusive_systems: override_exclusive_systems,\n                    } => {\n                        if active_exclusive_system.is_some() {\n                            continue;\n                        }\n                        let mut cmd = commands.spawn((\n                            YoleckRawEntry {\n                                header: YoleckEntryHeader {\n                                    type_name: type_name.clone(),\n                                    name: \"\".to_owned(),\n                                    uuid: None,\n                                },\n                                data: data.clone(),\n                            },\n                            YoleckBelongsToLevel { level: *level },\n                        ));\n                        if *select_created_entity {\n                            writer.write(YoleckEditorEvent::EntitySelected(cmd.id()));\n                            cmd.insert(YoleckEditMarker);\n                            for entity_to_deselect in yoleck_edited_query.iter() {\n                                commands\n                                    .entity(entity_to_deselect)\n                                    .remove::<YoleckEditMarker>();\n                                writer\n                                    .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));\n                            }\n                            *exclusive_systems_queue =\n                                entity_creation_exclusive_systems.create_queue();\n                            if let Some(override_exclusive_systems) = override_exclusive_systems {\n                                override_exclusive_systems(exclusive_systems_queue.as_mut());\n                            }\n                            *new_entity_created_this_frame = true;\n                        }\n                        yoleck.level_needs_saving = true;\n                    }\n                }\n            }\n\n            let entity_being_edited;\n            if let Ok((entity, mut yoleck_managed)) = yoleck_managed_query.single_mut() {\n                entity_being_edited = Some(entity);\n                ui.horizontal(|ui| {\n                    ui.heading(format_caption(entity, &yoleck_managed));\n                    if ui.button(\"Delete\").clicked() {\n                        commands.entity(entity).despawn();\n                        writer.write(YoleckEditorEvent::EntityDeselected(entity));\n                        yoleck.level_needs_saving = true;\n                    }\n                });\n                ui.horizontal(|ui| {\n                    ui.label(\"Custom Name:\");\n                    ui.text_edit_singleline(&mut yoleck_managed.name);\n                });\n            } else {\n                entity_being_edited = None;\n            }\n\n            if *previously_edited_entity != entity_being_edited {\n                *previously_edited_entity = entity_being_edited;\n                for knob_entity in knobs_cache.drain() {\n                    commands.entity(knob_entity).despawn();\n                }\n            } else {\n                knobs_cache.clean_untouched(|knob_entity| {\n                    commands.entity(knob_entity).despawn();\n                });\n            }\n        }\n        system_state.apply(world);\n\n        let frame = egui::Frame::new();\n        let mut prepared = frame.begin(ui);\n        let content_ui = std::mem::replace(\n            &mut prepared.content_ui,\n            ui.new_child(egui::UiBuilder {\n                max_rect: Some(ui.max_rect()),\n                layout: Some(*ui.layout()), // Is this necessary?\n                ..Default::default()\n            }),\n        );\n        world.insert_resource(YoleckUi(content_ui));\n        world.insert_resource(passed_data);\n\n        enum ActiveExclusiveSystemStatus {\n            DidNotRun,\n            StillRunningSame,\n            JustFinishedRunning,\n        }\n\n        let behavior_for_exclusive_system = if let Some(mut active_exclusive_system) =\n            world.remove_resource::<YoleckActiveExclusiveSystem>()\n        {\n            let result = active_exclusive_system\n                .0\n                .run((), world)\n                .map_err(|e| match e {\n                    bevy::ecs::system::RunSystemError::Skipped(e) => e.into(),\n                    bevy::ecs::system::RunSystemError::Failed(e) => e,\n                })?;\n            match result {\n                YoleckExclusiveSystemDirective::Listening => {\n                    world.insert_resource(active_exclusive_system);\n                    ActiveExclusiveSystemStatus::StillRunningSame\n                }\n                YoleckExclusiveSystemDirective::Finished => {\n                    ActiveExclusiveSystemStatus::JustFinishedRunning\n                }\n            }\n        } else {\n            ActiveExclusiveSystemStatus::DidNotRun\n        };\n\n        let should_run_regular_systems = match behavior_for_exclusive_system {\n            ActiveExclusiveSystemStatus::DidNotRun => loop {\n                let Some(mut new_exclusive_system) = world\n                    .resource_mut::<YoleckExclusiveSystemsQueue>()\n                    .pop_front()\n                else {\n                    break true;\n                };\n                new_exclusive_system.initialize(world);\n                let first_run_result =\n                    new_exclusive_system.run((), world).map_err(|e| match e {\n                        bevy::ecs::system::RunSystemError::Skipped(e) => e.into(),\n                        bevy::ecs::system::RunSystemError::Failed(e) => e,\n                    })?;\n                if *new_entity_created_this_frame\n                    || matches!(first_run_result, YoleckExclusiveSystemDirective::Listening)\n                {\n                    world.insert_resource(YoleckActiveExclusiveSystem(new_exclusive_system));\n                    break false;\n                }\n            },\n            ActiveExclusiveSystemStatus::StillRunningSame => false,\n            ActiveExclusiveSystemStatus::JustFinishedRunning => false,\n        };\n\n        if should_run_regular_systems {\n            world.resource_scope(|world, mut yoleck_edit_systems: Mut<YoleckEditSystems>| {\n                yoleck_edit_systems.run_systems(world);\n            });\n        }\n        let YoleckUi(content_ui) = world\n            .remove_resource()\n            .expect(\"The YoleckUi resource was put in the world by this very function\");\n        world.remove_resource::<YoleckPassedData>();\n        prepared.content_ui = content_ui;\n        prepared.end(&mut *ui);\n\n        // Some systems may have edited the entries, so we need to update them\n        world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);\n        Ok(())\n    })\n}\n"
  },
  {
    "path": "src/editor_panels.rs",
    "content": "use bevy::ecs::system::SystemId;\nuse bevy::prelude::*;\nuse bevy_egui::egui;\nuse std::ops::{Deref, DerefMut};\n\nuse crate::util::EditSpecificResources;\n\n/// An handle for the egui UI frame used in panel sections definitions\n#[derive(Resource)]\npub struct YoleckPanelUi(pub egui::Ui);\n\nimpl Deref for YoleckPanelUi {\n    type Target = egui::Ui;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl DerefMut for YoleckPanelUi {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        &mut self.0\n    }\n}\n\npub(crate) trait EditorPanel: Resource + Sized {\n    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>>;\n    fn wrapper(\n        &mut self,\n        ctx: &mut egui::Context,\n        add_content: impl FnOnce(&mut Self, &mut egui::Ui),\n    ) -> egui::Response;\n\n    fn show_panel(world: &mut World, ctx: &mut egui::Context) -> egui::Response {\n        world.resource_scope(|world, mut this: Mut<Self>| {\n            this.wrapper(ctx, |this, ui| {\n                let frame = egui::Frame::new();\n                let mut prepared = frame.begin(ui);\n                let content_ui = std::mem::replace(\n                    &mut prepared.content_ui,\n                    ui.new_child(egui::UiBuilder {\n                        max_rect: Some(ui.max_rect()),\n                        layout: Some(*ui.layout()), // Is this necessary?\n                        ..Default::default()\n                    }),\n                );\n                world.insert_resource(YoleckPanelUi(content_ui));\n\n                world.resource_scope(|world, mut edit_specific: Mut<EditSpecificResources>| {\n                    edit_specific.inject_to_world(world);\n                    for section in this.iter_sections() {\n                        world.run_system(section).unwrap().unwrap();\n                    }\n                    edit_specific.take_from_world(world);\n                });\n\n                let YoleckPanelUi(content_ui) = world.remove_resource().expect(\n                    \"The YoleckPanelUi resource was put in the world by this very function\",\n                );\n                prepared.content_ui = content_ui;\n                prepared.end(ui);\n            })\n        })\n    }\n}\n\n/// Sections for the left panel of the Yoleck editor window.\n///\n/// Already contains sections by default, but can be used to customize the editor by adding more\n/// sections. Each section is a Bevy system in the form of a [`SystemId`] with no input and a Bevy\n/// [`Result<()>`] for an output. These can be obtained by registering a system function on the\n/// Bevy app using [`register_system`](App::register_system).\n///\n/// The section system can draw on the panel using [`YoleckPanelUi`], accessible as a [`ResMut`].\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::{YoleckEditorLeftPanelSections, egui, YoleckPanelUi};\n/// # let mut app = App::new();\n/// let time_since_startup_section = app.register_system(|mut ui: ResMut<YoleckPanelUi>, time: Res<Time>| {\n///     ui.label(format!(\"Time since startup is {:?}\", time.elapsed()));\n///     Ok(())\n/// });\n/// app.world_mut().resource_mut::<YoleckEditorLeftPanelSections>().0.push(time_since_startup_section);\n/// ```\n#[derive(Resource)]\npub struct YoleckEditorLeftPanelSections(pub Vec<SystemId<(), Result>>);\n\nimpl FromWorld for YoleckEditorLeftPanelSections {\n    fn from_world(world: &mut World) -> Self {\n        Self(vec![\n            world.register_system(crate::editor::new_entity_section),\n            world.register_system(crate::editor::entity_selection_section),\n        ])\n    }\n}\n\nimpl EditorPanel for YoleckEditorLeftPanelSections {\n    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {\n        self.0.iter().copied()\n    }\n\n    fn wrapper(\n        &mut self,\n        ctx: &mut egui::Context,\n        add_content: impl FnOnce(&mut Self, &mut egui::Ui),\n    ) -> egui::Response {\n        egui::SidePanel::left(\"yoleck_left_panel\")\n            .resizable(true)\n            .default_width(300.0)\n            .max_width(ctx.content_rect().width() / 4.0)\n            .show(ctx, |ui| {\n                ui.heading(\"Level Hierarchy\");\n                ui.separator();\n                egui::ScrollArea::vertical().show(ui, |ui| {\n                    add_content(self, ui);\n                });\n            })\n            .response\n    }\n}\n\n/// Sections for the right panel of the Yoleck editor window. Works the same as\n/// [`YoleckEditorLeftPanelSections`].\n#[derive(Resource)]\npub struct YoleckEditorRightPanelSections(pub Vec<SystemId<(), Result>>);\n\nimpl FromWorld for YoleckEditorRightPanelSections {\n    fn from_world(world: &mut World) -> Self {\n        Self(vec![\n            world.register_system(crate::editor::entity_editing_section),\n        ])\n    }\n}\n\nimpl EditorPanel for YoleckEditorRightPanelSections {\n    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {\n        self.0.iter().copied()\n    }\n\n    fn wrapper(\n        &mut self,\n        ctx: &mut egui::Context,\n        add_content: impl FnOnce(&mut Self, &mut egui::Ui),\n    ) -> egui::Response {\n        egui::SidePanel::right(\"yoleck_right_panel\")\n            .resizable(true)\n            .default_width(300.0)\n            .max_width(ctx.content_rect().width() / 4.0)\n            .show(ctx, |ui| {\n                ui.heading(\"Properties\");\n                ui.separator();\n                egui::ScrollArea::vertical().show(ui, |ui| {\n                    add_content(self, ui);\n                });\n            })\n            .response\n    }\n}\n\n/// Sections for the top panel of the Yoleck editor window. Works the same as\n/// [`YoleckEditorLeftPanelSections`].\n#[derive(Resource)]\npub struct YoleckEditorTopPanelSections(pub Vec<SystemId<(), Result>>);\n\nimpl FromWorld for YoleckEditorTopPanelSections {\n    fn from_world(world: &mut World) -> Self {\n        Self(vec![\n            world.register_system(crate::level_files_manager::level_files_manager_top_section),\n            world.register_system(crate::level_files_manager::playtest_buttons_section),\n        ])\n    }\n}\n\nimpl EditorPanel for YoleckEditorTopPanelSections {\n    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {\n        self.0.iter().copied()\n    }\n\n    fn wrapper(\n        &mut self,\n        ctx: &mut egui::Context,\n        add_content: impl FnOnce(&mut Self, &mut egui::Ui),\n    ) -> egui::Response {\n        egui::TopBottomPanel::top(\"yoleck_top_panel\")\n            .resizable(false)\n            .show(ctx, |ui| {\n                let inner_margin = 3.;\n\n                ui.add_space(inner_margin);\n                ui.horizontal(|ui| {\n                    ui.add_space(inner_margin);\n                    ui.label(\"Yoleck Editor\");\n                    ui.separator();\n                    add_content(self, ui);\n                    ui.add_space(inner_margin);\n                });\n                ui.add_space(inner_margin);\n            })\n            .response\n    }\n}\n\n/// A tab in the bottom panel of the Yoleck editor window.\n///\n/// The [`sections`](Self::sections) parameter is a list of [`SystemId`] obtained similarly to the\n/// ones in [`YoleckEditorLeftPanelSections`].\npub struct YoleckEditorBottomPanelTab {\n    pub name: String,\n    pub sections: Vec<SystemId<(), Result>>,\n}\n\n/// Tabs for the bottom panel of the Yoleck editor window.\n///\n/// Works similar to [`YoleckEditorLeftPanelSections`], except instead of a single list of systems\n/// they reside within [`tabs`](Self::tabs).\n#[derive(Resource)]\npub struct YoleckEditorBottomPanelSections {\n    pub tabs: Vec<YoleckEditorBottomPanelTab>,\n    active_tab: usize,\n}\n\nimpl FromWorld for YoleckEditorBottomPanelSections {\n    fn from_world(world: &mut World) -> Self {\n        Self {\n            tabs: vec![YoleckEditorBottomPanelTab {\n                name: \"Console\".to_owned(),\n                sections: vec![world.register_system(crate::console::console_panel_section)],\n            }],\n            active_tab: 0,\n        }\n    }\n}\n\nimpl EditorPanel for YoleckEditorBottomPanelSections {\n    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {\n        self.tabs\n            .get(self.active_tab)\n            .into_iter()\n            .flat_map(|tab| tab.sections.iter().copied())\n    }\n\n    fn wrapper(\n        &mut self,\n        ctx: &mut egui::Context,\n        add_content: impl FnOnce(&mut Self, &mut egui::Ui),\n    ) -> egui::Response {\n        egui::TopBottomPanel::bottom(\"yoleck_bottom_panel\")\n            .resizable(true)\n            .default_height(200.0)\n            .max_height(ctx.content_rect().height() / 4.0)\n            .show(ctx, |ui| {\n                let inner_margin = 3.;\n                ui.add_space(inner_margin);\n\n                let mut new_active_tab = self.active_tab;\n                ui.horizontal(|ui| {\n                    for (i, tab) in self.tabs.iter().enumerate() {\n                        if ui\n                            .selectable_label(new_active_tab == i, &tab.name)\n                            .clicked()\n                        {\n                            new_active_tab = i;\n                        }\n                    }\n                });\n                self.active_tab = new_active_tab;\n\n                ui.separator();\n\n                add_content(self, ui);\n            })\n            .response\n    }\n}\n"
  },
  {
    "path": "src/editor_window.rs",
    "content": "use bevy::prelude::*;\nuse bevy::window::PrimaryWindow;\nuse bevy_egui::{EguiContext, PrimaryEguiContext, egui};\n\nuse crate::YoleckEditorBottomPanelSections;\nuse crate::YoleckEditorLeftPanelSections;\nuse crate::YoleckEditorRightPanelSections;\nuse crate::YoleckEditorTopPanelSections;\nuse crate::editor_panels::EditorPanel;\n\n#[derive(Resource, Default)]\npub struct YoleckEditorViewportRect {\n    pub rect: Option<egui::Rect>,\n}\n\npub(crate) fn yoleck_editor_window(\n    world: &mut World,\n    mut egui_query: Local<Option<QueryState<&mut EguiContext, With<PrimaryEguiContext>>>>,\n) {\n    let egui_query = egui_query.get_or_insert_with(|| world.query_filtered());\n    let mut borrowed_egui = if let Ok(mut egui_context) = egui_query.single_mut(world) {\n        core::mem::take(egui_context.as_mut())\n    } else {\n        return;\n    };\n\n    let ctx = borrowed_egui.get_mut();\n\n    // The order of panels is important, because panels that go first will take height/width from\n    // adjacent panels. The top panel must go first because it's height is very small (so it won't\n    // be impacting the side panels much) and it needs all the width it can get. The bottom panel\n    // can go last because giving the side panels more height for displaying their lists is more\n    // important than giving extra width to the console.\n\n    let top = YoleckEditorTopPanelSections::show_panel(world, ctx)\n        .rect\n        .height();\n\n    let left = YoleckEditorLeftPanelSections::show_panel(world, ctx)\n        .rect\n        .width();\n\n    let right = YoleckEditorRightPanelSections::show_panel(world, ctx)\n        .rect\n        .width();\n\n    let bottom = YoleckEditorBottomPanelSections::show_panel(world, ctx)\n        .rect\n        .height();\n\n    let viewport_rect = egui::Rect::from_min_max(\n        egui::Pos2::new(left, top),\n        egui::Pos2::new(\n            ctx.input(|i| i.viewport_rect().width()) - right,\n            ctx.input(|i| i.viewport_rect().height()) - bottom,\n        ),\n    );\n\n    if let Some(mut editor_viewport) = world.get_resource_mut::<YoleckEditorViewportRect>() {\n        editor_viewport.rect = Some(viewport_rect);\n    }\n\n    if let Ok(window) = world\n        .query_filtered::<&bevy::window::Window, With<PrimaryWindow>>()\n        .single(world)\n    {\n        let scale = window.scale_factor();\n\n        let left_px = (left * scale) as u32;\n        let right_px = (right * scale) as u32;\n        let top_px = (top * scale) as u32;\n        let bottom_px = (bottom * scale) as u32;\n\n        let pos = UVec2::new(left_px, top_px);\n        let size = UVec2::new(window.physical_width(), window.physical_height())\n            .saturating_sub(pos)\n            .saturating_sub(UVec2::new(right_px, bottom_px));\n\n        if size.x > 0 && size.y > 0 {\n            let mut camera_query =\n                world.query_filtered::<&mut Camera, Without<PrimaryEguiContext>>();\n            for mut camera in camera_query.iter_mut(world) {\n                camera.viewport = Some(bevy::camera::Viewport {\n                    physical_position: pos,\n                    physical_size: size,\n                    ..default()\n                });\n            }\n        }\n    }\n\n    if let Ok(mut egui_context) = egui_query.single_mut(world) {\n        *egui_context = borrowed_egui;\n    }\n}\n"
  },
  {
    "path": "src/entity_management.rs",
    "content": "use std::collections::BTreeSet;\n\nuse bevy::asset::io::Reader;\nuse bevy::asset::{AssetLoader, LoadContext};\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\nuse bevy::reflect::TypePath;\nuse serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::editor::YoleckEditorState;\nuse crate::entity_upgrading::YoleckEntityUpgrading;\nuse crate::errors::YoleckAssetLoaderError;\nuse crate::level_files_upgrading::upgrade_level_file;\nuse crate::populating::PopulateReason;\nuse crate::prelude::{YoleckEntityUuid, YoleckUuidRegistry};\nuse crate::{\n    YoleckBelongsToLevel, YoleckEntityConstructionSpecs, YoleckEntityLifecycleStatus,\n    YoleckInternalSchedule, YoleckLevelJustLoaded, YoleckManaged, YoleckSchedule, YoleckState,\n};\n\n/// Used by Yoleck to determine how to handle the entity.\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct YoleckEntryHeader {\n    #[serde(rename = \"type\")]\n    pub type_name: String,\n    /// A name to display near the entity in the entities list.\n    ///\n    /// This is for level editors' convenience only - it will not be used in the games.\n    #[serde(default)]\n    pub name: String,\n\n    /// A persistable way to identify the specific entity.\n    ///\n    /// Will be set automatically if the entity type was defined with\n    /// [`with_uuid`](crate::YoleckEntityType::with_uuid).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub uuid: Option<Uuid>,\n}\n\n/// An entry for a Yoleck entity, as it appears in level files.\n#[derive(Component, Debug, Clone)]\npub struct YoleckRawEntry {\n    pub header: YoleckEntryHeader,\n    pub data: serde_json::Map<String, serde_json::Value>,\n}\n\nimpl Serialize for YoleckRawEntry {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n        (&self.header, &self.data).serialize(serializer)\n    }\n}\n\nimpl<'de> Deserialize<'de> for YoleckRawEntry {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let (header, data) = Deserialize::deserialize(deserializer)?;\n        Ok(Self { header, data })\n    }\n}\n\npub(crate) fn yoleck_process_raw_entries(\n    editor_state: Res<State<YoleckEditorState>>,\n    mut commands: Commands,\n    mut raw_entries_query: Query<(Entity, &mut YoleckRawEntry), With<YoleckBelongsToLevel>>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    mut uuid_registry: ResMut<YoleckUuidRegistry>,\n) {\n    let mut entities_by_type = HashMap::<String, Vec<Entity>>::new();\n    for (entity, mut raw_entry) in raw_entries_query.iter_mut() {\n        entities_by_type\n            .entry(raw_entry.header.type_name.clone())\n            .or_default()\n            .push(entity);\n        let mut cmd = commands.entity(entity);\n        cmd.remove::<YoleckRawEntry>();\n\n        let mut components_data = HashMap::new();\n\n        if let Some(entity_type_info) =\n            construction_specs.get_entity_type_info(&raw_entry.header.type_name)\n        {\n            if entity_type_info.has_uuid {\n                let uuid = raw_entry.header.uuid.unwrap_or_else(Uuid::new_v4);\n                cmd.insert(YoleckEntityUuid(uuid));\n                uuid_registry.0.insert(uuid, cmd.id());\n            }\n            for component_name in entity_type_info.components.iter() {\n                let Some(handler) = construction_specs.component_handlers.get(component_name)\n                else {\n                    error!(\"Component type {:?} is not registered\", component_name);\n                    continue;\n                };\n                let raw_component_data = raw_entry\n                    .data\n                    .get_mut(handler.key())\n                    .map(|component_data| component_data.take());\n                handler.init_in_entity(raw_component_data, &mut cmd, &mut components_data);\n            }\n            for dlg in entity_type_info.on_init.iter() {\n                dlg(*editor_state.get(), &mut cmd);\n            }\n        } else {\n            error!(\"Entity type {:?} is not registered\", raw_entry.header.name);\n        }\n\n        cmd.insert(YoleckManaged {\n            name: raw_entry.header.name.to_owned(),\n            type_name: raw_entry.header.type_name.to_owned(),\n            lifecycle_status: YoleckEntityLifecycleStatus::JustCreated,\n            components_data,\n        });\n    }\n}\n\npub(crate) fn yoleck_prepare_populate_schedule(\n    mut query: Query<(Entity, &mut YoleckManaged)>,\n    mut entities_to_populate: ResMut<EntitiesToPopulate>,\n    mut yoleck_state: Option<ResMut<YoleckState>>,\n    editor_state: Res<State<YoleckEditorState>>,\n) {\n    entities_to_populate.0.clear();\n    let mut level_needs_saving = false;\n    for (entity, mut yoleck_managed) in query.iter_mut() {\n        match yoleck_managed.lifecycle_status {\n            YoleckEntityLifecycleStatus::Synchronized => {}\n            YoleckEntityLifecycleStatus::JustCreated => {\n                let populate_reason = match editor_state.get() {\n                    YoleckEditorState::EditorActive => PopulateReason::EditorInit,\n                    YoleckEditorState::GameActive => PopulateReason::RealGame,\n                };\n                entities_to_populate.0.push((entity, populate_reason));\n            }\n            YoleckEntityLifecycleStatus::JustChanged => {\n                entities_to_populate\n                    .0\n                    .push((entity, PopulateReason::EditorUpdate));\n                level_needs_saving = true;\n            }\n        }\n        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::Synchronized;\n    }\n    if level_needs_saving && let Some(yoleck_state) = yoleck_state.as_mut() {\n        yoleck_state.level_needs_saving = true;\n    }\n}\n\npub(crate) fn yoleck_run_populate_schedule(world: &mut World) {\n    world.run_schedule(YoleckSchedule::Populate);\n    world.run_schedule(YoleckSchedule::OverrideCommonComponents);\n}\n\n#[derive(Resource)]\npub(crate) struct EntitiesToPopulate(pub Vec<(Entity, PopulateReason)>);\n\npub(crate) fn process_loading_command(\n    query: Query<(Entity, &YoleckLoadLevel)>,\n    mut raw_levels_assets: ResMut<Assets<YoleckRawLevel>>,\n    entity_upgrading: Option<Res<YoleckEntityUpgrading>>,\n    mut commands: Commands,\n) {\n    for (level_entity, load_level) in query.iter() {\n        if let Some(raw_level) = raw_levels_assets.get_mut(&load_level.0) {\n            if let Some(entity_upgrading) = &entity_upgrading {\n                entity_upgrading.upgrade_raw_level_file(raw_level);\n            }\n            commands\n                .entity(level_entity)\n                .remove::<YoleckLoadLevel>()\n                .insert((YoleckLevelJustLoaded, YoleckKeepLevel));\n            for entry in raw_level.entries() {\n                commands.spawn((\n                    entry.clone(),\n                    YoleckBelongsToLevel {\n                        level: level_entity,\n                    },\n                ));\n            }\n        }\n    }\n}\n\npub(crate) fn yoleck_run_post_load_resolutions_schedule(world: &mut World) {\n    world.run_schedule(YoleckInternalSchedule::PostLoadResolutions);\n}\n\npub(crate) fn yoleck_run_level_loaded_schedule(world: &mut World) {\n    world.run_schedule(YoleckSchedule::LevelLoaded);\n}\n\npub(crate) fn yoleck_remove_just_loaded_marker_from_levels(\n    query: Query<Entity, With<YoleckLevelJustLoaded>>,\n    mut commands: Commands,\n) {\n    for level_entity in query.iter() {\n        commands\n            .entity(level_entity)\n            .remove::<YoleckLevelJustLoaded>();\n    }\n}\n\npub(crate) fn process_unloading_command(\n    mut removed_levels: RemovedComponents<YoleckKeepLevel>,\n    level_owned_entities_query: Query<(Entity, &YoleckBelongsToLevel)>,\n    mut commands: Commands,\n) {\n    if removed_levels.is_empty() {\n        return;\n    }\n    let removed_levels: BTreeSet<Entity> = removed_levels.read().collect();\n    for (entity, belongs_to_level) in level_owned_entities_query.iter() {\n        if removed_levels.contains(&belongs_to_level.level) {\n            commands.entity(entity).despawn();\n        }\n    }\n}\n\n/// Command Yoleck to load a level.\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// fn level_loading_system(\n///     asset_server: Res<AssetServer>,\n///     mut commands: Commands,\n/// ) {\n///     commands.spawn(YoleckLoadLevel(asset_server.load(\"levels/level1.yol\")));\n/// }\n/// ```\n///\n/// After the level is loaded, `YoleckLoadLevel` will be removed and [`YoleckKeepLevel`] will be\n/// added instead. To unload the level, either remove `YoleckKeepLevel` or despawn the entire level\n/// entity.\n///\n/// Immediately after the level is loaded, but before the populate systems get to run, Yoleck will\n/// run the [`YoleckSchedule::LevelLoaded`] schedule, allowing the game to register systems there\n/// and interfere with the level entities while they are still just freshly deserialized\n/// [`YoleckComponent`](crate::prelude::YoleckComponent) data. During that time, the entities of\n/// the levels that were just loaded will be marked with [`YoleckLevelJustLoaded`], allowing to\n/// these systems to distinguish them from already existing levels.\n///\n/// Note that the entities inside the level will _not_ be children of the level entity. Games that\n/// want to load multiple levels and dynamically position them should use\n/// [`VpeolRepositionLevel`](crate::vpeol::VpeolRepositionLevel).\n#[derive(Component)]\npub struct YoleckLoadLevel(pub Handle<YoleckRawLevel>);\n\n/// Marks an entity that represents a level. Its removal will unload the level.\n///\n/// This component is created automatically on entities that use [`YoleckLoadLevel`] when the level\n/// is loaded.\n///\n/// To unload the level, either despawn the entity or remove this component from it.\n#[derive(Component)]\npub struct YoleckKeepLevel;\n\n#[derive(TypePath)]\npub(crate) struct YoleckLevelAssetLoader;\n\n/// Represents a level file.\n#[derive(Asset, TypePath, Debug, Serialize, Deserialize, Clone)]\npub struct YoleckRawLevel(\n    pub(crate) YoleckRawLevelHeader,\n    serde_json::Value, // level data\n    pub(crate) Vec<YoleckRawEntry>,\n);\n\n/// Internal Yoleck metadata for a level file.\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct YoleckRawLevelHeader {\n    format_version: usize,\n    pub app_format_version: usize,\n}\n\nimpl YoleckRawLevel {\n    pub fn new(\n        app_format_version: usize,\n        entries: impl IntoIterator<Item = YoleckRawEntry>,\n    ) -> Self {\n        Self(\n            YoleckRawLevelHeader {\n                format_version: 2,\n                app_format_version,\n            },\n            serde_json::Value::Object(Default::default()),\n            entries.into_iter().collect(),\n        )\n    }\n\n    pub fn entries(&self) -> &[YoleckRawEntry] {\n        &self.2\n    }\n\n    pub fn into_entries(self) -> impl Iterator<Item = YoleckRawEntry> {\n        self.2.into_iter()\n    }\n}\n\nimpl AssetLoader for YoleckLevelAssetLoader {\n    type Asset = YoleckRawLevel;\n    type Settings = ();\n    type Error = YoleckAssetLoaderError;\n\n    fn extensions(&self) -> &[&str] {\n        &[\"yol\"]\n    }\n\n    async fn load(\n        &self,\n        reader: &mut dyn Reader,\n        _settings: &Self::Settings,\n        _load_context: &mut LoadContext<'_>,\n    ) -> Result<Self::Asset, Self::Error> {\n        let mut bytes = Vec::new();\n        reader.read_to_end(&mut bytes).await?;\n        let json = std::str::from_utf8(&bytes)?;\n        let level: serde_json::Value = serde_json::from_str(json)?;\n        let level = upgrade_level_file(level)?;\n        let level: YoleckRawLevel = serde_json::from_value(level)?;\n        Ok(level)\n    }\n}\n"
  },
  {
    "path": "src/entity_ref.rs",
    "content": "use std::any::TypeId;\n\nuse bevy::ecs::component::Mutable;\nuse bevy::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::YoleckManaged;\nuse crate::entity_uuid::YoleckUuidRegistry;\nuse crate::errors::YoleckEntityRefCannotBeResolved;\n\n/// A reference to another Yoleck entity, stored by UUID for persistence.\n///\n/// This allows one entity to reference another entity in a way that survives saving and loading.\n/// The reference is stored as a UUID in the level file, which gets resolved to an actual `Entity`\n/// at runtime.\n///\n/// # Requirements\n///\n/// **Important:** Only entities with `.with_uuid()` can be referenced. When defining entity types\n/// that should be referenceable, make sure to add `.with_uuid()` to the entity type:\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # let mut app = App::new();\n/// app.add_yoleck_entity_type({\n///     YoleckEntityType::new(\"Planet\")\n///         .with_uuid()  // Required for entity references!\n///         // ... other configuration\n/// #       ;YoleckEntityType::new(\"Planet\")\n/// });\n/// ```\n///\n/// # Editor Features\n///\n/// In the editor, entity references can be set using:\n/// - Dropdown menu to select from available entities\n/// - Drag and drop from the entity list (only entities with UUID can be dragged)\n/// - Viewport click selection using the 🎯 button\n///\n/// # Usage\n///\n/// Add a `YoleckEntityRef` field to your component with the `entity_ref` attribute to filter by\n/// entity type:\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # use serde::{Deserialize, Serialize};\n/// #[derive(Component, YoleckComponent, YoleckAutoEdit, Serialize, Deserialize, Clone, PartialEq, Default)]\n/// struct LaserPointer {\n///     #[yoleck(entity_ref = \"Planet\")]\n///     target: YoleckEntityRef,\n/// }\n/// ```\n#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default, Debug)]\npub struct YoleckEntityRef {\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    uuid: Option<Uuid>,\n    #[serde(skip)]\n    resolved: Option<Entity>,\n}\n\n// impl std::hash::Hash for YoleckEntityRef {\n// fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n// self.uuid.hash(state);\n// }\n// }\n\n// impl PartialEq for YoleckEntityRef {\n// fn eq(&self, other: &Self) -> bool {\n// self.uuid == other.uuid\n// }\n// }\n\nimpl YoleckEntityRef {\n    pub fn new() -> Self {\n        Self {\n            uuid: None,\n            resolved: None,\n        }\n    }\n\n    pub fn from_uuid(uuid: Uuid) -> Self {\n        Self {\n            uuid: Some(uuid),\n            resolved: None,\n        }\n    }\n\n    pub fn is_some(&self) -> bool {\n        self.uuid.is_some()\n    }\n\n    pub fn is_none(&self) -> bool {\n        self.uuid.is_none()\n    }\n\n    pub fn entity(&self) -> Option<Entity> {\n        self.resolved\n    }\n\n    pub fn uuid(&self) -> Option<Uuid> {\n        self.uuid\n    }\n\n    pub fn clear(&mut self) {\n        self.uuid = None;\n        self.resolved = None;\n    }\n\n    pub fn set(&mut self, uuid: Uuid) {\n        self.uuid = Some(uuid);\n        self.resolved = None;\n    }\n\n    pub fn resolve(\n        &mut self,\n        registry: &YoleckUuidRegistry,\n    ) -> Result<(), YoleckEntityRefCannotBeResolved> {\n        if let Some(uuid) = self.uuid {\n            self.resolved = registry.get(uuid);\n            if self.resolved.is_none() {\n                return Err(YoleckEntityRefCannotBeResolved { uuid });\n            }\n        } else {\n            self.resolved = None;\n        }\n        Ok(())\n    }\n}\n\npub trait YoleckEntityRefAccessor: Sized + Send + Sync + 'static {\n    fn entity_ref_fields() -> &'static [(&'static str, Option<&'static str>)];\n    fn get_entity_ref_mut(&mut self, field_name: &str) -> &mut YoleckEntityRef;\n    // TODO: make this more versatile\n    fn resolve_entity_refs(&mut self, registry: &YoleckUuidRegistry);\n}\n\npub(crate) fn validate_entity_ref_requirements_for<T: YoleckEntityRefAccessor>(\n    construction_specs: &crate::YoleckEntityConstructionSpecs,\n) {\n    for (field_name, filter) in T::entity_ref_fields() {\n        if let Some(required_entity_type) = filter\n            && let Some(entity_type_info) =\n                construction_specs.get_entity_type_info(required_entity_type)\n            && !entity_type_info.has_uuid\n        {\n            error!(\n                \"Component '{}' field '{}' requires entity type '{}' to have UUID.\",\n                std::any::type_name::<T>(),\n                field_name,\n                required_entity_type\n            );\n        }\n    }\n}\n\npub fn resolve_entity_refs<\n    T: 'static + Component<Mutability = Mutable> + YoleckEntityRefAccessor,\n>(\n    mut query: Query<(&mut T, &mut YoleckManaged)>,\n    registry: Res<YoleckUuidRegistry>,\n) {\n    for (mut component, mut managed) in query.iter_mut() {\n        component.resolve_entity_refs(registry.as_ref());\n        if let Some(data) = managed.components_data.get_mut(&TypeId::of::<T>())\n            && let Some(data) = data.downcast_mut::<T>()\n        {\n            data.resolve_entity_refs(registry.as_ref());\n        }\n    }\n}\n"
  },
  {
    "path": "src/entity_upgrading.rs",
    "content": "use std::collections::BTreeMap;\n\nuse bevy::prelude::*;\n\nuse crate::YoleckRawLevel;\n\n/// Support upgrading of entities when the layout of the Yoleck entities and components change.\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # let mut app = App::new();\n/// app.add_plugins(YoleckEntityUpgradingPlugin {\n///     app_format_version: 5,\n/// });\n///\n/// // The newest upgrade, from 4 to 5\n/// app.add_yoleck_entity_upgrade_for(5, \"Foo\", |data| {\n///     let mut old_data = data.remove(\"OldFooComponent\").unwrap();\n///     data[\"NewFooComponent\"] = old_data;\n/// });\n///\n/// // Some older upgrade, from 2 to 3\n/// app.add_yoleck_entity_upgrade(3, |_type_name, data| {\n///     if let Some(component_data) = data.get_mut(\"Bar\") {\n///         component_data[\"some_new_field\"] = 42.into();\n///     }\n/// });\n/// ```\npub struct YoleckEntityUpgradingPlugin {\n    /// The current version of the app data.\n    ///\n    /// If `YoleckEntityUpgradingPlugin` is not added, the current version is considered 0\n    /// (\"unversioned\")\n    pub app_format_version: usize,\n}\n\nimpl Plugin for YoleckEntityUpgradingPlugin {\n    fn build(&self, app: &mut App) {\n        app.insert_resource(YoleckEntityUpgrading {\n            app_format_version: self.app_format_version,\n            upgrade_functions: Default::default(),\n        });\n    }\n}\n\n#[derive(Resource)]\npub(crate) struct YoleckEntityUpgrading {\n    pub app_format_version: usize,\n    #[allow(clippy::type_complexity)]\n    pub upgrade_functions: BTreeMap<\n        usize,\n        Vec<\n            Box<\n                dyn 'static\n                    + Send\n                    + Sync\n                    + Fn(&str, &mut serde_json::Map<String, serde_json::Value>),\n            >,\n        >,\n    >,\n}\n\nimpl YoleckEntityUpgrading {\n    pub fn upgrade_raw_level_file(&self, levels_file: &mut YoleckRawLevel) {\n        let first_target_version = levels_file.0.app_format_version + 1;\n        for (target_version, upgrade_functions) in\n            self.upgrade_functions.range(first_target_version..)\n        {\n            for entity in levels_file.2.iter_mut() {\n                for function in upgrade_functions.iter() {\n                    function(&entity.header.type_name, &mut entity.data);\n                }\n            }\n            levels_file.0.app_format_version = *target_version;\n        }\n    }\n}\n"
  },
  {
    "path": "src/entity_uuid.rs",
    "content": "use bevy::prelude::*;\n\nuse bevy::platform::collections::HashMap;\nuse uuid::Uuid;\n\n/// A UUID automatically added to entity types defined with\n/// [`with_uuid`](crate::YoleckEntityType::with_uuid)\n///\n/// This UUID can be used to refer to the entity in a persistent way - e.g. from a\n/// [`YoleckComponent`](crate::prelude::YoleckComponent) of another entity. The `Entity` ID itself\n/// will change between runs, but the UUID can reliably store the connection between the entities\n/// in the `.yol` file.\n///\n/// To find an entity by UUID use [`YoleckUuidRegistry`].\n#[derive(Component, Debug)]\npub struct YoleckEntityUuid(pub(crate) Uuid);\n\nimpl YoleckEntityUuid {\n    pub fn get(&self) -> Uuid {\n        self.0\n    }\n}\n\n/// Helper registry for finding [`with_uuid`](crate::YoleckEntityType::with_uuid) defined entities\n/// by their UUID.\n///\n/// To find a UUID given the `Entity` - check its [`YoleckEntityUuid`] component.\n#[derive(Resource)]\npub struct YoleckUuidRegistry(pub(crate) HashMap<Uuid, Entity>);\n\nimpl YoleckUuidRegistry {\n    pub fn get(&self, uuid: Uuid) -> Option<Entity> {\n        self.0.get(&uuid).copied()\n    }\n}\n"
  },
  {
    "path": "src/errors.rs",
    "content": "use bevy::ecs::error::BevyError;\nuse uuid::Uuid;\n\n#[derive(thiserror::Error, Debug)]\npub(crate) enum YoleckAssetLoaderError {\n    #[error(\"{0}\")]\n    Io(#[from] std::io::Error),\n    #[error(\"{0}\")]\n    Utf8(#[from] std::str::Utf8Error),\n    #[error(\"{0}\")]\n    SerdeJson(#[from] serde_json::Error),\n    #[error(\"{0}\")]\n    Bevy(BevyError),\n}\n\n// For some reason #[from] doesn't work...\nimpl From<BevyError> for YoleckAssetLoaderError {\n    fn from(value: BevyError) -> Self {\n        Self::Bevy(value)\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\n#[error(\"{uuid} does not resolve to any known entity in the registry\")]\npub struct YoleckEntityRefCannotBeResolved {\n    pub uuid: Uuid,\n}\n"
  },
  {
    "path": "src/exclusive_systems.rs",
    "content": "use std::collections::VecDeque;\n\nuse bevy::prelude::*;\n\npub(crate) struct YoleckExclusiveSystemsPlugin;\n\nimpl Plugin for YoleckExclusiveSystemsPlugin {\n    fn build(&self, app: &mut App) {\n        app.init_resource::<YoleckExclusiveSystemsQueue>();\n        app.init_resource::<YoleckEntityCreationExclusiveSystems>();\n    }\n}\n\n/// The result of an exclusive system.\n#[derive(Debug)]\npub enum YoleckExclusiveSystemDirective {\n    /// An exclusive system needs to return this when it is not done yet and wants to still be\n    /// active in the next frame.\n    Listening,\n    /// An exclusive system needs to return this when it is has nothing more to do.\n    ///\n    /// This means that either the exclusive system received the input it was waiting for (e.g. - a\n    /// user click) or that it is not viable for the currently selected entity.\n    Finished,\n}\n\npub type YoleckExclusiveSystem = Box<dyn System<In = (), Out = YoleckExclusiveSystemDirective>>;\n\n/// The currently pending exclusive systems.\n///\n/// Other edit systems (exclusive or otherwise) may [`push_back`](Self::push_back) exclusive edit\n/// systems into this queue:\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # use bevy_yoleck::exclusive_systems::*;\n/// # use bevy_yoleck::vpeol::prelude::*;\n/// # #[derive(Component)]\n/// # struct LookingAt2D(Vec2);\n/// fn regular_edit_system(\n///     edit: YoleckEdit<(), With<LookingAt2D>>,\n///     mut ui: ResMut<YoleckUi>,\n///     mut exclusive_queue: ResMut<YoleckExclusiveSystemsQueue>,\n/// ) {\n///     if edit.single().is_err() {\n///         return;\n///     }\n///     if ui.button(\"Look At\").clicked() {\n///         exclusive_queue.push_back(exclusive_system);\n///     }\n/// }\n///\n/// fn exclusive_system(\n///     mut edit: YoleckEdit<&mut LookingAt2D>,\n///     // Getting the actual input is still quite manual. May be chanced in the future.\n///     cameras_query: Query<&VpeolCameraState>,\n///     ui: ResMut<YoleckUi>,\n///     buttons: Res<ButtonInput<MouseButton>>,\n/// ) -> YoleckExclusiveSystemDirective {\n///     let Ok(mut looking_at) = edit.single_mut() else {\n///         return YoleckExclusiveSystemDirective::Finished;\n///     };\n///\n///     let Some(cursor_ray) = cameras_query.iter().find_map(|camera_state| camera_state.cursor_ray) else {\n///         return YoleckExclusiveSystemDirective::Listening;\n///     };\n///     looking_at.0 = cursor_ray.origin.truncate();\n///\n///     if ui.ctx().is_pointer_over_area() {\n///         return YoleckExclusiveSystemDirective::Listening;\n///     }\n///\n///     if buttons.just_released(MouseButton::Left) {\n///         return YoleckExclusiveSystemDirective::Finished;\n///     }\n///\n///     return YoleckExclusiveSystemDirective::Listening;\n/// }\n/// ```\n#[derive(Resource, Default)]\npub struct YoleckExclusiveSystemsQueue(VecDeque<YoleckExclusiveSystem>);\n\nimpl YoleckExclusiveSystemsQueue {\n    /// Add an exclusive system to be ran starting from the next frame.\n    ///\n    /// If there are already exclusive systems running or enqueued, the new one will run after they\n    /// finish.\n    pub fn push_back<P>(&mut self, system: impl IntoSystem<(), YoleckExclusiveSystemDirective, P>) {\n        self.0.push_back(Box::new(IntoSystem::into_system(system)));\n    }\n\n    /// Add an exclusive system to be ran starting from the next frame.\n    ///\n    /// If there are already exclusive systems enqueued, the new one will run before them. If there\n    /// is an exclusive system already running, the new one will only run after it finishes.\n    pub fn push_front<P>(\n        &mut self,\n        system: impl IntoSystem<(), YoleckExclusiveSystemDirective, P>,\n    ) {\n        self.0.push_front(Box::new(IntoSystem::into_system(system)));\n    }\n\n    /// Remove all enqueued exclusive systems.\n    ///\n    /// This does not affect an exclusive system that is already running. That system will keep\n    /// running until it returns [`Finished`](YoleckExclusiveSystemDirective::Finished).\n    pub fn clear(&mut self) {\n        self.0.clear();\n    }\n\n    pub(crate) fn pop_front(&mut self) -> Option<YoleckExclusiveSystem> {\n        self.0.pop_front()\n    }\n}\n\n#[derive(Resource)]\npub(crate) struct YoleckActiveExclusiveSystem(pub YoleckExclusiveSystem);\n\n/// The exclusive systems that will run automatically when a new entity is created.\n///\n/// Note that this may contain exclusive systems that are not relevant for all entities. These\n/// exclusive systems are expected to return [`Finished`](YoleckExclusiveSystemDirective::Finished)\n/// immediately when they do not apply, so that the next ones would run immediately\n#[derive(Default, Resource)]\npub struct YoleckEntityCreationExclusiveSystems(\n    #[allow(clippy::type_complexity)]\n    Vec<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,\n);\n\nimpl YoleckEntityCreationExclusiveSystems {\n    /// Add a modification to the exclusive systems queue when new entities are created.\n    pub fn on_entity_creation(\n        &mut self,\n        dlg: impl 'static + Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue),\n    ) {\n        self.0.push(Box::new(dlg));\n    }\n\n    pub(crate) fn create_queue(&self) -> YoleckExclusiveSystemsQueue {\n        let mut queue = YoleckExclusiveSystemsQueue::default();\n        for dlg in self.0.iter() {\n            dlg(&mut queue);\n        }\n        queue\n    }\n}\n"
  },
  {
    "path": "src/knobs.rs",
    "content": "use std::any::{Any, TypeId};\nuse std::hash::{BuildHasher, Hash};\n\nuse bevy::ecs::system::{EntityCommands, SystemParam};\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\n\nuse crate::BoxedArc;\nuse crate::editor::YoleckPassedData;\n\n#[doc(hidden)]\n#[derive(Default, Resource)]\npub struct YoleckKnobsCache {\n    by_key_hash: HashMap<u64, Vec<CachedKnob>>,\n}\n\n#[doc(hidden)]\n#[derive(Component)]\npub struct YoleckKnobMarker;\n\nstruct CachedKnob {\n    key: Box<dyn Send + Sync + Any>,\n    entity: Entity,\n    keep_alive: bool,\n}\n\nimpl YoleckKnobsCache {\n    pub fn access<'a, K>(&mut self, key: K, commands: &'a mut Commands) -> KnobFromCache<'a>\n    where\n        K: 'static + Send + Sync + Hash + Eq,\n    {\n        let entries = self\n            .by_key_hash\n            .entry(self.by_key_hash.hasher().hash_one(&key))\n            .or_default();\n        for entry in entries.iter_mut() {\n            if let Some(cached_key) = entry.key.downcast_ref::<K>()\n                && key == *cached_key\n            {\n                entry.keep_alive = true;\n                return KnobFromCache {\n                    cmd: commands.entity(entry.entity),\n                    is_new: false,\n                };\n            }\n        }\n        let cmd = commands.spawn(YoleckKnobMarker);\n        entries.push(CachedKnob {\n            key: Box::new(key),\n            entity: cmd.id(),\n            keep_alive: true,\n        });\n        KnobFromCache { cmd, is_new: true }\n    }\n\n    pub fn clean_untouched(&mut self, mut clean_func: impl FnMut(Entity)) {\n        self.by_key_hash.retain(|_, entries| {\n            entries.retain_mut(|entry| {\n                if entry.keep_alive {\n                    entry.keep_alive = false;\n                    true\n                } else {\n                    clean_func(entry.entity);\n                    false\n                }\n            });\n            !entries.is_empty()\n        });\n    }\n\n    pub fn drain(&mut self) -> impl '_ + Iterator<Item = Entity> {\n        self.by_key_hash\n            .drain()\n            .flat_map(|(_, entries)| entries.into_iter().map(|entry| entry.entity))\n    }\n}\n\npub struct KnobFromCache<'a> {\n    pub cmd: EntityCommands<'a>,\n    pub is_new: bool,\n}\n\n/// An handle for intearcing with a knob from an edit system.\npub struct YoleckKnobHandle<'a> {\n    /// The command of the knob entity.\n    pub cmd: EntityCommands<'a>,\n    /// `true` if the knob entity is just created this frame.\n    pub is_new: bool,\n    passed: HashMap<TypeId, BoxedArc>,\n}\n\nimpl YoleckKnobHandle<'_> {\n    /// Get data sent to the knob from external systems (usually interaciton from the level\n    /// editor)\n    ///\n    /// The data is sent using [a directive event](crate::YoleckDirective::pass_to_entity).\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;;\n    /// # use bevy_yoleck::vpeol::YoleckKnobClick;\n    /// # #[derive(Component)]\n    /// # struct Example {\n    /// #     num_clicks_on_knob: usize,\n    /// # };\n    /// fn edit_example_with_knob(mut edit: YoleckEdit<&mut Example>, mut knobs: YoleckKnobs) {\n    ///     let Ok(mut example) = edit.single_mut() else { return };\n    ///     let mut knob = knobs.knob(\"click-counting\");\n    ///     knob.cmd.insert((\n    ///         // setup the knobs position and graphics\n    ///     ));\n    ///     if knob.get_passed_data::<YoleckKnobClick>().is_some() {\n    ///         example.num_clicks_on_knob += 1;\n    ///     }\n    /// }\n    /// ```\n    pub fn get_passed_data<T: 'static>(&self) -> Option<&T> {\n        if let Some(dynamic) = self.passed.get(&TypeId::of::<T>()) {\n            dynamic.downcast_ref()\n        } else {\n            None\n        }\n    }\n}\n\n#[derive(SystemParam)]\npub struct YoleckKnobs<'w, 's> {\n    knobs_cache: ResMut<'w, YoleckKnobsCache>,\n    commands: Commands<'w, 's>,\n    passed_data: Res<'w, YoleckPassedData>,\n}\n\nimpl YoleckKnobs<'_, '_> {\n    pub fn knob<K>(&mut self, key: K) -> YoleckKnobHandle<'_>\n    where\n        K: 'static + Send + Sync + Hash + Eq,\n    {\n        let KnobFromCache { cmd, is_new } = self.knobs_cache.access(key, &mut self.commands);\n        let passed = self\n            .passed_data\n            .0\n            .get(&cmd.id())\n            // TODO: find a way to do this with the borrow checker, without cloning\n            .cloned()\n            .unwrap_or_default();\n        YoleckKnobHandle {\n            cmd,\n            is_new,\n            passed,\n        }\n    }\n}\n"
  },
  {
    "path": "src/level_files_manager.rs",
    "content": "use std::path::PathBuf;\nuse std::{fs, io};\n\nuse bevy::platform::collections::HashSet;\nuse bevy::prelude::*;\nuse bevy_egui::egui;\n\nuse crate::editor_panels::YoleckPanelUi;\nuse crate::entity_management::{\n    YoleckEntryHeader, YoleckKeepLevel, YoleckLoadLevel, YoleckRawEntry,\n};\nuse crate::entity_upgrading::YoleckEntityUpgrading;\nuse crate::exclusive_systems::YoleckActiveExclusiveSystem;\nuse crate::knobs::YoleckKnobsCache;\nuse crate::level_files_upgrading::upgrade_level_file;\nuse crate::level_index::YoleckLevelIndexEntry;\nuse crate::prelude::{YoleckEditorState, YoleckEntityUuid};\nuse crate::{\n    YoleckEditableLevels, YoleckEntityConstructionSpecs, YoleckLevelInEditor,\n    YoleckLevelInPlaytest, YoleckLevelIndex, YoleckManaged, YoleckPlaytestLevel, YoleckRawLevel,\n    YoleckState,\n};\n\nconst EXTENSION: &str = \".yol\";\nconst EXTENSION_WITHOUT_DOT: &str = \"yol\";\n\n/// The path for the levels directory.\n///\n/// [The plugin](crate::YoleckPluginForEditor) sets it to `./assets/levels/`, but it can be set to\n/// other values:\n/// ```no_run\n/// # use std::path::Path;\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::YoleckEditorLevelsDirectoryPath;\n/// # let mut app = App::new();\n/// app.insert_resource(YoleckEditorLevelsDirectoryPath(\n///     Path::new(\".\").join(\"some\").join(\"other\").join(\"path\"),\n/// ));\n/// ```\n#[derive(Resource)]\npub struct YoleckEditorLevelsDirectoryPath(pub PathBuf);\n\n#[derive(Debug)]\nenum SelectedLevelFile {\n    Unsaved(String),\n    Existing(String),\n}\n\n#[doc(hidden)]\npub struct LevelFilesManagerTopSectionLocals {\n    should_list_files: bool,\n    loaded_files_index: io::Result<Vec<YoleckLevelIndexEntry>>,\n    file_popup_open: bool,\n    selected_level_file: SelectedLevelFile,\n}\n\nimpl Default for LevelFilesManagerTopSectionLocals {\n    fn default() -> Self {\n        Self {\n            should_list_files: true,\n            loaded_files_index: Ok(vec![]),\n            file_popup_open: false,\n            selected_level_file: SelectedLevelFile::Unsaved(String::new()),\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\npub fn level_files_manager_top_section(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut locals: Local<LevelFilesManagerTopSectionLocals>,\n    mut commands: Commands,\n    mut yoleck: ResMut<YoleckState>,\n    mut levels_directory: ResMut<YoleckEditorLevelsDirectoryPath>,\n    mut editable_levels: ResMut<YoleckEditableLevels>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    yoleck_managed_query: Query<(&YoleckManaged, Option<&YoleckEntityUuid>)>,\n    keep_levels_query: Query<Entity, With<YoleckKeepLevel>>,\n    editor_state: Res<State<YoleckEditorState>>,\n    mut knobs_cache: ResMut<YoleckKnobsCache>,\n    mut level_assets: ResMut<Assets<YoleckRawLevel>>,\n    entity_upgrading: Option<Res<YoleckEntityUpgrading>>,\n    active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,\n) -> Result {\n    if active_exclusive_system.is_some() {\n        return Ok(());\n    }\n\n    let LevelFilesManagerTopSectionLocals {\n        should_list_files,\n        loaded_files_index,\n        file_popup_open,\n        selected_level_file,\n    } = &mut *locals;\n\n    let gen_raw_level_file = || {\n        let app_format_version = if let Some(entity_upgrading) = &entity_upgrading {\n            entity_upgrading.app_format_version\n        } else {\n            0\n        };\n        YoleckRawLevel::new(app_format_version, {\n            yoleck_managed_query\n                .iter()\n                .map(|(yoleck_managed, entity_uuid)| YoleckRawEntry {\n                    header: YoleckEntryHeader {\n                        type_name: yoleck_managed.type_name.clone(),\n                        name: yoleck_managed.name.clone(),\n                        uuid: entity_uuid.map(|entity_uuid| entity_uuid.get()),\n                    },\n                    data: {\n                        if let Some(entity_type_info) =\n                            construction_specs.get_entity_type_info(&yoleck_managed.type_name)\n                        {\n                            entity_type_info\n                                .components\n                                .iter()\n                                .filter_map(|component| {\n                                    let component_data =\n                                        yoleck_managed.components_data.get(component)?;\n                                    let handler = &construction_specs.component_handlers[component];\n                                    Some((\n                                        handler.key().to_owned(),\n                                        handler.serialize(component_data.as_ref()),\n                                    ))\n                                })\n                                .collect()\n                        } else {\n                            error!(\n                                \"Entity type {:?} is not registered\",\n                                yoleck_managed.type_name\n                            );\n                            Default::default()\n                        }\n                    },\n                })\n        })\n    };\n\n    if matches!(editor_state.get(), YoleckEditorState::EditorActive) {\n        enum LevelManagementAction {\n            DoNothing,\n            ClearLevel,\n            LoadLevel { filename: String },\n            SaveExisting { filename: String },\n        }\n\n        let mut level_management_action = LevelManagementAction::DoNothing;\n\n        let file_button_response = ui.button(\"File\");\n        if file_button_response.clicked() {\n            *file_popup_open = !*file_popup_open;\n        }\n\n        if *file_popup_open {\n            let button_rect = file_button_response.rect;\n            let area_response = egui::Area::new(egui::Id::new(\"file_popup_area\"))\n                .order(egui::Order::Foreground)\n                .fixed_pos(egui::pos2(button_rect.left(), button_rect.bottom() + 2.0))\n                .show(ui.ctx(), |ui| {\n                    egui::Frame::popup(ui.style()).show(ui, |ui| {\n                        ui.set_min_width(400.0);\n                        ui.set_max_width(600.0);\n\n                        let mut path_str = levels_directory.0.to_string_lossy().to_string();\n                        ui.horizontal(|ui| {\n                            ui.label(\"Levels Directory:\");\n                            if ui.text_edit_singleline(&mut path_str).lost_focus() {\n                                *should_list_files = true;\n                            }\n                        });\n                        levels_directory.0 = path_str.into();\n\n                        let mk_files_index = || levels_directory.0.join(\"index.yoli\");\n\n                        let save_index = |loaded_files_index: &[YoleckLevelIndexEntry]| {\n                            let index_file = mk_files_index();\n                            match fs::File::create(&index_file) {\n                                Ok(fd) => {\n                                    let index =\n                                        YoleckLevelIndex::new(loaded_files_index.iter().cloned());\n                                    serde_json::to_writer(fd, &index).unwrap();\n                                }\n                                Err(err) => {\n                                    warn!(\"Cannot open {:?} - {}\", index_file, err);\n                                }\n                            }\n                        };\n\n                        if *should_list_files {\n                            *should_list_files = false;\n\n                            let editable_levels_update_result = fs::read_dir(&levels_directory.0)\n                                .and_then(|files| {\n                                    editable_levels.levels = files\n                                        .filter_map(|file| {\n                                            let file = match file {\n                                                Ok(file) => file,\n                                                Err(err) => return Some(Err(err)),\n                                            };\n                                            if file.path().extension()\n                                                != Some(std::ffi::OsStr::new(EXTENSION_WITHOUT_DOT))\n                                            {\n                                                return None;\n                                            }\n                                            Some(Ok(file.file_name().to_string_lossy().into()))\n                                        })\n                                        .collect::<Result<_, _>>()?;\n                                    Ok(())\n                                });\n\n                            *loaded_files_index = editable_levels_update_result.and_then(|()| {\n                                let index_file = mk_files_index();\n                                let mut files_index: Vec<YoleckLevelIndexEntry> =\n                                    match fs::File::open(&index_file) {\n                                        Ok(fd) => {\n                                            let index: YoleckLevelIndex =\n                                                serde_json::from_reader(fd)?;\n                                            index.iter().cloned().collect()\n                                        }\n                                        Err(err) => {\n                                            warn!(\"Cannot open {:?} - {}\", index_file, err);\n                                            Vec::new()\n                                        }\n                                    };\n                                let mut existing_files: HashSet<String> = files_index\n                                    .iter()\n                                    .map(|file| file.filename.clone())\n                                    .collect();\n                                for filename in editable_levels.names() {\n                                    if !existing_files.remove(filename) {\n                                        files_index.push(YoleckLevelIndexEntry {\n                                            filename: filename.to_owned(),\n                                        });\n                                    }\n                                }\n                                files_index.retain(|file| !existing_files.contains(&file.filename));\n                                save_index(&files_index);\n                                Ok(files_index)\n                            });\n                        }\n\n                        match &mut *loaded_files_index {\n                            Ok(files) => {\n                                let mut swap_with_previous = None;\n                                egui::ScrollArea::vertical()\n                                    .max_height(200.0)\n                                    .show(ui, |ui| {\n                                        for (index, file) in files.iter().enumerate() {\n                                            let is_selected =\n                                                if let SelectedLevelFile::Existing(selected_name) =\n                                                    &*selected_level_file\n                                                {\n                                                    *selected_name == file.filename\n                                                } else {\n                                                    false\n                                                };\n                                            ui.horizontal(|ui| {\n                                                if ui\n                                                    .add_enabled(0 < index, egui::Button::new(\"^\"))\n                                                    .clicked()\n                                                {\n                                                    swap_with_previous = Some(index);\n                                                }\n                                                if ui\n                                                    .add_enabled(\n                                                        index < files.len() - 1,\n                                                        egui::Button::new(\"v\"),\n                                                    )\n                                                    .clicked()\n                                                {\n                                                    swap_with_previous = Some(index + 1);\n                                                }\n                                                let yoleck = yoleck.as_mut();\n                                                if ui\n                                                    .selectable_label(is_selected, &file.filename)\n                                                    .clicked()\n                                                {\n                                                    #[allow(clippy::collapsible_else_if)]\n                                                    if !is_selected && !yoleck.level_needs_saving {\n                                                        *selected_level_file =\n                                                            SelectedLevelFile::Existing(\n                                                                file.filename.clone(),\n                                                            );\n                                                        level_management_action =\n                                                            LevelManagementAction::LoadLevel {\n                                                                filename: file.filename.clone(),\n                                                            };\n                                                    }\n                                                }\n                                            });\n                                        }\n                                    });\n                                if let Some(swap_with_previous) = swap_with_previous {\n                                    files.swap(swap_with_previous, swap_with_previous - 1);\n                                    save_index(files);\n                                }\n                                ui.horizontal(|ui| {\n                                    #[allow(clippy::collapsible_else_if)]\n                                    match &mut *selected_level_file {\n                                        SelectedLevelFile::Unsaved(file_name) => {\n                                            ui.text_edit_singleline(file_name);\n                                            let button = ui.add_enabled(\n                                                !file_name.is_empty(),\n                                                egui::Button::new(\"Create\"),\n                                            );\n                                            if button.clicked() {\n                                                if !file_name.ends_with(EXTENSION) {\n                                                    file_name.push_str(EXTENSION);\n                                                }\n                                                let mut file_path = levels_directory.0.clone();\n                                                file_path.push(&file_name);\n                                                match fs::OpenOptions::new()\n                                                    .write(true)\n                                                    .create_new(true)\n                                                    .open(&file_path)\n                                                {\n                                                    Ok(fd) => {\n                                                        info!(\n                                                            \"Saving current new level to {:?}\",\n                                                            file_path\n                                                        );\n                                                        serde_json::to_writer(\n                                                            fd,\n                                                            &gen_raw_level_file(),\n                                                        )\n                                                        .unwrap();\n                                                        *selected_level_file =\n                                                            SelectedLevelFile::Existing(\n                                                                file_name.to_owned(),\n                                                            );\n                                                        *should_list_files = true;\n                                                        yoleck.level_needs_saving = false;\n                                                    }\n                                                    Err(err) => {\n                                                        warn!(\n                                                            \"Cannot open {:?} - {}\",\n                                                            file_path, err\n                                                        );\n                                                    }\n                                                }\n                                            }\n                                        }\n                                        SelectedLevelFile::Existing(_) => {\n                                            let button = ui.add_enabled(\n                                                !yoleck.level_needs_saving,\n                                                egui::Button::new(\"New Level\"),\n                                            );\n                                            if button.clicked() {\n                                                level_management_action =\n                                                    LevelManagementAction::ClearLevel;\n                                                *selected_level_file =\n                                                    SelectedLevelFile::Unsaved(String::new());\n                                                yoleck.level_being_edited = commands\n                                                    .spawn((YoleckLevelInEditor, YoleckKeepLevel))\n                                                    .id();\n                                            }\n                                        }\n                                    }\n                                });\n                            }\n                            Err(err) => {\n                                ui.label(format!(\"Cannot read: {err}\"));\n                            }\n                        }\n                    });\n                });\n\n            if ui.input(|i| i.key_pressed(egui::Key::Escape)) {\n                *file_popup_open = false;\n            }\n\n            if ui.input(|i| i.pointer.primary_clicked())\n                && let Some(pos) = ui.input(|i| i.pointer.interact_pos())\n                && !area_response.response.rect.contains(pos)\n                && !button_rect.contains(pos)\n            {\n                *file_popup_open = false;\n            }\n        }\n\n        match selected_level_file {\n            SelectedLevelFile::Unsaved(_) => {\n                ui.add_enabled_ui(yoleck.level_needs_saving, |ui| {\n                    if ui.button(\"Wipe Level\").clicked() {\n                        level_management_action = LevelManagementAction::ClearLevel;\n                    }\n                });\n            }\n            SelectedLevelFile::Existing(filename) => {\n                ui.label(filename.as_str());\n                ui.add_enabled_ui(yoleck.level_needs_saving, |ui| {\n                    if ui.button(\"SAVE\").clicked() {\n                        level_management_action = LevelManagementAction::SaveExisting {\n                            filename: filename.clone(),\n                        }\n                    }\n                    if ui.button(\"REVERT\").clicked() {\n                        level_management_action = LevelManagementAction::LoadLevel {\n                            filename: filename.clone(),\n                        };\n                    }\n                });\n            }\n        }\n\n        match level_management_action {\n            LevelManagementAction::DoNothing => {}\n            LevelManagementAction::ClearLevel => {\n                for level_entity in keep_levels_query.iter() {\n                    commands.entity(level_entity).despawn();\n                }\n                for knob_entity in knobs_cache.drain() {\n                    commands.entity(knob_entity).despawn();\n                }\n\n                yoleck.level_needs_saving = false;\n            }\n            LevelManagementAction::LoadLevel { filename } => {\n                // Clear the level before loading\n                for level_entity in keep_levels_query.iter() {\n                    commands.entity(level_entity).despawn();\n                }\n                for knob_entity in knobs_cache.drain() {\n                    commands.entity(knob_entity).despawn();\n                }\n\n                yoleck.level_needs_saving = false;\n\n                let fd = fs::File::open(levels_directory.0.join(&filename)).unwrap();\n                let level: serde_json::Value = serde_json::from_reader(fd).unwrap();\n                match upgrade_level_file(level) {\n                    Ok(level) => {\n                        let level: YoleckRawLevel = serde_json::from_value(level).unwrap();\n                        let level_asset_handle = level_assets.add(level);\n                        yoleck.level_being_edited = commands\n                            .spawn((YoleckLevelInEditor, YoleckLoadLevel(level_asset_handle)))\n                            .id();\n                    }\n                    Err(err) => {\n                        warn!(\"Cannot upgrade {:?} - {}\", filename, err);\n                    }\n                }\n            }\n            LevelManagementAction::SaveExisting { filename } => {\n                let file_path = levels_directory.0.join(filename);\n                info!(\"Saving current level to {:?}\", file_path);\n                let fd = fs::OpenOptions::new()\n                    .write(true)\n                    .create(false)\n                    .truncate(true)\n                    .open(file_path)?;\n                serde_json::to_writer(fd, &gen_raw_level_file())?;\n                yoleck.level_needs_saving = false;\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// The UI part for Playtest buttons in the top panel.\n#[allow(clippy::too_many_arguments)]\npub fn playtest_buttons_section(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut commands: Commands,\n    mut yoleck: ResMut<YoleckState>,\n    mut playtest_level: ResMut<YoleckPlaytestLevel>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    yoleck_managed_query: Query<(&YoleckManaged, Option<&YoleckEntityUuid>)>,\n    keep_levels_query: Query<Entity, With<YoleckKeepLevel>>,\n    mut next_editor_state: ResMut<NextState<YoleckEditorState>>,\n    mut knobs_cache: ResMut<YoleckKnobsCache>,\n    mut level_assets: ResMut<Assets<YoleckRawLevel>>,\n    entity_upgrading: Option<Res<YoleckEntityUpgrading>>,\n) -> Result {\n    let gen_raw_level_file = || {\n        let app_format_version = if let Some(entity_upgrading) = &entity_upgrading {\n            entity_upgrading.app_format_version\n        } else {\n            0\n        };\n        YoleckRawLevel::new(app_format_version, {\n            yoleck_managed_query\n                .iter()\n                .map(|(yoleck_managed, entity_uuid)| YoleckRawEntry {\n                    header: YoleckEntryHeader {\n                        type_name: yoleck_managed.type_name.clone(),\n                        name: yoleck_managed.name.clone(),\n                        uuid: entity_uuid.map(|entity_uuid| entity_uuid.get()),\n                    },\n                    data: {\n                        if let Some(entity_type_info) =\n                            construction_specs.get_entity_type_info(&yoleck_managed.type_name)\n                        {\n                            entity_type_info\n                                .components\n                                .iter()\n                                .filter_map(|component| {\n                                    let component_data =\n                                        yoleck_managed.components_data.get(component)?;\n                                    let handler = &construction_specs.component_handlers[component];\n                                    Some((\n                                        handler.key().to_owned(),\n                                        handler.serialize(component_data.as_ref()),\n                                    ))\n                                })\n                                .collect()\n                        } else {\n                            error!(\n                                \"Entity type {:?} is not registered\",\n                                yoleck_managed.type_name\n                            );\n                            Default::default()\n                        }\n                    },\n                })\n        })\n    };\n\n    let mut clear_level = |commands: &mut Commands| {\n        for level_entity in keep_levels_query.iter() {\n            commands.entity(level_entity).despawn();\n        }\n        for knob_entity in knobs_cache.drain() {\n            commands.entity(knob_entity).despawn();\n        }\n    };\n\n    if let Some(level) = &playtest_level.0 {\n        let finish_playtest_response = ui.button(\"Finish Playtest\");\n        if ui.button(\"Restart Playtest\").clicked() {\n            clear_level(&mut commands);\n            let level_asset_handle = level_assets.add(level.clone());\n            yoleck.level_being_edited = commands\n                .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))\n                .id();\n        }\n        if finish_playtest_response.clicked() {\n            clear_level(&mut commands);\n            next_editor_state.set(YoleckEditorState::EditorActive);\n            let level_asset_handle = level_assets.add(level.clone());\n            yoleck.level_being_edited = commands\n                .spawn((YoleckLevelInEditor, YoleckLoadLevel(level_asset_handle)))\n                .id();\n            playtest_level.0 = None;\n        }\n    } else if ui.button(\"Playtest\").clicked() {\n        let level = gen_raw_level_file();\n        clear_level(&mut commands);\n        next_editor_state.set(YoleckEditorState::GameActive);\n        let level_asset_handle = level_assets.add(level.clone());\n        yoleck.level_being_edited = commands\n            .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))\n            .id();\n        playtest_level.0 = Some(level);\n    }\n\n    ui.separator();\n    Ok(())\n}\n"
  },
  {
    "path": "src/level_files_upgrading.rs",
    "content": "use bevy::prelude::*;\n\n/// Upgrade a level file to the most recent Yoleck level format\npub fn upgrade_level_file(mut level: serde_json::Value) -> Result<serde_json::Value> {\n    let parts = level.as_array_mut().ok_or(\"Level file must be an array\")?;\n    let mut format_version = parts\n        .first()\n        .ok_or(\"Level file array must not be empty\")?\n        .as_object()\n        .ok_or(\"Level file header must be an object\")?\n        .get(\"format_version\")\n        .ok_or(\"Level file header must have a `format_version` field\")?\n        .as_u64()\n        .ok_or(\"`format_version` must be a non-negative number\")?;\n\n    for (upgrade_to, upgrade_fn) in [(2, upgrade_level_file_1_to_2)] {\n        if format_version < upgrade_to {\n            upgrade_fn(parts)?;\n            format_version = upgrade_to;\n        }\n    }\n\n    parts[0].as_object_mut().expect(\"already verified\")[\"format_version\"] = format_version.into();\n\n    Ok(level)\n}\n\nfn upgrade_level_file_1_to_2(parts: &mut [serde_json::Value]) -> Result<()> {\n    let header = parts\n        .get_mut(0)\n        .ok_or(\"Level file must have header as first element\")?\n        .as_object_mut()\n        .ok_or(\"Header must be object\")?;\n    header.insert(\"app_format_version\".to_owned(), 0.into());\n\n    let entities = parts\n        .get_mut(2)\n        .ok_or(\"Level file must have entities list as third element\")?\n        .as_array_mut()\n        .ok_or(\"Entity list must be array\")?;\n\n    for entity in entities.iter_mut() {\n        let entity_type = entity\n            .pointer(\"/0/type\")\n            .ok_or(\"Entity must have a header with a `type` field\")?\n            .as_str()\n            .ok_or(\"Entity `type` must be a string\")?\n            .to_owned();\n        let entity_data = entity.get_mut(1).ok_or(\"Entity must have data\")?;\n        let orig_data = entity_data.take();\n        *entity_data = serde_json::Value::Object(Default::default());\n        entity_data[entity_type] = orig_data;\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "src/level_index.rs",
    "content": "use std::collections::BTreeSet;\nuse std::ops::Deref;\n\nuse bevy::asset::AssetLoader;\nuse bevy::prelude::*;\nuse bevy::reflect::TypePath;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::errors::YoleckAssetLoaderError;\n\n/// Describes a level in the index.\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct YoleckLevelIndexEntry {\n    /// The name of the file containing the level, relative to where the levels index file is.\n    pub filename: String,\n}\n\n/// An asset loaded from a `.yoli` file (usually `index.yoli`) representing the game's levels.\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// fn load_level_system(\n///     mut level_index_handle: Local<Option<Handle<YoleckLevelIndex>>>,\n///     asset_server: Res<AssetServer>,\n///     level_index_assets: Res<Assets<YoleckLevelIndex>>,\n///     mut commands: Commands,\n/// ) {\n///     # let level_number: usize = todo!();\n///     // Keep the handle in local resource, so that Bevy will not unload the level index asset\n///     // between frames.\n///     let level_index_handle = level_index_handle\n///         .get_or_insert_with(|| asset_server.load(\"levels/index.yoli\"))\n///         .clone();\n///     let Some(level_index) = level_index_assets.get(&level_index_handle) else {\n///         // During the first invocation of this system, the level index asset is not going to be\n///         // loaded just yet. Since this system is going to run on every frame during the Loading\n///         // state, it just has to keep trying until it starts in a frame where it is loaded.\n///         return;\n///     };\n///     let level_to_load = level_index[level_number];\n///     let level_handle: Handle<YoleckRawLevel> = asset_server.load(&format!(\"levels/{}\", level_to_load.filename));\n///     commands.spawn(YoleckLoadLevel(level_handle));\n/// }\n/// ```\n#[derive(Asset, TypePath, Debug, Serialize, Deserialize)]\npub struct YoleckLevelIndex(YoleckLevelIndexHeader, Vec<YoleckLevelIndexEntry>);\n\n/// Internal Yoleck metadata for the levels index.\n#[derive(Debug, Serialize, Deserialize)]\npub struct YoleckLevelIndexHeader {\n    format_version: usize,\n}\n\nimpl YoleckLevelIndex {\n    pub fn new(entries: impl IntoIterator<Item = YoleckLevelIndexEntry>) -> Self {\n        Self(\n            YoleckLevelIndexHeader { format_version: 1 },\n            entries.into_iter().collect(),\n        )\n    }\n}\n\nimpl Deref for YoleckLevelIndex {\n    type Target = [YoleckLevelIndexEntry];\n\n    fn deref(&self) -> &Self::Target {\n        &self.1\n    }\n}\n\n#[derive(TypePath)]\npub(crate) struct YoleckLevelIndexLoader;\n\nimpl AssetLoader for YoleckLevelIndexLoader {\n    type Asset = YoleckLevelIndex;\n    type Settings = ();\n    type Error = YoleckAssetLoaderError;\n\n    async fn load(\n        &self,\n        reader: &mut dyn bevy::asset::io::Reader,\n        _settings: &Self::Settings,\n        _load_context: &mut bevy::asset::LoadContext<'_>,\n    ) -> Result<Self::Asset, Self::Error> {\n        let mut bytes = Vec::new();\n        reader.read_to_end(&mut bytes).await?;\n        let json = std::str::from_utf8(&bytes)?;\n        let level_index: YoleckLevelIndex = serde_json::from_str(json)?;\n        Ok(level_index)\n    }\n\n    fn extensions(&self) -> &[&str] {\n        &[\"yoli\"]\n    }\n}\n\n/// Accessible only to edit systems - provides information about available levels.\n#[derive(Resource)]\npub struct YoleckEditableLevels {\n    pub(crate) levels: BTreeSet<String>,\n}\n\nimpl YoleckEditableLevels {\n    /// The names of the level files (relative to the levels directory, not the assets directory)\n    pub fn names(&self) -> impl Iterator<Item = &str> {\n        self.levels.iter().map(|l| l.as_str())\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! # Your Own Level Editor Creation Kit\n//!\n//! Yoleck is a crate for having a game built with the Bevy game engine act as its own level\n//! editor.\n//!\n//! Yoleck uses Plain Old Rust Structs to store the data, and uses Serde to store them in files.\n//! The user code defines _populate systems_ for creating Bevy entities (populating their\n//! components) from these structs and _edit systems_ to edit these structs with egui.\n//!\n//! The synchronization between the structs and the files is bidirectional, and so is the\n//! synchronization between the structs and the egui widgets, but the synchronization from the\n//! structs to the entities is unidirectional - changes in the entities are not reflected in the\n//! structs:\n//!\n//! ```none\n//! ┌────────┐  Populate   ┏━━━━━━━━━┓   Edit      ┌───────┐\n//! │Bevy    │  Systems    ┃Yoleck   ┃   Systems   │egui   │\n//! │Entities│◄────────────┃Component┃◄═══════════►│Widgets│\n//! └────────┘             ┃Structs  ┃             └───────┘\n//!                        ┗━━━━━━━━━┛\n//!                            ▲\n//!                            ║\n//!                            ║ Serde\n//!                            ║\n//!                            ▼\n//!                          ┌─────┐\n//!                          │.yol │\n//!                          │Files│\n//!                          └─────┘\n//! ```\n//!\n//! To support integrate Yoleck, a game needs to:\n//!\n//! * Define the component structs, and make sure they implement:\n//!   ```text\n//!   #[derive(Default, Clone, PartialEq, Component, Serialize, Deserialize, YoleckComponent)]\n//!   ```\n//! * For each entity type that can be created in the level editor, use\n//!   [`add_yoleck_entity_type`](YoleckExtForApp::add_yoleck_entity_type) to add a\n//!   [`YoleckEntityType`]. Use [`YoleckEntityType::with`] to register the\n//!   [`YoleckComponent`](crate::specs_registration::YoleckComponent)s for that entity type.\n//! * Register edit systems with\n//!   [`add_yoleck_edit_system`](YoleckExtForApp::add_yoleck_edit_system).\n//! * Register populate systems on [`YoleckSchedule::Populate`]\n//! * If the application starts in editor mode:\n//!   * Add the `EguiPlugin` plugin.\n//!   * Add the [`YoleckPluginForEditor`] plugin.\n//!   * Use [`YoleckSyncWithEditorState`](crate::editor::YoleckSyncWithEditorState) to synchronize\n//!     the game's state with the [`YoleckEditorState`] (optional but highly recommended)\n//! * If the application starts in game mode:\n//!   * Add the [`YoleckPluginForGame`] plugin.\n//!   * Use the [`YoleckLevelIndex`] asset to determine the list of available levels (optional)\n//!   * Spawn an entity with the [`YoleckLoadLevel`](entity_management::YoleckLoadLevel) component\n//!     to load the level. Note that the level can be unloaded by despawning that entity or by\n//!     removing the [`YoleckKeepLevel`] component that will automatically be added to it.\n//!\n//! To support picking and moving entities in the viewport with the mouse, check out the\n//! [`vpeol_2d`] and [`vpeol_3d`] modules. After adding the appropriate feature flag\n//! (`vpeol_2d`/`vpeol_3d`), import their types from\n//! [`bevy_yoleck::vpeol::prelude::*`](crate::vpeol::prelude).\n//!\n//! # Example\n//!\n//! ```no_run\n//! use bevy::prelude::*;\n//! use bevy_yoleck::bevy_egui::EguiPlugin;\n//! use bevy_yoleck::prelude::*;\n//! use serde::{Deserialize, Serialize};\n//! # use bevy_yoleck::egui;\n//!\n//! fn main() {\n//!     let is_editor = std::env::args().any(|arg| arg == \"--editor\");\n//!\n//!     let mut app = App::new();\n//!     app.add_plugins(DefaultPlugins);\n//!     if is_editor {\n//!         // Doesn't matter in this example, but a proper game would have systems that can work\n//!         // on the entity in `GameState::Game`, so while the level is edited we want to be in\n//!         // `GameState::Editor` - which can be treated as a pause state. When the editor wants\n//!         // to playtest the level we want to move to `GameState::Game` so that they can play it.\n//!         app.add_plugins(EguiPlugin::default());\n//!         app.add_plugins(YoleckSyncWithEditorState {\n//!             when_editor: GameState::Editor,\n//!             when_game: GameState::Game,\n//!         });\n//!         app.add_plugins(YoleckPluginForEditor);\n//!     } else {\n//!         app.add_plugins(YoleckPluginForGame);\n//!         app.init_state::<GameState>();\n//!         // In editor mode Yoleck takes care of level loading. In game mode the game needs to\n//!         // tell yoleck which levels to load and when.\n//!         app.add_systems(Update, load_first_level.run_if(in_state(GameState::Loading)));\n//!     }\n//!     app.add_systems(Startup, setup_camera);\n//!\n//!     app.add_yoleck_entity_type({\n//!         YoleckEntityType::new(\"Rectangle\")\n//!             .with::<Rectangle>()\n//!     });\n//!     app.add_yoleck_edit_system(edit_rectangle);\n//!     app.add_systems(YoleckSchedule::Populate, populate_rectangle);\n//!\n//!     app.run();\n//! }\n//!\n//! #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]\n//! enum GameState {\n//!     #[default]\n//!     Loading,\n//!     Game,\n//!     Editor,\n//! }\n//!\n//! fn setup_camera(mut commands: Commands) {\n//!     commands.spawn(Camera2d::default());\n//! }\n//!\n//! #[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n//! struct Rectangle {\n//!     width: f32,\n//!     height: f32,\n//! }\n//!\n//! impl Default for Rectangle {\n//!     fn default() -> Self {\n//!         Self {\n//!             width: 50.0,\n//!             height: 50.0,\n//!         }\n//!     }\n//! }\n//!\n//! fn populate_rectangle(mut populate: YoleckPopulate<&Rectangle>) {\n//!     populate.populate(|_ctx, mut cmd, rectangle| {\n//!         cmd.insert(Sprite {\n//!             color: bevy::color::palettes::css::RED.into(),\n//!             custom_size: Some(Vec2::new(rectangle.width, rectangle.height)),\n//!             ..Default::default()\n//!         });\n//!     });\n//! }\n//!\n//! fn edit_rectangle(mut ui: ResMut<YoleckUi>, mut edit: YoleckEdit<&mut Rectangle>) {\n//!     let Ok(mut rectangle) = edit.single_mut() else { return };\n//!     ui.add(egui::Slider::new(&mut rectangle.width, 50.0..=500.0).prefix(\"Width: \"));\n//!     ui.add(egui::Slider::new(&mut rectangle.height, 50.0..=500.0).prefix(\"Height: \"));\n//! }\n//!\n//! fn load_first_level(\n//!     mut level_index_handle: Local<Option<Handle<YoleckLevelIndex>>>,\n//!     asset_server: Res<AssetServer>,\n//!     level_index_assets: Res<Assets<YoleckLevelIndex>>,\n//!     mut commands: Commands,\n//!     mut game_state: ResMut<NextState<GameState>>,\n//! ) {\n//!     // Keep the handle in local resource, so that Bevy will not unload the level index asset\n//!     // between frames.\n//!     let level_index_handle = level_index_handle\n//!         .get_or_insert_with(|| asset_server.load(\"levels/index.yoli\"))\n//!         .clone();\n//!     let Some(level_index) = level_index_assets.get(&level_index_handle) else {\n//!         // During the first invocation of this system, the level index asset is not going to be\n//!         // loaded just yet. Since this system is going to run on every frame during the Loading\n//!         // state, it just has to keep trying until it starts in a frame where it is loaded.\n//!         return;\n//!     };\n//!     // A proper game would have a proper level progression system, but here we are just\n//!     // taking the first level and loading it.\n//!     let level_handle: Handle<YoleckRawLevel> =\n//!         asset_server.load(&format!(\"levels/{}\", level_index[0].filename));\n//!     commands.spawn(YoleckLoadLevel(level_handle));\n//!     game_state.set(GameState::Game);\n//! }\n//! ```\n\npub mod auto_edit;\nmod console;\nmod editing;\nmod editor;\nmod editor_panels;\nmod editor_window;\nmod entity_management;\npub mod entity_ref;\nmod entity_upgrading;\nmod entity_uuid;\nmod errors;\npub mod exclusive_systems;\npub mod knobs;\nmod level_files_manager;\npub mod level_files_upgrading;\nmod level_index;\nmod picking_helpers;\nmod populating;\nmod specs_registration;\nmod util;\n#[cfg(feature = \"vpeol\")]\npub mod vpeol;\n#[cfg(feature = \"vpeol_2d\")]\npub mod vpeol_2d;\n#[cfg(feature = \"vpeol_3d\")]\npub mod vpeol_3d;\n\nuse std::any::{Any, TypeId};\nuse std::path::Path;\nuse std::sync::Arc;\n\nuse bevy::ecs::schedule::ScheduleLabel;\nuse bevy::ecs::system::{EntityCommands, SystemId};\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\nuse bevy_egui::EguiPrimaryContextPass;\n\npub mod prelude {\n    pub use crate::auto_edit::{YoleckAutoEdit, YoleckAutoEditExt};\n    pub use crate::editing::{YoleckEdit, YoleckUi};\n    pub use crate::editor::{YoleckEditorState, YoleckPassedData, YoleckSyncWithEditorState};\n    pub use crate::entity_management::{YoleckKeepLevel, YoleckLoadLevel, YoleckRawLevel};\n    pub use crate::entity_ref::{YoleckEntityRef, YoleckEntityRefAccessor};\n    pub use crate::entity_upgrading::YoleckEntityUpgradingPlugin;\n    pub use crate::entity_uuid::{YoleckEntityUuid, YoleckUuidRegistry};\n    pub use crate::knobs::YoleckKnobs;\n    pub use crate::level_index::{YoleckLevelIndex, YoleckLevelIndexEntry};\n    pub use crate::populating::{YoleckMarking, YoleckPopulate};\n    pub use crate::specs_registration::{YoleckComponent, YoleckEntityType};\n    pub use crate::{\n        YoleckBelongsToLevel, YoleckExtForApp, YoleckLevelInEditor, YoleckLevelInPlaytest,\n        YoleckLevelJustLoaded, YoleckPluginForEditor, YoleckPluginForGame, YoleckSchedule,\n    };\n    pub use bevy_yoleck_macros::{YoleckAutoEdit, YoleckComponent};\n}\n\npub use self::console::{YoleckConsoleLogHistory, YoleckConsoleState, console_layer_factory};\npub use self::editing::YoleckEditMarker;\npub use self::editor::YoleckDirective;\npub use self::editor::YoleckEditorEvent;\nuse self::editor::YoleckEditorState;\npub use self::editor_panels::{\n    YoleckEditorBottomPanelSections, YoleckEditorBottomPanelTab, YoleckEditorLeftPanelSections,\n    YoleckEditorRightPanelSections, YoleckEditorTopPanelSections, YoleckPanelUi,\n};\npub use self::editor_window::YoleckEditorViewportRect;\npub use self::picking_helpers::*;\n\nuse self::entity_management::{EntitiesToPopulate, YoleckRawLevel};\nuse self::entity_upgrading::YoleckEntityUpgrading;\nuse self::exclusive_systems::YoleckExclusiveSystemsPlugin;\nuse self::knobs::YoleckKnobsCache;\npub use self::level_files_manager::YoleckEditorLevelsDirectoryPath;\npub use self::level_index::YoleckEditableLevels;\nuse self::level_index::YoleckLevelIndex;\npub use self::populating::{YoleckPopulateContext, YoleckSystemMarker};\nuse self::prelude::{YoleckKeepLevel, YoleckUuidRegistry};\nuse self::specs_registration::{YoleckComponentHandler, YoleckEntityType};\nuse self::util::EditSpecificResources;\npub use bevy_egui;\npub use bevy_egui::egui;\n\nstruct YoleckPluginBase;\npub struct YoleckPluginForGame;\npub struct YoleckPluginForEditor;\n\n#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemSet)]\nenum YoleckSystems {\n    ProcessRawEntities,\n    RunPopulateSchedule,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemSet)]\npub(crate) struct YoleckRunEditSystems;\n\nimpl Plugin for YoleckPluginBase {\n    fn build(&self, app: &mut App) {\n        app.init_resource::<YoleckEntityConstructionSpecs>();\n        app.insert_resource(YoleckUuidRegistry(Default::default()));\n        app.register_asset_loader(entity_management::YoleckLevelAssetLoader);\n        app.init_asset::<YoleckRawLevel>();\n        app.register_asset_loader(level_index::YoleckLevelIndexLoader);\n        app.init_asset::<YoleckLevelIndex>();\n\n        app.configure_sets(\n            Update,\n            (\n                YoleckSystems::ProcessRawEntities,\n                YoleckSystems::RunPopulateSchedule,\n            )\n                .chain(),\n        );\n\n        app.add_systems(\n            Update,\n            (\n                entity_management::yoleck_process_raw_entries,\n                ApplyDeferred,\n                (\n                    entity_management::yoleck_run_post_load_resolutions_schedule,\n                    entity_management::yoleck_run_level_loaded_schedule.run_if(\n                        |freshly_loaded_level_entities: Query<\n                            (),\n                            (With<YoleckLevelJustLoaded>, Without<YoleckLevelInEditor>),\n                        >| { !freshly_loaded_level_entities.is_empty() },\n                    ),\n                    entity_management::yoleck_remove_just_loaded_marker_from_levels,\n                    ApplyDeferred,\n                )\n                    .chain()\n                    .run_if(\n                        |freshly_loaded_level_entities: Query<(), With<YoleckLevelJustLoaded>>| {\n                            !freshly_loaded_level_entities.is_empty()\n                        },\n                    ),\n            )\n                .chain()\n                .in_set(YoleckSystems::ProcessRawEntities),\n        );\n        app.insert_resource(EntitiesToPopulate(Default::default()));\n        app.add_systems(\n            Update,\n            (\n                entity_management::yoleck_prepare_populate_schedule,\n                entity_management::yoleck_run_populate_schedule.run_if(\n                    |entities_to_populate: Res<EntitiesToPopulate>| {\n                        !entities_to_populate.0.is_empty()\n                    },\n                ),\n            )\n                .chain()\n                .in_set(YoleckSystems::RunPopulateSchedule),\n        );\n        app.add_systems(\n            Update,\n            ((\n                entity_management::process_unloading_command,\n                entity_management::process_loading_command,\n                ApplyDeferred,\n            )\n                .chain()\n                .before(YoleckSystems::ProcessRawEntities),),\n        );\n        app.add_schedule(Schedule::new(YoleckSchedule::Populate));\n        app.add_schedule(Schedule::new(YoleckInternalSchedule::PostLoadResolutions));\n        app.add_schedule(Schedule::new(YoleckSchedule::LevelLoaded));\n        app.add_schedule(Schedule::new(YoleckSchedule::OverrideCommonComponents));\n    }\n}\n\nimpl Plugin for YoleckPluginForGame {\n    fn build(&self, app: &mut App) {\n        app.init_state::<YoleckEditorState>();\n        app.add_systems(\n            Startup,\n            |mut state: ResMut<NextState<YoleckEditorState>>| {\n                state.set(YoleckEditorState::GameActive);\n            },\n        );\n        app.add_plugins(YoleckPluginBase);\n    }\n}\n\nimpl Plugin for YoleckPluginForEditor {\n    fn build(&self, app: &mut App) {\n        app.init_state::<YoleckEditorState>();\n        app.add_message::<YoleckEditorEvent>();\n        app.add_plugins(YoleckPluginBase);\n        app.add_plugins(YoleckExclusiveSystemsPlugin);\n        app.init_resource::<YoleckEditSystems>();\n        app.insert_resource(YoleckKnobsCache::default());\n        let level_being_edited = app\n            .world_mut()\n            .spawn((YoleckLevelInEditor, YoleckKeepLevel))\n            .id();\n        app.insert_resource(YoleckState {\n            level_being_edited,\n            level_needs_saving: false,\n        });\n        app.insert_resource(YoleckEditorLevelsDirectoryPath(\n            Path::new(\".\").join(\"assets\").join(\"levels\"),\n        ));\n        app.init_resource::<YoleckEditorLeftPanelSections>();\n        app.init_resource::<YoleckEditorRightPanelSections>();\n        app.init_resource::<YoleckEditorTopPanelSections>();\n        app.init_resource::<YoleckEditorBottomPanelSections>();\n        app.init_resource::<YoleckEditorViewportRect>();\n        app.init_resource::<YoleckConsoleState>();\n        app.init_resource::<YoleckConsoleLogHistory>();\n        app.init_resource::<YoleckPlaytestLevel>();\n        app.insert_resource(EditSpecificResources::new().with(YoleckEditableLevels {\n            levels: Default::default(),\n        }));\n        app.add_message::<YoleckDirective>();\n        app.configure_sets(\n            Update,\n            YoleckRunEditSystems.after(YoleckSystems::ProcessRawEntities),\n        );\n        app.add_systems(\n            EguiPrimaryContextPass,\n            editor_window::yoleck_editor_window.in_set(YoleckRunEditSystems),\n        );\n\n        app.add_schedule(Schedule::new(\n            YoleckInternalSchedule::UpdateManagedDataFromComponents,\n        ));\n    }\n}\n\npub trait YoleckExtForApp {\n    /// Add a type of entity that can be edited in Yoleck's level editor.\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;\n    /// # use serde::{Deserialize, Serialize};\n    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n    /// # struct Component1;\n    /// # type Component2 = Component1;\n    /// # type Component3 = Component1;\n    /// # let mut app = App::new();\n    /// app.add_yoleck_entity_type({\n    ///     YoleckEntityType::new(\"MyEntityType\")\n    ///         .with::<Component1>()\n    ///         .with::<Component2>()\n    ///         .with::<Component3>()\n    /// });\n    /// ```\n    fn add_yoleck_entity_type(&mut self, entity_type: YoleckEntityType);\n\n    /// Add a system for editing Yoleck components in the level editor.\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;\n    /// # use serde::{Deserialize, Serialize};\n    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n    /// # struct Component1;\n    /// # let mut app = App::new();\n    ///\n    /// app.add_yoleck_edit_system(edit_component1);\n    ///\n    /// fn edit_component1(mut ui: ResMut<YoleckUi>, mut edit: YoleckEdit<&mut Component1>) {\n    ///     let Ok(component1) = edit.single_mut() else { return };\n    ///     // Edit `component1` with the `ui`\n    /// }\n    /// ```\n    ///\n    /// See [`YoleckEdit`](crate::editing::YoleckEdit).\n    fn add_yoleck_edit_system<P>(&mut self, system: impl 'static + IntoSystem<(), (), P>);\n\n    /// Register a function that upgrades entities from a previous version of the app format.\n    ///\n    /// This should only be called _after_ adding\n    /// [`YoleckEntityUpgradingPlugin`](crate::entity_upgrading::YoleckEntityUpgradingPlugin). See\n    /// that plugin's docs for more info.\n    fn add_yoleck_entity_upgrade(\n        &mut self,\n        to_version: usize,\n        upgrade_dlg: impl 'static\n        + Send\n        + Sync\n        + Fn(&str, &mut serde_json::Map<String, serde_json::Value>),\n    );\n\n    /// Register a function that upgrades entities of a specific type from a previous version of\n    /// the app format.\n    fn add_yoleck_entity_upgrade_for(\n        &mut self,\n        to_version: usize,\n        for_type_name: impl ToString,\n        upgrade_dlg: impl 'static + Send + Sync + Fn(&mut serde_json::Map<String, serde_json::Value>),\n    ) {\n        let for_type_name = for_type_name.to_string();\n        self.add_yoleck_entity_upgrade(to_version, move |type_name, data| {\n            if type_name == for_type_name {\n                upgrade_dlg(data);\n            }\n        });\n    }\n}\n\nimpl YoleckExtForApp for App {\n    fn add_yoleck_entity_type(&mut self, entity_type: YoleckEntityType) {\n        let construction_specs = self\n            .world_mut()\n            .get_resource_or_insert_with(YoleckEntityConstructionSpecs::default);\n\n        let mut component_type_ids = Vec::with_capacity(entity_type.components.len());\n        let mut component_handlers_to_register = Vec::new();\n        for handler in entity_type.components.into_iter() {\n            component_type_ids.push(handler.component_type());\n            if !construction_specs\n                .component_handlers\n                .contains_key(&handler.component_type())\n            {\n                component_handlers_to_register.push(handler);\n            }\n        }\n\n        for handler in component_handlers_to_register.iter() {\n            handler.build_in_bevy_app(self);\n        }\n\n        let new_entry = YoleckEntityTypeInfo {\n            name: entity_type.name.clone(),\n            components: component_type_ids,\n            on_init: entity_type.on_init,\n            has_uuid: entity_type.has_uuid,\n        };\n\n        let mut construction_specs = self\n            .world_mut()\n            .get_resource_mut::<YoleckEntityConstructionSpecs>()\n            .expect(\"YoleckEntityConstructionSpecs was inserted earlier in this function\");\n\n        let new_index = construction_specs.entity_types.len();\n        construction_specs\n            .entity_types_index\n            .insert(entity_type.name, new_index);\n        construction_specs.entity_types.push(new_entry);\n        for handler in component_handlers_to_register {\n            // Can handlers can register systems? If so, this needs to be broken into two phases...\n            construction_specs\n                .component_handlers\n                .insert(handler.component_type(), handler);\n        }\n    }\n\n    fn add_yoleck_edit_system<P>(&mut self, system: impl 'static + IntoSystem<(), (), P>) {\n        let system_id = self.world_mut().register_system(system);\n        let mut edit_systems = self\n            .world_mut()\n            .get_resource_or_insert_with(YoleckEditSystems::default);\n        edit_systems.edit_systems.push(system_id);\n    }\n\n    fn add_yoleck_entity_upgrade(\n        &mut self,\n        to_version: usize,\n        upgrade_dlg: impl 'static\n        + Send\n        + Sync\n        + Fn(&str, &mut serde_json::Map<String, serde_json::Value>),\n    ) {\n        let mut entity_upgrading = self.world_mut().get_resource_mut::<YoleckEntityUpgrading>()\n            .expect(\"add_yoleck_entity_upgrade can only be called after the YoleckEntityUpgrading plugin was added\");\n        if entity_upgrading.app_format_version < to_version {\n            panic!(\n                \"Cannot create an upgrade system to version {} when YoleckEntityUpgrading set the version to {}\",\n                to_version, entity_upgrading.app_format_version\n            );\n        }\n        entity_upgrading\n            .upgrade_functions\n            .entry(to_version)\n            .or_default()\n            .push(Box::new(upgrade_dlg));\n    }\n}\n\ntype BoxedArc = Arc<dyn Send + Sync + Any>;\ntype BoxedAny = Box<dyn Send + Sync + Any>;\n\n/// A component that describes how Yoleck manages an entity under its control.\n#[derive(Component)]\npub struct YoleckManaged {\n    /// A name to display near the entity in the entities list.\n    ///\n    /// This is for level editors' convenience only - it will not be used in the games.\n    pub name: String,\n\n    /// The type of the Yoleck entity, as registered with\n    /// [`add_yoleck_entity_type`](YoleckExtForApp::add_yoleck_entity_type).\n    ///\n    /// This defines the Yoleck components that can be edited for the entity.\n    pub type_name: String,\n\n    lifecycle_status: YoleckEntityLifecycleStatus,\n\n    pub(crate) components_data: HashMap<TypeId, BoxedAny>,\n}\n\n/// A marker for entities that belongs to the Yoleck level and should be despawned with it.\n///\n/// Yoleck already adds this automatically to entities created from the editor. The game itself\n/// should add this to entities created during gameplay, like bullets or spawned enemeis, so that\n/// they'll be despawned when a playtest is finished or restarted.\n///\n/// When removing a [`YoleckKeepLevel`] from entity (or removing the entire entity), Yoleck will\n/// automatically despawn all the entities that have this component and point to that level.\n///\n/// There is no need to add this to child entities of entities that already has this marker,\n/// because Bevy will already despawn them when despawning their parent.\n#[derive(Component, Debug, Clone)]\npub struct YoleckBelongsToLevel {\n    /// The entity which was used with [`YoleckLoadLevel`](entity_management::YoleckLoadLevel) to\n    /// load the level that this entity belongs to.\n    pub level: Entity,\n}\n\npub enum YoleckEntityLifecycleStatus {\n    Synchronized,\n    JustCreated,\n    JustChanged,\n}\n\n#[derive(Default, Resource)]\nstruct YoleckEditSystems {\n    edit_systems: Vec<SystemId>,\n}\n\nimpl YoleckEditSystems {\n    pub(crate) fn run_systems(&mut self, world: &mut World) {\n        for system_id in self.edit_systems.iter() {\n            world\n                .run_system(*system_id)\n                .expect(\"edit systems handled by Yoleck - system should been properly handled\");\n        }\n    }\n}\n\npub(crate) struct YoleckEntityTypeInfo {\n    pub name: String,\n    pub components: Vec<TypeId>,\n    #[allow(clippy::type_complexity)]\n    pub(crate) on_init:\n        Vec<Box<dyn 'static + Sync + Send + Fn(YoleckEditorState, &mut EntityCommands)>>,\n    pub has_uuid: bool,\n}\n\n#[derive(Default, Resource)]\npub(crate) struct YoleckEntityConstructionSpecs {\n    pub entity_types: Vec<YoleckEntityTypeInfo>,\n    pub entity_types_index: HashMap<String, usize>,\n    pub component_handlers: HashMap<TypeId, Box<dyn YoleckComponentHandler>>,\n}\n\nimpl YoleckEntityConstructionSpecs {\n    pub fn get_entity_type_info(&self, entity_type: &str) -> Option<&YoleckEntityTypeInfo> {\n        Some(&self.entity_types[*self.entity_types_index.get(entity_type)?])\n    }\n}\n\n/// Fields of the Yoleck editor.\n#[derive(Resource)]\npub(crate) struct YoleckState {\n    level_being_edited: Entity,\n    level_needs_saving: bool,\n}\n\n/// The level currently being playtested, if any.\n#[derive(Default, Resource)]\npub struct YoleckPlaytestLevel(pub Option<YoleckRawLevel>);\n\n#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]\npub(crate) enum YoleckInternalSchedule {\n    UpdateManagedDataFromComponents,\n    /// Before [`LevelLoaded`][YoleckSchedule::LevelLoaded] to resolve things like entity\n    /// references.\n    PostLoadResolutions,\n}\n\n/// Schedules for user code to do the actual entity/level population after Yoleck spawns the level\n/// \"skeleton\".\n#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]\npub enum YoleckSchedule {\n    /// This is where user defined populate systems should reside.\n    ///\n    /// Note that populate systems, rather than directly trying to query the entities to be\n    /// populated, should use [`YoleckPopulate`](crate::prelude::YoleckPopulate):\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;\n    /// # use serde::{Deserialize, Serialize};\n    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n    /// # struct Component1;\n    /// # let mut app = App::new();\n    ///\n    /// app.add_systems(YoleckSchedule::Populate, populate_component1);\n    ///\n    /// fn populate_component1(mut populate: YoleckPopulate<&Component1>) {\n    ///     populate.populate(|_ctx, mut cmd, component1| {\n    ///         // Add Bevy components derived from `component1` to `cmd`.\n    ///     });\n    /// }\n    /// ```\n    Populate,\n    /// Right after all the level entities are loaded, but before any populate systems manage to\n    /// run.\n    LevelLoaded,\n    /// Since many bundles add their own transform and visibility components, systems that override\n    /// them explicitly need to go here.\n    OverrideCommonComponents,\n}\n\n/// Automatically added to level entities that are being edited in the level editor.\n#[derive(Component)]\npub struct YoleckLevelInEditor;\n\n/// Automatically added to level entities that are being play-tested in the level editor.\n///\n/// Note that this only gets added to the levels that are launched from the editor UI. If game\n/// systems load new levels during the play-test, this component will not be added to them.\n#[derive(Component)]\npub struct YoleckLevelInPlaytest;\n\n/// During the [`YoleckSchedule::LevelLoaded`] schedule, this component marks the level entities\n/// that were just loaded and triggered that schedule.\n///\n/// Note that this component will be removed after that schedule finishes running - it should not\n/// be relied on in systems outside that schedule.\n#[derive(Component)]\npub struct YoleckLevelJustLoaded;\n"
  },
  {
    "path": "src/picking_helpers.rs",
    "content": "use bevy::prelude::*;\nuse uuid::Uuid;\n\nuse crate::exclusive_systems::YoleckExclusiveSystemDirective;\nuse crate::prelude::*;\n\n/// Transforms an entity to its UUID. Meant to be used with [Yoleck's exclusive edit\n/// systems](crate::exclusive_systems::YoleckExclusiveSystemsQueue) and with Bevy's system piping.\n///\n/// It accepts and returns an `Option` because it is meant to be used with\n/// [`vpeol_read_click_on_entity`](crate::vpeol::vpeol_read_click_on_entity).\npub fn yoleck_map_entity_to_uuid(\n    In(entity): In<Option<Entity>>,\n    uuid_query: Query<&YoleckEntityUuid>,\n) -> Option<Uuid> {\n    Some(uuid_query.get(entity?).ok()?.get())\n}\n\n/// Pipe an [exclusive system](crate::exclusive_systems::YoleckExclusiveSystemsQueue) into this\n/// system to make it cancellable by either pressing the Escape key or clicking on a button in the\n/// UI.\npub fn yoleck_exclusive_system_cancellable(\n    In(directive): In<YoleckExclusiveSystemDirective>,\n    mut ui: ResMut<YoleckUi>,\n    keyboard: Res<ButtonInput<KeyCode>>,\n) -> YoleckExclusiveSystemDirective {\n    if matches!(directive, YoleckExclusiveSystemDirective::Finished) {\n        return directive;\n    }\n\n    if keyboard.just_released(KeyCode::Escape) || ui.button(\"Abort Entity Selection\").clicked() {\n        return YoleckExclusiveSystemDirective::Finished;\n    }\n\n    directive\n}\n"
  },
  {
    "path": "src/populating.rs",
    "content": "use std::ops::RangeFrom;\n\nuse bevy::ecs::query::{QueryData, QueryFilter};\nuse bevy::ecs::system::{EntityCommands, SystemParam};\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\n\nuse crate::entity_management::EntitiesToPopulate;\n\n/// Wrapper for writing queries in populate systems.\n#[derive(SystemParam)]\npub struct YoleckPopulate<'w, 's, Q: 'static + QueryData, F: 'static + QueryFilter = ()> {\n    entities_to_populate: Res<'w, EntitiesToPopulate>,\n    query: Query<'w, 's, Q, F>,\n    commands: Commands<'w, 's>,\n}\n\nimpl<Q: 'static + QueryData, F: 'static + QueryFilter> YoleckPopulate<'_, '_, Q, F> {\n    /// Iterate over the entities that need populating in order to add/update components using\n    /// a Bevy command.\n    pub fn populate(\n        &mut self,\n        mut dlg: impl FnMut(YoleckPopulateContext, EntityCommands, <Q as QueryData>::Item<'_, '_>),\n    ) {\n        for (entity, populate_reason) in self.entities_to_populate.0.iter() {\n            if let Ok(data) = self.query.get_mut(*entity) {\n                let cmd = self.commands.entity(*entity);\n                let context = YoleckPopulateContext {\n                    reason: *populate_reason,\n                };\n                dlg(context, cmd, data);\n            }\n        }\n    }\n}\n\n#[derive(Clone, Copy)]\npub(crate) enum PopulateReason {\n    EditorInit,\n    EditorUpdate,\n    RealGame,\n}\n\n/// A context for [`YoleckPopulate::populate`].\npub struct YoleckPopulateContext {\n    pub(crate) reason: PopulateReason,\n}\n\nimpl YoleckPopulateContext {\n    /// `true` if the entity is created in editor mode, `false` if created in playtest or actual game.\n    pub fn is_in_editor(&self) -> bool {\n        match self.reason {\n            PopulateReason::EditorInit => true,\n            PopulateReason::EditorUpdate => true,\n            PopulateReason::RealGame => false,\n        }\n    }\n\n    /// `true` if this is this is the first time the entity is populated, `false` if the entity was\n    /// popultated before.\n    pub fn is_first_time(&self) -> bool {\n        match self.reason {\n            PopulateReason::EditorInit => true,\n            PopulateReason::EditorUpdate => false,\n            PopulateReason::RealGame => true,\n        }\n    }\n}\n\n/// See [`YoleckMarking`].\n#[derive(Debug, Component, PartialEq, Eq, Clone, Copy)]\npub struct YoleckSystemMarker(usize);\n\n#[derive(Resource)]\nstruct MarkerGenerator(RangeFrom<usize>);\n\nimpl FromWorld for YoleckSystemMarker {\n    fn from_world(world: &mut World) -> Self {\n        let mut marker = world.get_resource_or_insert_with(|| MarkerGenerator(1..));\n        YoleckSystemMarker(marker.0.next().unwrap())\n    }\n}\n\n/// Use to mark child entities created from a specific system.\n///\n/// Using `despawn_descendants` is dangerous because it can delete entities created by other\n/// populate systems in the same frame. Instead, a populate system that wants to despawn its own\n/// child entities should do something like this:\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::prelude::*;\n/// # use serde::{Deserialize, Serialize};\n/// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n/// # struct MyComponent;\n/// fn populate_system(mut populate: YoleckPopulate<&MyComponent>, marking: YoleckMarking) {\n///     populate.populate(|_ctx, mut cmd, my_component| {\n///         marking.despawn_marked(&mut cmd);\n///         cmd.with_children(|commands| {\n///             let mut child = commands.spawn(marking.marker());\n///             child.insert((\n///                 // relevant Bevy components\n///             ));\n///         });\n///     });\n/// }\n/// ```\n#[derive(SystemParam)]\npub struct YoleckMarking<'w, 's> {\n    designated_marker: Local<'s, YoleckSystemMarker>,\n    children_query: Query<'w, 's, &'static Children>,\n    marked_query: Query<'w, 's, (&'static ChildOf, &'static YoleckSystemMarker)>,\n}\n\nimpl YoleckMarking<'_, '_> {\n    /// Create a marker unique to this system.\n    pub fn marker(&self) -> YoleckSystemMarker {\n        *self.designated_marker\n    }\n\n    /// Despawn all the entities marked by the current system, that are descendants of the entity\n    /// edited by the supplied `cmd`.\n    pub fn despawn_marked(&self, cmd: &mut EntityCommands) {\n        let mut marked_children_map: HashMap<Entity, Vec<Entity>> = Default::default();\n        for child in self.children_query.iter_descendants(cmd.id()) {\n            let Ok((child_of, marker)) = self.marked_query.get(child) else {\n                continue;\n            };\n            if *marker == *self.designated_marker {\n                marked_children_map\n                    .entry(child_of.parent())\n                    .or_default()\n                    .push(child);\n            }\n        }\n        for (parent, children) in marked_children_map {\n            cmd.commands().entity(parent).detach_children(&children);\n            for child in children {\n                cmd.commands().entity(child).despawn();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/specs_registration.rs",
    "content": "use std::any::{Any, TypeId};\nuse std::marker::PhantomData;\n\nuse bevy::ecs::component::Mutable;\nuse bevy::ecs::system::EntityCommands;\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nuse crate::prelude::YoleckEditorState;\nuse crate::{BoxedAny, YoleckEntityLifecycleStatus, YoleckInternalSchedule, YoleckManaged};\n\n/// A component that Yoleck will write to and read from `.yol` files.\n///\n/// Rather than being used for general ECS behavior definition, `YoleckComponent`s should be used\n/// for spawning the actual components using [populate\n/// systems](crate::prelude::YoleckSchedule::Populate).\npub trait YoleckComponent:\n    Default + Clone + PartialEq + Component<Mutability = Mutable> + Serialize + for<'a> Deserialize<'a>\n{\n    const KEY: &'static str;\n}\n\n/// A type of entity that can be created and edited with the Yoleck level editor.\n///\n/// Yoleck will only read and write the components registered on the entity type with the\n/// [`with`](YoleckEntityType::with) method, even if the file has data of other components or if\n/// the Bevy entity has other [`YoleckComponent`]s inserted to it. These components will still take\n/// effect in edit and populate systems though, even if they are not registered on the entity.\npub struct YoleckEntityType {\n    /// The `type_name` used to identify the entity type.\n    pub name: String,\n    pub(crate) components: Vec<Box<dyn YoleckComponentHandler>>,\n    #[allow(clippy::type_complexity)]\n    pub(crate) on_init:\n        Vec<Box<dyn 'static + Sync + Send + Fn(YoleckEditorState, &mut EntityCommands)>>,\n    pub has_uuid: bool,\n}\n\nimpl YoleckEntityType {\n    pub fn new(name: impl ToString) -> Self {\n        Self {\n            name: name.to_string(),\n            components: Default::default(),\n            on_init: Default::default(),\n            has_uuid: false,\n        }\n    }\n\n    /// Register a [`YoleckComponent`] for entities of this type.\n    pub fn with<T: YoleckComponent>(mut self) -> Self {\n        self.components\n            .push(Box::<YoleckComponentHandlerImpl<T>>::default());\n        self\n    }\n\n    /// Automatically spawn regular Bevy components when creating entities of this type.\n    ///\n    /// This is useful for marker components that don't carry data that needs to be saved to files.\n    pub fn insert_on_init<T: Bundle>(\n        mut self,\n        bundle_maker: impl 'static + Sync + Send + Fn() -> T,\n    ) -> Self {\n        self.on_init.push(Box::new(move |_, cmd| {\n            cmd.insert(bundle_maker());\n        }));\n        self\n    }\n\n    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in the\n    /// editor. Will not be added during playtests or actual game.\n    pub fn insert_on_init_during_editor<T: Bundle>(\n        mut self,\n        bundle_maker: impl 'static + Sync + Send + Fn() -> T,\n    ) -> Self {\n        self.on_init.push(Box::new(move |editor_state, cmd| {\n            if matches!(editor_state, YoleckEditorState::EditorActive) {\n                cmd.insert(bundle_maker());\n            }\n        }));\n        self\n    }\n\n    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in\n    /// playtests or the actual game. Will not be added in the editor.\n    pub fn insert_on_init_during_game<T: Bundle>(\n        mut self,\n        bundle_maker: impl 'static + Sync + Send + Fn() -> T,\n    ) -> Self {\n        self.on_init.push(Box::new(move |editor_state, cmd| {\n            if matches!(editor_state, YoleckEditorState::GameActive) {\n                cmd.insert(bundle_maker());\n            }\n        }));\n        self\n    }\n\n    /// Give the entity a UUID, so that it can be persistently referred in `.yol` files.\n    ///\n    /// These entities will have a `uuid` field in their record header in the `.yol` file, and a\n    /// [`YoleckEntityUuid`](crate::prelude::YoleckEntityUuid) component that stores the same UUID\n    /// when loaded. The [`YoleckUuidRegistry`](crate::prelude::YoleckUuidRegistry) resource can be\n    /// used to resolve the entity from the UUID.\n    ///\n    /// # Required for Entity References\n    ///\n    /// **This method must be called for entity types that need to be referenced by other entities.**\n    /// Only entities with UUID can be:\n    /// - Referenced using [`YoleckEntityRef`](crate::prelude::YoleckEntityRef)\n    /// - Dragged and dropped onto entity reference fields in the editor\n    /// - Selected via the viewport click tool for entity references\n    ///\n    /// # Example\n    ///\n    /// ```no_run\n    /// # use bevy::prelude::*;\n    /// # use bevy_yoleck::prelude::*;\n    /// # let mut app = App::new();\n    /// // Planet can be referenced by other entities\n    /// app.add_yoleck_entity_type({\n    ///     YoleckEntityType::new(\"Planet\")\n    ///         .with_uuid()  // Required for references!\n    /// #       ;YoleckEntityType::new(\"Planet\")\n    /// });\n    /// ```\n    pub fn with_uuid(mut self) -> Self {\n        self.has_uuid = true;\n        self\n    }\n}\n\npub(crate) trait YoleckComponentHandler: 'static + Sync + Send {\n    fn component_type(&self) -> TypeId;\n    fn key(&self) -> &'static str;\n    fn init_in_entity(\n        &self,\n        data: Option<serde_json::Value>,\n        cmd: &mut EntityCommands,\n        components_data: &mut HashMap<TypeId, BoxedAny>,\n    );\n    fn build_in_bevy_app(&self, app: &mut App);\n    fn serialize(&self, component: &dyn Any) -> serde_json::Value;\n}\n\n#[derive(Default)]\nstruct YoleckComponentHandlerImpl<T: YoleckComponent> {\n    _phantom_data: PhantomData<T>,\n}\n\nimpl<T: YoleckComponent> YoleckComponentHandler for YoleckComponentHandlerImpl<T> {\n    fn component_type(&self) -> TypeId {\n        TypeId::of::<T>()\n    }\n\n    fn key(&self) -> &'static str {\n        T::KEY\n    }\n\n    fn init_in_entity(\n        &self,\n        data: Option<serde_json::Value>,\n        cmd: &mut EntityCommands,\n        components_data: &mut HashMap<TypeId, BoxedAny>,\n    ) {\n        let component: T = if let Some(data) = data {\n            match serde_json::from_value(data) {\n                Ok(component) => component,\n                Err(err) => {\n                    error!(\"Cannot load {:?}: {:?}\", T::KEY, err);\n                    return;\n                }\n            }\n        } else {\n            Default::default()\n        };\n        components_data.insert(self.component_type(), Box::new(component.clone()));\n        cmd.insert(component);\n    }\n\n    fn build_in_bevy_app(&self, app: &mut App) {\n        if let Some(schedule) =\n            app.get_schedule_mut(YoleckInternalSchedule::UpdateManagedDataFromComponents)\n        {\n            schedule.add_systems(Self::update_data_from_components);\n        }\n    }\n\n    fn serialize(&self, component: &dyn Any) -> serde_json::Value {\n        let concrete = component\n            .downcast_ref::<T>()\n            .expect(\"Serialize must be called with the correct type\");\n        serde_json::to_value(concrete).expect(\"Component must always be serializable\")\n    }\n}\n\nimpl<T: YoleckComponent> YoleckComponentHandlerImpl<T> {\n    fn update_data_from_components(mut query: Query<(&mut YoleckManaged, &mut T)>) {\n        for (mut yoleck_managed, component) in query.iter_mut() {\n            let yoleck_managed = yoleck_managed.as_mut();\n            match yoleck_managed.components_data.entry(TypeId::of::<T>()) {\n                bevy::platform::collections::hash_map::Entry::Vacant(entry) => {\n                    yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;\n                    entry.insert(Box::<T>::new(component.clone()));\n                }\n                bevy::platform::collections::hash_map::Entry::Occupied(mut entry) => {\n                    let existing: &mut T = entry\n                        .get_mut()\n                        .downcast_mut()\n                        .expect(\"Component data is of wrong type\");\n                    if existing != component.as_ref() {\n                        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;\n                        *existing = component.clone();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/util.rs",
    "content": "use bevy::prelude::*;\n\ntrait ContextSpecificResource: 'static + Sync + Send {\n    fn inject_to_world(&mut self, world: &mut World);\n    fn take_from_world(&mut self, world: &mut World);\n}\n\nimpl<T> ContextSpecificResource for Option<T>\nwhere\n    T: Resource,\n{\n    fn inject_to_world(&mut self, world: &mut World) {\n        world.insert_resource(self.take().unwrap());\n    }\n\n    fn take_from_world(&mut self, world: &mut World) {\n        *self = world.remove_resource();\n    }\n}\n\n#[derive(Resource)]\npub(crate) struct EditSpecificResources(Vec<Box<dyn ContextSpecificResource>>);\n\nimpl EditSpecificResources {\n    pub fn new() -> Self {\n        Self(Vec::new())\n    }\n\n    pub fn with(mut self, resource: impl Resource) -> Self {\n        self.0.push(Box::new(Some(resource)));\n        self\n    }\n\n    pub fn inject_to_world(&mut self, world: &mut World) {\n        for resource in self.0.iter_mut() {\n            resource.inject_to_world(world);\n        }\n    }\n\n    pub fn take_from_world(&mut self, world: &mut World) {\n        for resource in self.0.iter_mut().rev() {\n            resource.take_from_world(world);\n        }\n    }\n}\n"
  },
  {
    "path": "src/vpeol.rs",
    "content": "//! # Viewport Editing Overlay - utilities for editing entities from a viewport.\n//!\n//! This module does not do much, but provide common functionalities for more concrete modules like\n//! [`vpeol_2d`](crate::vpeol_2d) and [`vpeol_3d`](crate::vpeol_3d).\n//!\n//! `vpeol` modules also support `bevy_reflect::Reflect` by enabling the feature `beavy_reflect`.\n\nuse bevy::camera::RenderTarget;\nuse bevy::camera::primitives::{Aabb, MeshAabb};\nuse bevy::ecs::query::QueryFilter;\nuse bevy::ecs::system::SystemParam;\nuse bevy::mesh::VertexAttributeValues;\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\nuse bevy::render::render_resource::PrimitiveTopology;\nuse bevy::window::{PrimaryWindow, WindowRef};\nuse bevy_egui::EguiContexts;\n\nuse crate::entity_management::YoleckRawEntry;\nuse crate::knobs::YoleckKnobMarker;\nuse crate::prelude::{YoleckEditorState, YoleckUi};\nuse crate::{\n    YoleckDirective, YoleckEditMarker, YoleckEditorEvent, YoleckEntityConstructionSpecs,\n    YoleckManaged, YoleckRunEditSystems, YoleckState,\n};\n\npub mod prelude {\n    pub use crate::vpeol::{\n        VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolSelectionCuePlugin,\n        VpeolWillContainClickableChildren, YoleckKnobClick,\n    };\n    #[cfg(feature = \"vpeol_2d\")]\n    pub use crate::vpeol_2d::{\n        Vpeol2dCameraControl, Vpeol2dPluginForEditor, Vpeol2dPluginForGame, Vpeol2dPosition,\n        Vpeol2dRotatation, Vpeol2dScale,\n    };\n    #[cfg(feature = \"vpeol_3d\")]\n    pub use crate::vpeol_3d::{\n        Vpeol3dCameraControl, Vpeol3dCameraMode, Vpeol3dPluginForEditor, Vpeol3dPluginForGame,\n        Vpeol3dPosition, Vpeol3dRotation, Vpeol3dScale, Vpeol3dSnapToPlane,\n        Vpeol3dTranslationGizmoConfig, Vpeol3dTranslationGizmoMode, YoleckCameraChoices,\n    };\n}\n\n/// Order of Vpeol operations. Important for abstraction and backends to talk with each other.\n#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]\npub enum VpeolSystems {\n    /// Initialize [`VpeolCameraState`]\n    ///\n    /// * Clear all pointing.\n    /// * Update [`entities_of_interest`](VpeolCameraState::entities_of_interest).\n    /// * Update cursor position (can be overridden later if needed)\n    PrepareCameraState,\n    /// Mostly used by the backend to iterate over the entities and determine which ones are\n    /// being pointed (using [`consider`](VpeolCameraState::consider))\n    UpdateCameraState,\n    /// Interpret the mouse data and pass it back to Yoleck.\n    HandleCameraState,\n}\n\n/// Add base systems common for Vpeol editing.\npub struct VpeolBasePlugin;\n\nimpl Plugin for VpeolBasePlugin {\n    fn build(&self, app: &mut App) {\n        app.configure_sets(\n            Update,\n            (\n                VpeolSystems::PrepareCameraState.run_if(in_state(YoleckEditorState::EditorActive)),\n                VpeolSystems::UpdateCameraState.run_if(in_state(YoleckEditorState::EditorActive)),\n                VpeolSystems::HandleCameraState.run_if(in_state(YoleckEditorState::EditorActive)),\n                YoleckRunEditSystems,\n            )\n                .chain(), // .run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.init_resource::<VpeolClipboard>();\n        app.add_systems(\n            Update,\n            (prepare_camera_state, update_camera_world_position)\n                .in_set(VpeolSystems::PrepareCameraState),\n        );\n        app.add_systems(\n            Update,\n            handle_camera_state.in_set(VpeolSystems::HandleCameraState),\n        );\n        app.add_systems(\n            Update,\n            (\n                handle_delete_entity_key,\n                handle_copy_entity_key,\n                handle_paste_entity_key,\n            )\n                .run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        #[cfg(feature = \"bevy_reflect\")]\n        app.register_type::<VpeolDragPlane>();\n    }\n}\n\n/// A plane to define the drag direction of entities.\n///\n/// This is both a component and a resource. Entities that have the component will use the plane\n/// defined by it, while entities that don't will use the global one defined by the resource.\n/// Child entities will use the plane of the root Yoleck managed entity (if it has one). Knobs will\n/// use the one attached to the knob entity.\n///\n/// This configuration is only meaningful for 3D, but vpeol_2d still requires it resource.\n/// `Vpeol2dPluginForEditor` already adds it as `Vec3::Z`. Don't modify it.\n#[derive(Component, Resource)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct VpeolDragPlane(pub InfinitePlane3d);\n\nimpl VpeolDragPlane {\n    pub const XY: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Z });\n    pub const XZ: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Y });\n}\n\n/// Data passed between Vpeol abstraction and backends.\n#[derive(Component, Default, Debug)]\npub struct VpeolCameraState {\n    /// Where this camera considers the cursor to be in the world.\n    pub cursor_ray: Option<Ray3d>,\n    /// The topmost entity being pointed by the cursor.\n    pub entity_under_cursor: Option<(Entity, VpeolCursorPointing)>,\n    /// Entities that may or may not be topmost, but the editor needs to know whether or not they\n    /// are pointed at.\n    pub entities_of_interest: HashMap<Entity, Option<VpeolCursorPointing>>,\n    /// The mouse selection state.\n    pub clicks_on_objects_state: VpeolClicksOnObjectsState,\n}\n\n/// Information on how the cursor is pointing at an entity.\n#[derive(Clone, Debug)]\npub struct VpeolCursorPointing {\n    /// The location on the entity, in world coords, where the cursor is pointing.\n    pub cursor_position_world_coords: Vec3,\n    /// Used to determine entity selection priorities.\n    pub z_depth_screen_coords: f32,\n}\n\n/// State for determining how the user is interacting with entities using the mouse buttons.\n#[derive(Default, Debug)]\npub enum VpeolClicksOnObjectsState {\n    #[default]\n    Empty,\n    BeingDragged {\n        entity: Entity,\n        /// Used for deciding if the cursor has moved.\n        prev_screen_pos: Vec2,\n        /// Offset from the entity's center to the cursor's position on the drag plane.\n        offset: Vec3,\n        select_on_mouse_release: bool,\n    },\n}\n\nimpl VpeolCameraState {\n    /// Tell Vpeol the the user is pointing at an entity.\n    ///\n    /// This function may ignore the input if the entity is covered by another entity and is not an\n    /// entity of interest.\n    pub fn consider(\n        &mut self,\n        entity: Entity,\n        z_depth_screen_coords: f32,\n        cursor_position_world_coords: impl FnOnce() -> Vec3,\n    ) {\n        let should_update_entity = if let Some((_, old_cursor)) = self.entity_under_cursor.as_ref()\n        {\n            old_cursor.z_depth_screen_coords < z_depth_screen_coords\n        } else {\n            true\n        };\n\n        if let Some(of_interest) = self.entities_of_interest.get_mut(&entity) {\n            let pointing = VpeolCursorPointing {\n                cursor_position_world_coords: cursor_position_world_coords(),\n                z_depth_screen_coords,\n            };\n            if should_update_entity {\n                self.entity_under_cursor = Some((entity, pointing.clone()));\n            }\n            *of_interest = Some(pointing);\n        } else if should_update_entity {\n            self.entity_under_cursor = Some((\n                entity,\n                VpeolCursorPointing {\n                    cursor_position_world_coords: cursor_position_world_coords(),\n                    z_depth_screen_coords,\n                },\n            ));\n        }\n    }\n\n    pub fn pointing_at_entity(&self, entity: Entity) -> Option<&VpeolCursorPointing> {\n        if let Some((entity_under_cursor, pointing_at)) = &self.entity_under_cursor\n            && *entity_under_cursor == entity\n        {\n            return Some(pointing_at);\n        }\n        self.entities_of_interest.get(&entity)?.as_ref()\n    }\n}\n\nfn prepare_camera_state(\n    mut query: Query<&mut VpeolCameraState>,\n    knob_query: Query<Entity, With<YoleckKnobMarker>>,\n) {\n    for mut camera_state in query.iter_mut() {\n        camera_state.entity_under_cursor = None;\n        camera_state.entities_of_interest = knob_query\n            .iter()\n            .chain(match camera_state.clicks_on_objects_state {\n                VpeolClicksOnObjectsState::Empty => None,\n                VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(entity),\n            })\n            .map(|entity| (entity, None))\n            .collect();\n    }\n}\n\nfn update_camera_world_position(\n    mut cameras_query: Query<(\n        &mut VpeolCameraState,\n        &GlobalTransform,\n        &Camera,\n        &RenderTarget,\n    )>,\n    window_getter: WindowGetter,\n) {\n    for (mut camera_state, camera_transform, camera, render_target) in cameras_query.iter_mut() {\n        camera_state.cursor_ray = (|| {\n            let RenderTarget::Window(window_ref) = render_target else {\n                return None;\n            };\n            let window = window_getter.get_window(*window_ref)?;\n            let cursor_in_screen_pos = window.cursor_position()?;\n            camera\n                .viewport_to_world(camera_transform, cursor_in_screen_pos)\n                .ok()\n        })();\n    }\n}\n\n#[derive(SystemParam)]\npub(crate) struct WindowGetter<'w, 's> {\n    windows: Query<'w, 's, &'static Window>,\n    primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,\n}\n\nimpl WindowGetter<'_, '_> {\n    pub fn get_window(&self, window_ref: WindowRef) -> Option<&Window> {\n        match window_ref {\n            WindowRef::Primary => self.primary_window.single().ok(),\n            WindowRef::Entity(window_id) => self.windows.get(window_id).ok(),\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nfn handle_camera_state(\n    mut egui_context: EguiContexts,\n    mut query: Query<(&RenderTarget, &mut VpeolCameraState)>,\n    window_getter: WindowGetter,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n    keyboard: Res<ButtonInput<KeyCode>>,\n    global_transform_query: Query<&GlobalTransform>,\n    selected_query: Query<(), With<YoleckEditMarker>>,\n    knob_query: Query<Entity, With<YoleckKnobMarker>>,\n    mut directives_writer: MessageWriter<YoleckDirective>,\n    global_drag_plane: Res<VpeolDragPlane>,\n    drag_plane_overrides_query: Query<&VpeolDragPlane>,\n) -> Result {\n    enum MouseButtonOp {\n        JustPressed,\n        BeingPressed,\n        JustReleased,\n    }\n    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Left) {\n        if egui_context.ctx_mut()?.is_pointer_over_area() {\n            return Ok(());\n        }\n        MouseButtonOp::JustPressed\n    } else if mouse_buttons.just_released(MouseButton::Left) {\n        MouseButtonOp::JustReleased\n    } else if mouse_buttons.pressed(MouseButton::Left) {\n        MouseButtonOp::BeingPressed\n    } else {\n        for (_, mut camera_state) in query.iter_mut() {\n            camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;\n        }\n        return Ok(());\n    };\n    for (render_target, mut camera_state) in query.iter_mut() {\n        let Some(cursor_ray) = camera_state.cursor_ray else {\n            continue;\n        };\n        let calc_cursor_in_world_position = |entity: Entity, plane_origin: Vec3| -> Option<Vec3> {\n            let VpeolDragPlane(drag_plane) = drag_plane_overrides_query\n                .get(entity)\n                .unwrap_or(&global_drag_plane);\n            let distance = cursor_ray.intersect_plane(plane_origin, *drag_plane)?;\n            Some(cursor_ray.get_point(distance))\n        };\n\n        let RenderTarget::Window(window_ref) = render_target else {\n            continue;\n        };\n        let Some(window) = window_getter.get_window(*window_ref) else {\n            continue;\n        };\n        let Some(cursor_in_screen_pos) = window.cursor_position() else {\n            continue;\n        };\n\n        match (&mouse_button_op, &camera_state.clicks_on_objects_state) {\n            (MouseButtonOp::JustPressed, VpeolClicksOnObjectsState::Empty) => {\n                if keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {\n                    if let Some((entity, _)) = &camera_state.entity_under_cursor {\n                        directives_writer.write(YoleckDirective::toggle_selected(*entity));\n                    }\n                } else if let Some((knob_entity, cursor_pointing)) =\n                    knob_query.iter().find_map(|knob_entity| {\n                        Some((knob_entity, camera_state.pointing_at_entity(knob_entity)?))\n                    })\n                {\n                    directives_writer.write(YoleckDirective::pass_to_entity(\n                        knob_entity,\n                        YoleckKnobClick,\n                    ));\n                    let Ok(knob_transform) = global_transform_query.get(knob_entity) else {\n                        continue;\n                    };\n                    let Some(cursor_in_world_position) = calc_cursor_in_world_position(\n                        knob_entity,\n                        cursor_pointing.cursor_position_world_coords,\n                    ) else {\n                        continue;\n                    };\n                    camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::BeingDragged {\n                        entity: knob_entity,\n                        prev_screen_pos: cursor_in_screen_pos,\n                        offset: cursor_in_world_position - knob_transform.translation(),\n                        select_on_mouse_release: false,\n                    }\n                } else {\n                    camera_state.clicks_on_objects_state = if let Some((entity, cursor_pointing)) =\n                        &camera_state.entity_under_cursor\n                    {\n                        let Ok(entity_transform) = global_transform_query.get(*entity) else {\n                            continue;\n                        };\n                        let select_on_mouse_release = selected_query.contains(*entity);\n                        if !select_on_mouse_release {\n                            directives_writer.write(YoleckDirective::set_selected(Some(*entity)));\n                        }\n                        let Some(cursor_in_world_position) = calc_cursor_in_world_position(\n                            *entity,\n                            cursor_pointing.cursor_position_world_coords,\n                        ) else {\n                            continue;\n                        };\n                        VpeolClicksOnObjectsState::BeingDragged {\n                            entity: *entity,\n                            prev_screen_pos: cursor_in_screen_pos,\n                            offset: cursor_in_world_position - entity_transform.translation(),\n                            select_on_mouse_release,\n                        }\n                    } else {\n                        directives_writer.write(YoleckDirective::set_selected(None));\n                        VpeolClicksOnObjectsState::Empty\n                    };\n                }\n            }\n            (\n                MouseButtonOp::BeingPressed,\n                VpeolClicksOnObjectsState::BeingDragged {\n                    entity,\n                    prev_screen_pos,\n                    offset,\n                    select_on_mouse_release: _,\n                },\n            ) => {\n                if 0.1 <= prev_screen_pos.distance_squared(cursor_in_screen_pos) {\n                    let Ok(entity_transform) = global_transform_query.get(*entity) else {\n                        continue;\n                    };\n                    let drag_point = entity_transform.translation() + *offset;\n                    let Some(cursor_in_world_position) =\n                        calc_cursor_in_world_position(*entity, drag_point)\n                    else {\n                        continue;\n                    };\n                    directives_writer.write(YoleckDirective::pass_to_entity(\n                        *entity,\n                        cursor_in_world_position - *offset,\n                    ));\n                    camera_state.clicks_on_objects_state =\n                        VpeolClicksOnObjectsState::BeingDragged {\n                            entity: *entity,\n                            prev_screen_pos: cursor_in_screen_pos,\n                            offset: *offset,\n                            select_on_mouse_release: false,\n                        };\n                }\n            }\n            (\n                MouseButtonOp::JustReleased,\n                VpeolClicksOnObjectsState::BeingDragged {\n                    entity,\n                    prev_screen_pos: _,\n                    offset: _,\n                    select_on_mouse_release: true,\n                },\n            ) => {\n                directives_writer.write(YoleckDirective::set_selected(Some(*entity)));\n                camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;\n            }\n            _ => {}\n        }\n    }\n    Ok(())\n}\n\n/// A [passed data](crate::knobs::YoleckKnobHandle::get_passed_data) to a knob entity that indicate\n/// it was clicked by the level editor.\npub struct YoleckKnobClick;\n\n/// Marker for entities that will be interacted in the viewport using their children.\n///\n/// Populate systems should mark the entity with this component when applicable. The viewport\n/// overlay plugin is responsible for handling it by using [`handle_clickable_children_system`].\n#[derive(Component)]\npub struct VpeolWillContainClickableChildren;\n\n/// Marker for viewport editor overlay plugins to route child interaction to parent entities.\n#[derive(Component)]\npub struct VpeolRouteClickTo(pub Entity);\n\n/// Helper utility for finding the Yoleck controlled entity that's in charge of an entity the user\n/// points at.\n#[derive(SystemParam)]\npub struct VpeolRootResolver<'w, 's> {\n    root_resolver: Query<'w, 's, &'static VpeolRouteClickTo>,\n    #[allow(clippy::type_complexity)]\n    has_managed_query: Query<'w, 's, (), Or<(With<YoleckManaged>, With<YoleckKnobMarker>)>>,\n}\n\nimpl VpeolRootResolver<'_, '_> {\n    /// Find the Yoleck controlled entity that's in charge of an entity the user points at.\n    pub fn resolve_root(&self, entity: Entity) -> Option<Entity> {\n        if let Ok(VpeolRouteClickTo(root_entity)) = self.root_resolver.get(entity) {\n            Some(*root_entity)\n        } else {\n            self.has_managed_query.get(entity).ok()?;\n            Some(entity)\n        }\n    }\n}\n\n/// Add [`VpeolRouteClickTo`] of entities marked with [`VpeolWillContainClickableChildren`].\npub fn handle_clickable_children_system<F, B>(\n    parents_query: Query<(Entity, &Children), With<VpeolWillContainClickableChildren>>,\n    children_query: Query<&Children>,\n    should_add_query: Query<Entity, F>,\n    mut commands: Commands,\n) where\n    F: QueryFilter,\n    B: Default + Bundle,\n{\n    for (parent, children) in parents_query.iter() {\n        if children.is_empty() {\n            continue;\n        }\n        let mut any_added = false;\n        let mut children_to_check: Vec<Entity> = children.iter().collect();\n        while let Some(child) = children_to_check.pop() {\n            if let Ok(child_children) = children_query.get(child) {\n                children_to_check.extend(child_children.iter());\n            }\n            if should_add_query.get(child).is_ok() {\n                commands\n                    .entity(child)\n                    .try_insert((VpeolRouteClickTo(parent), B::default()));\n                any_added = true;\n            }\n        }\n        if any_added {\n            commands\n                .entity(parent)\n                .remove::<VpeolWillContainClickableChildren>();\n        }\n    }\n}\n\n/// Add a pulse effect when an entity is being selected.\npub struct VpeolSelectionCuePlugin {\n    /// How long, in seconds, the entire pulse effect will take. Defaults to 0.3.\n    pub effect_duration: f32,\n    /// By how much (relative to original size) the entity will grow during the pulse. Defaults to 0.3.\n    pub effect_magnitude: f32,\n}\n\nimpl Default for VpeolSelectionCuePlugin {\n    fn default() -> Self {\n        Self {\n            effect_duration: 0.3,\n            effect_magnitude: 0.3,\n        }\n    }\n}\n\nimpl Plugin for VpeolSelectionCuePlugin {\n    fn build(&self, app: &mut App) {\n        app.add_systems(Update, manage_selection_transform_components);\n        app.add_systems(PostUpdate, {\n            add_selection_cue_before_transform_propagate(\n                1.0 / self.effect_duration,\n                2.0 * self.effect_magnitude,\n            )\n            .before(TransformSystems::Propagate)\n        });\n        app.add_systems(PostUpdate, {\n            restore_transform_from_cache_after_transform_propagate\n                .after(TransformSystems::Propagate)\n        });\n    }\n}\n\n#[derive(Component)]\nstruct SelectionCueAnimation {\n    cached_transform: Transform,\n    progress: f32,\n}\n\nfn manage_selection_transform_components(\n    add_cue_query: Query<Entity, (Without<SelectionCueAnimation>, With<YoleckEditMarker>)>,\n    remove_cue_query: Query<Entity, (With<SelectionCueAnimation>, Without<YoleckEditMarker>)>,\n    mut commands: Commands,\n) {\n    for entity in add_cue_query.iter() {\n        commands.entity(entity).insert(SelectionCueAnimation {\n            cached_transform: Default::default(),\n            progress: 0.0,\n        });\n    }\n    for entity in remove_cue_query.iter() {\n        commands.entity(entity).remove::<SelectionCueAnimation>();\n    }\n}\n\nfn add_selection_cue_before_transform_propagate(\n    time_speedup: f32,\n    magnitude_scale: f32,\n) -> impl FnMut(Query<(&mut SelectionCueAnimation, &mut Transform)>, Res<Time>) {\n    move |mut query, time| {\n        for (mut animation, mut transform) in query.iter_mut() {\n            animation.cached_transform = *transform;\n            if animation.progress < 1.0 {\n                animation.progress += time_speedup * time.delta_secs();\n                let extra = if animation.progress < 0.5 {\n                    animation.progress\n                } else {\n                    1.0 - animation.progress\n                };\n                transform.scale *= 1.0 + magnitude_scale * extra;\n            }\n        }\n    }\n}\n\nfn restore_transform_from_cache_after_transform_propagate(\n    mut query: Query<(&SelectionCueAnimation, &mut Transform)>,\n) {\n    for (animation, mut transform) in query.iter_mut() {\n        *transform = animation.cached_transform;\n    }\n}\n\npub(crate) fn ray_intersection_with_mesh(ray: Ray3d, mesh: &Mesh) -> Option<f32> {\n    let aabb = mesh.compute_aabb()?;\n    let distance_to_aabb = ray_intersection_with_aabb(ray, aabb)?;\n\n    if let Some(mut triangles) = iter_triangles(mesh) {\n        triangles.find_map(|triangle| triangle.ray_intersection(ray))\n    } else {\n        Some(distance_to_aabb)\n    }\n}\n\nfn ray_intersection_with_aabb(ray: Ray3d, aabb: Aabb) -> Option<f32> {\n    let center: Vec3 = aabb.center.into();\n    let mut max_low = f32::NEG_INFINITY;\n    let mut min_high = f32::INFINITY;\n    for (axis, half_extent) in [\n        (Vec3::X, aabb.half_extents.x),\n        (Vec3::Y, aabb.half_extents.y),\n        (Vec3::Z, aabb.half_extents.z),\n    ] {\n        let dot = ray.direction.dot(axis);\n        if dot == 0.0 {\n            let distance_from_center = (ray.origin - center).dot(axis);\n            if half_extent < distance_from_center.abs() {\n                return None;\n            }\n        } else {\n            let low = ray.intersect_plane(center - half_extent * axis, InfinitePlane3d::new(axis));\n            let high = ray.intersect_plane(center + half_extent * axis, InfinitePlane3d::new(axis));\n            let (low, high) = if 0.0 <= dot { (low, high) } else { (high, low) };\n            if let Some(low) = low {\n                max_low = max_low.max(low);\n            }\n            if let Some(high) = high {\n                min_high = min_high.min(high);\n            } else {\n                return None;\n            }\n        }\n    }\n    if max_low <= min_high {\n        Some(max_low)\n    } else {\n        None\n    }\n}\n\nfn iter_triangles(mesh: &Mesh) -> Option<impl '_ + Iterator<Item = Triangle>> {\n    if mesh.primitive_topology() != PrimitiveTopology::TriangleList {\n        return None;\n    }\n    let indices = mesh.indices()?;\n    let Some(VertexAttributeValues::Float32x3(positions)) =\n        mesh.attribute(Mesh::ATTRIBUTE_POSITION)\n    else {\n        return None;\n    };\n    let mut it = indices.iter();\n    Some(std::iter::from_fn(move || {\n        Some(Triangle(\n            [it.next()?, it.next()?, it.next()?].map(|idx| Vec3::from_array(positions[idx])),\n        ))\n    }))\n}\n\n#[derive(Debug)]\nstruct Triangle([Vec3; 3]);\n\nimpl Triangle {\n    fn ray_intersection(&self, ray: Ray3d) -> Option<f32> {\n        let directions = [\n            self.0[1] - self.0[0],\n            self.0[2] - self.0[1],\n            self.0[0] - self.0[2],\n        ];\n        let normal = directions[0].cross(directions[1]); // no need to normalize it\n        let plane = InfinitePlane3d {\n            normal: Dir3::new(normal).ok()?,\n        };\n        let distance = ray.intersect_plane(self.0[0], plane)?;\n        let point = ray.get_point(distance);\n        if self\n            .0\n            .iter()\n            .zip(directions.iter())\n            .all(|(vertex, direction)| {\n                let vertical = direction.cross(normal);\n                vertical.dot(point - *vertex) <= 0.0\n            })\n        {\n            Some(distance)\n        } else {\n            None\n        }\n    }\n}\n\n/// Detects an entity that's being clicked on. Meant to be used with [Yoleck's exclusive edit\n/// systems](crate::exclusive_systems::YoleckExclusiveSystemsQueue) and with Bevy's system piping.\n///\n/// Note that this only returns `Some` when the user clicks on an entity - it does not finish the\n/// exclusive system. The other systems that this gets piped into should decide whether or not it\n/// should be finished.\npub fn vpeol_read_click_on_entity<Filter: QueryFilter>(\n    mut ui: ResMut<YoleckUi>,\n    cameras_query: Query<&VpeolCameraState>,\n    yoleck_managed_query: Query<&YoleckManaged>,\n    filter_query: Query<(), Filter>,\n    buttons: Res<ButtonInput<MouseButton>>,\n    mut candidate: Local<Option<Entity>>,\n) -> Option<Entity> {\n    let target = if ui.ctx().is_pointer_over_area() {\n        None\n    } else {\n        cameras_query\n            .iter()\n            .find_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))\n    };\n\n    let Some(target) = target else {\n        ui.label(\"No Target\");\n        return None;\n    };\n\n    let Ok(yoleck_managed) = yoleck_managed_query.get(target) else {\n        ui.label(\"No Target\");\n        return None;\n    };\n\n    if !filter_query.contains(target) {\n        ui.label(format!(\"Invalid Target ({})\", yoleck_managed.type_name));\n        return None;\n    }\n    ui.label(format!(\n        \"Targeting {:?} ({})\",\n        target, yoleck_managed.type_name\n    ));\n\n    if buttons.just_pressed(MouseButton::Left) {\n        *candidate = Some(target);\n    } else if buttons.just_released(MouseButton::Left)\n        && let Some(candidate) = candidate.take()\n        && candidate == target\n    {\n        return Some(target);\n    }\n    None\n}\n\n/// Apply a transform to every entity in the level.\n///\n/// Note that:\n/// * It is the duty of [`vpeol_2d`](crate::vpeol_2d)/[`vpeol_3d`](crate::vpeol_3d) to handle the\n///   actual repositioning, and they do so only for entities that use their existing components\n///   ([`Vpeol2dPosition`](crate::vpeol_2d::Vpeol2dPosition)/[`Vpeol3dPosition`](crate::vpeol_3d::Vpeol3dPosition)\n///   and friends). If there are entities that do not use these mechanisms, it falls under the\n///   responsibility of whatever populates their `Transform` to take this component (of their level\n///   entity) into account.\n/// * The repositioning is done directly on the `Transform` - not on the `GlobalTransform`.\n#[derive(Component)]\npub struct VpeolRepositionLevel(pub Transform);\n\nfn handle_delete_entity_key(\n    mut egui_context: EguiContexts,\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    mut yoleck_state: ResMut<YoleckState>,\n    query: Query<Entity, With<YoleckEditMarker>>,\n    mut commands: Commands,\n    mut writer: MessageWriter<YoleckEditorEvent>,\n) -> Result {\n    if egui_context.ctx_mut()?.wants_keyboard_input() {\n        return Ok(());\n    }\n\n    if keyboard_input.just_pressed(KeyCode::Delete) {\n        for entity in query.iter() {\n            commands.entity(entity).despawn();\n            writer.write(YoleckEditorEvent::EntityDeselected(entity));\n        }\n        if !query.is_empty() {\n            yoleck_state.level_needs_saving = true;\n        }\n    }\n\n    Ok(())\n}\n\n#[derive(Resource)]\nenum VpeolClipboard {\n    #[cfg(feature = \"arboard\")]\n    Arboard(arboard::Clipboard),\n    Internal(String),\n}\n\nimpl FromWorld for VpeolClipboard {\n    fn from_world(_: &mut World) -> Self {\n        #[cfg(feature = \"arboard\")]\n        match arboard::Clipboard::new() {\n            Ok(clipboard) => {\n                debug!(\"Arboard clipbaord successfully initiated\");\n                return VpeolClipboard::Arboard(clipboard);\n            }\n            Err(err) => {\n                warn!(\"Cannot initiate Arboard clipboard: {err}\");\n            }\n        }\n        VpeolClipboard::Internal(String::new())\n    }\n}\n\nfn handle_copy_entity_key(\n    mut egui_context: EguiContexts,\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    query: Query<&YoleckManaged, With<YoleckEditMarker>>,\n    construction_specs: Res<YoleckEntityConstructionSpecs>,\n    mut clipboard: ResMut<VpeolClipboard>,\n) -> Result {\n    if egui_context.ctx_mut()?.wants_keyboard_input() {\n        return Ok(());\n    }\n\n    let ctrl_pressed = keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);\n\n    if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyC) {\n        let entities: Vec<YoleckRawEntry> = query\n            .iter()\n            .filter_map(|yoleck_managed| {\n                let entity_type =\n                    construction_specs.get_entity_type_info(&yoleck_managed.type_name)?;\n\n                let data: serde_json::Map<String, serde_json::Value> = entity_type\n                    .components\n                    .iter()\n                    .filter_map(|component| {\n                        let component_data = yoleck_managed.components_data.get(component)?;\n                        let handler = &construction_specs.component_handlers[component];\n                        Some((\n                            handler.key().to_string(),\n                            handler.serialize(component_data.as_ref()),\n                        ))\n                    })\n                    .collect();\n\n                Some(YoleckRawEntry {\n                    header: crate::entity_management::YoleckEntryHeader {\n                        type_name: yoleck_managed.type_name.clone(),\n                        name: yoleck_managed.name.clone(),\n                        uuid: None,\n                    },\n                    data,\n                })\n            })\n            .collect();\n\n        if !entities.is_empty()\n            && let Ok(json) = serde_json::to_string(&entities)\n        {\n            match clipboard.as_mut() {\n                #[cfg(feature = \"arboard\")]\n                VpeolClipboard::Arboard(clipboard) => {\n                    clipboard.set_text(json)?;\n                }\n                VpeolClipboard::Internal(clipboard) => {\n                    *clipboard = json;\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\nfn handle_paste_entity_key(\n    mut egui_context: EguiContexts,\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    yoleck_state: Res<YoleckState>,\n    mut directives_writer: MessageWriter<YoleckDirective>,\n    mut clipboard: ResMut<VpeolClipboard>,\n) -> Result {\n    if egui_context.ctx_mut()?.wants_keyboard_input() {\n        return Ok(());\n    }\n\n    let ctrl_pressed = keyboard_input.pressed(KeyCode::ControlLeft)\n        || keyboard_input.pressed(KeyCode::ControlRight);\n\n    if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyV) {\n        #[cfg(feature = \"arboard\")]\n        let arboard_text_storage: String;\n        let text_to_paste: Option<&str> = match clipboard.as_mut() {\n            #[cfg(feature = \"arboard\")]\n            VpeolClipboard::Arboard(clipboard) => match clipboard.get_text() {\n                Ok(text) => {\n                    arboard_text_storage = text;\n                    Some(&arboard_text_storage)\n                }\n                Err(err) => {\n                    error!(\"Cannot load text from arboard: {err}\");\n                    None\n                }\n            },\n            VpeolClipboard::Internal(clipboard) => {\n                Some(clipboard.as_str()).filter(|txt| !txt.is_empty())\n            }\n        };\n\n        if let Some(text) = text_to_paste\n            && let Ok(entities) =\n                serde_json::from_str::<Vec<YoleckRawEntry>>(text).inspect_err(|err| {\n                    warn!(\"Cannot paste - failure to parse copied text: {err}\");\n                })\n            && !entities.is_empty()\n        {\n            let level_being_edited = yoleck_state.level_being_edited;\n\n            for entry in entities {\n                directives_writer.write(\n                    YoleckDirective::spawn_entity(level_being_edited, entry.header.type_name, true)\n                        .extend(entry.data.into_iter())\n                        .into(),\n                );\n            }\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/vpeol_2d.rs",
    "content": "//! # Viewport Editing Overlay for 2D games.\n//!\n//! Use this module to implement simple 2D editing for 2D games.\n//!\n//! To use add the egui and Yoleck plugins to the Bevy app, as well as the plugin of this module:\n//!\n//! ```no_run\n//! # use bevy::prelude::*;\n//! # use bevy_yoleck::bevy_egui::EguiPlugin;\n//! # use bevy_yoleck::prelude::*;\n//! # use bevy_yoleck::vpeol::prelude::*;\n//! # let mut app = App::new();\n//! app.add_plugins(EguiPlugin::default());\n//! app.add_plugins(YoleckPluginForEditor);\n//! // Use `Vpeol2dPluginForGame` instead when setting up for game.\n//! app.add_plugins(Vpeol2dPluginForEditor);\n//! ```\n//!\n//! Add the following components to the camera entity:\n//! * [`VpeolCameraState`] in order to select and drag entities.\n//! * [`Vpeol2dCameraControl`] in order to pan and zoom the camera with the mouse. This one can be\n//!   skipped if there are other means to control the camera inside the editor, or if no camera\n//!   control inside the editor is desired.\n//!\n//! ```no_run\n//! # use bevy::prelude::*;\n//! # use bevy_yoleck::vpeol::VpeolCameraState;\n//! # use bevy_yoleck::vpeol::prelude::*;\n//! # let commands: Commands = panic!();\n//! commands\n//!     .spawn(Camera2d::default())\n//!     .insert(VpeolCameraState::default())\n//!     .insert(Vpeol2dCameraControl::default());\n//! ```\n//!\n//! Entity selection by clicking on it is supported by just adding the plugin. To implement\n//! dragging, there are two options:\n//!\n//! 1. Add  the [`Vpeol2dPosition`] Yoleck component and use it as the source of position. Optionally\n//!    add [`Vpeol2dRotatation`] (edited in degrees) and [`Vpeol2dScale`] (edited with X, Y values)\n//!    for rotation and scale support.\n//!     ```no_run\n//!     # use bevy::prelude::*;\n//!     # use bevy_yoleck::prelude::*;\n//!     # use bevy_yoleck::vpeol::prelude::*;\n//!     # use serde::{Deserialize, Serialize};\n//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n//!     # struct Example;\n//!     # let mut app = App::new();\n//!     app.add_yoleck_entity_type({\n//!         YoleckEntityType::new(\"Example\")\n//!             .with::<Vpeol2dPosition>() // vpeol_2d dragging\n//!             .with::<Vpeol2dRotatation>() // optional: rotation with egui (degrees)\n//!             .with::<Vpeol2dScale>() // optional: scale with egui\n//!             .with::<Example>() // entity's specific data and systems\n//!     });\n//!     ```\n//! 2. Use data passing. vpeol_2d will pass a `Vec3` to the entity being dragged:\n//!     ```no_run\n//!     # use bevy::prelude::*;\n//!     # use bevy_yoleck::prelude::*;\n//!     # use serde::{Deserialize, Serialize};\n//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n//!     # struct Example {\n//!     #     position: Vec2,\n//!     # }\n//!     # let mut app = App::new();\n//!     fn edit_example(mut edit: YoleckEdit<(Entity, &mut Example)>, passed_data: Res<YoleckPassedData>) {\n//!         let Ok((entity, mut example)) = edit.single_mut() else { return };\n//!         if let Some(pos) = passed_data.get::<Vec3>(entity) {\n//!             example.position = pos.truncate();\n//!         }\n//!     }\n//!\n//!     fn populate_example(mut populate: YoleckPopulate<&Example>) {\n//!         populate.populate(|_ctx, mut cmd, example| {\n//!             cmd.insert(Transform::from_translation(example.position.extend(0.0)));\n//!             cmd.insert(Sprite {\n//!                 // Actual sprite data\n//!                 ..Default::default()\n//!             });\n//!         });\n//!     }\n//!     ```\n\nuse std::any::TypeId;\n\nuse crate::bevy_egui::{EguiContexts, egui};\nuse crate::exclusive_systems::{\n    YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,\n};\nuse crate::vpeol::{\n    VpeolBasePlugin, VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver,\n    VpeolSystems, WindowGetter, handle_clickable_children_system, ray_intersection_with_mesh,\n};\nuse bevy::camera::RenderTarget;\nuse bevy::camera::visibility::VisibleEntities;\nuse bevy::input::mouse::MouseWheel;\nuse bevy::math::DVec2;\nuse bevy::platform::collections::HashMap;\nuse bevy::prelude::*;\nuse bevy::sprite::Anchor;\nuse bevy::text::TextLayoutInfo;\nuse serde::{Deserialize, Serialize};\n\nuse crate::{YoleckBelongsToLevel, YoleckSchedule, prelude::*};\n\n/// Add the systems required for loading levels that use vpeol_2d components\npub struct Vpeol2dPluginForGame;\n\nimpl Plugin for Vpeol2dPluginForGame {\n    fn build(&self, app: &mut App) {\n        app.add_systems(\n            YoleckSchedule::OverrideCommonComponents,\n            vpeol_2d_populate_transform,\n        );\n        #[cfg(feature = \"bevy_reflect\")]\n        register_reflect_types(app);\n    }\n}\n\n#[cfg(feature = \"bevy_reflect\")]\nfn register_reflect_types(app: &mut App) {\n    app.register_type::<Vpeol2dPosition>();\n    app.register_type::<Vpeol2dRotatation>();\n    app.register_type::<Vpeol2dScale>();\n    app.register_type::<Vpeol2dCameraControl>();\n}\n\n/// Add the systems required for 2D editing.\n///\n/// * 2D camera control (for cameras with [`Vpeol2dCameraControl`])\n/// * Entity selection.\n/// * Entity dragging.\n/// * Connecting nested entities.\npub struct Vpeol2dPluginForEditor;\n\nimpl Plugin for Vpeol2dPluginForEditor {\n    fn build(&self, app: &mut App) {\n        app.add_plugins(VpeolBasePlugin);\n        app.add_plugins(Vpeol2dPluginForGame);\n        app.insert_resource(VpeolDragPlane::XY);\n\n        app.add_systems(\n            Update,\n            (\n                update_camera_status_for_sprites,\n                update_camera_status_for_2d_meshes,\n                update_camera_status_for_text_2d,\n            )\n                .in_set(VpeolSystems::UpdateCameraState),\n        );\n        app.add_systems(\n            PostUpdate, // to prevent camera shaking (only seen it in 3D, but still)\n            (camera_2d_pan, camera_2d_zoom).run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_systems(\n            Update,\n            (\n                ApplyDeferred,\n                handle_clickable_children_system::<\n                    Or<(With<Sprite>, (With<TextLayoutInfo>, With<Anchor>))>,\n                    (),\n                >,\n                ApplyDeferred,\n            )\n                .chain()\n                .run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_yoleck_edit_system(vpeol_2d_edit_transform_group);\n        app.world_mut()\n            .resource_mut::<YoleckEntityCreationExclusiveSystems>()\n            .on_entity_creation(|queue| queue.push_back(vpeol_2d_init_position));\n    }\n}\n\nstruct CursorInWorldPos {\n    cursor_in_world_pos: Vec2,\n}\n\nimpl CursorInWorldPos {\n    fn from_camera_state(camera_state: &VpeolCameraState) -> Option<Self> {\n        Some(Self {\n            cursor_in_world_pos: camera_state.cursor_ray?.origin.truncate(),\n        })\n    }\n\n    fn cursor_in_entity_space(&self, transform: &GlobalTransform) -> Vec2 {\n        transform\n            .to_matrix()\n            .inverse()\n            .project_point3(self.cursor_in_world_pos.extend(0.0))\n            .truncate()\n    }\n\n    fn check_square(\n        &self,\n        entity_transform: &GlobalTransform,\n        anchor: &Anchor,\n        size: Vec2,\n    ) -> bool {\n        let cursor = self.cursor_in_entity_space(entity_transform);\n        let anchor = anchor.as_vec();\n        let mut min_corner = Vec2::new(-0.5, -0.5) - anchor;\n        let mut max_corner = Vec2::new(0.5, 0.5) - anchor;\n        for corner in [&mut min_corner, &mut max_corner] {\n            corner.x *= size.x;\n            corner.y *= size.y;\n        }\n        min_corner.x <= cursor.x\n            && cursor.x <= max_corner.x\n            && min_corner.y <= cursor.y\n            && cursor.y <= max_corner.y\n    }\n}\n\n#[allow(clippy::type_complexity)]\nfn update_camera_status_for_sprites(\n    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,\n    entities_query: Query<(Entity, &GlobalTransform, &Sprite, &Anchor)>,\n    image_assets: Res<Assets<Image>>,\n    texture_atlas_layout_assets: Res<Assets<TextureAtlasLayout>>,\n    root_resolver: VpeolRootResolver,\n) {\n    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {\n        let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {\n            continue;\n        };\n\n        for (entity, entity_transform, sprite, anchor) in\n            entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))\n        // entities_query.iter()\n        {\n            let size = if let Some(custom_size) = sprite.custom_size {\n                custom_size\n            } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {\n                let Some(texture_atlas_layout) =\n                    texture_atlas_layout_assets.get(&texture_atlas.layout)\n                else {\n                    continue;\n                };\n                texture_atlas_layout.textures[texture_atlas.index]\n                    .size()\n                    .as_vec2()\n            } else if let Some(texture) = image_assets.get(&sprite.image) {\n                texture.size().as_vec2()\n            } else {\n                continue;\n            };\n            if cursor.check_square(entity_transform, anchor, size) {\n                let z_depth = entity_transform.translation().z;\n                let Some(root_entity) = root_resolver.resolve_root(entity) else {\n                    continue;\n                };\n                camera_state.consider(root_entity, z_depth, || {\n                    cursor.cursor_in_world_pos.extend(z_depth)\n                });\n            }\n        }\n    }\n}\n\nfn update_camera_status_for_2d_meshes(\n    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,\n    entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,\n    mesh_assets: Res<Assets<Mesh>>,\n    root_resolver: VpeolRootResolver,\n) {\n    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {\n        let Some(cursor_ray) = camera_state.cursor_ray else {\n            continue;\n        };\n        for (entity, global_transform, mesh) in\n            entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))\n        {\n            let Some(mesh) = mesh_assets.get(&mesh.0) else {\n                continue;\n            };\n\n            let inverse_transform = global_transform.to_matrix().inverse();\n\n            let ray_in_object_coords = Ray3d {\n                origin: inverse_transform.transform_point3(cursor_ray.origin),\n                direction: inverse_transform\n                    .transform_vector3(*cursor_ray.direction)\n                    .try_into()\n                    .unwrap(),\n            };\n\n            let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {\n                continue;\n            };\n\n            let Some(root_entity) = root_resolver.resolve_root(entity) else {\n                continue;\n            };\n            camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));\n        }\n    }\n}\n\nfn update_camera_status_for_text_2d(\n    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,\n    entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,\n    root_resolver: VpeolRootResolver,\n) {\n    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {\n        let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {\n            continue;\n        };\n\n        for (entity, entity_transform, text_layout_info, anchor) in\n            // Weird that it is not `WithText`...\n            entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))\n        {\n            if cursor.check_square(entity_transform, anchor, text_layout_info.size) {\n                let z_depth = entity_transform.translation().z;\n                let Some(root_entity) = root_resolver.resolve_root(entity) else {\n                    continue;\n                };\n                camera_state.consider(root_entity, z_depth, || {\n                    cursor.cursor_in_world_pos.extend(z_depth)\n                });\n            }\n        }\n    }\n}\n\n/// Pan and zoom a camera entity with the mouse while inisde the editor.\n#[derive(Component)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol2dCameraControl {\n    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Line` units.\n    pub zoom_per_scroll_line: f32,\n    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Pixel` units.\n    pub zoom_per_scroll_pixel: f32,\n}\n\nimpl Default for Vpeol2dCameraControl {\n    fn default() -> Self {\n        Self {\n            zoom_per_scroll_line: 0.2,\n            zoom_per_scroll_pixel: 0.001,\n        }\n    }\n}\n\nfn camera_2d_pan(\n    mut egui_context: EguiContexts,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n    mut cameras_query: Query<\n        (Entity, &mut Transform, &VpeolCameraState),\n        With<Vpeol2dCameraControl>,\n    >,\n    mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,\n) -> Result {\n    enum MouseButtonOp {\n        JustPressed,\n        BeingPressed,\n    }\n\n    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {\n        if egui_context.ctx_mut()?.is_pointer_over_area() {\n            return Ok(());\n        }\n        MouseButtonOp::JustPressed\n    } else if mouse_buttons.pressed(MouseButton::Right) {\n        MouseButtonOp::BeingPressed\n    } else {\n        last_cursor_world_pos_by_camera.clear();\n        return Ok(());\n    };\n\n    for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {\n        let Some(cursor_ray) = camera_state.cursor_ray else {\n            continue;\n        };\n        let world_pos = cursor_ray.origin.truncate();\n\n        match mouse_button_op {\n            MouseButtonOp::JustPressed => {\n                last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);\n            }\n            MouseButtonOp::BeingPressed => {\n                if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {\n                    let movement = *prev_pos - world_pos;\n                    camera_transform.translation += movement.extend(0.0);\n                }\n            }\n        }\n    }\n    Ok(())\n}\n\nfn camera_2d_zoom(\n    mut egui_context: EguiContexts,\n    window_getter: WindowGetter,\n    mut cameras_query: Query<(\n        &mut Transform,\n        &VpeolCameraState,\n        &Camera,\n        &RenderTarget,\n        &Vpeol2dCameraControl,\n    )>,\n    mut wheel_events_reader: MessageReader<MouseWheel>,\n) -> Result {\n    if egui_context.ctx_mut()?.is_pointer_over_area() {\n        return Ok(());\n    }\n\n    for (mut camera_transform, camera_state, camera, render_target, camera_control) in\n        cameras_query.iter_mut()\n    {\n        let Some(cursor_ray) = camera_state.cursor_ray else {\n            continue;\n        };\n        let world_pos = cursor_ray.origin.truncate();\n\n        let zoom_amount: f32 = wheel_events_reader\n            .read()\n            .map(|wheel_event| match wheel_event.unit {\n                bevy::input::mouse::MouseScrollUnit::Line => {\n                    wheel_event.y * camera_control.zoom_per_scroll_line\n                }\n                bevy::input::mouse::MouseScrollUnit::Pixel => {\n                    wheel_event.y * camera_control.zoom_per_scroll_pixel\n                }\n            })\n            .sum();\n\n        if zoom_amount == 0.0 {\n            continue;\n        }\n\n        let scale_by = (-zoom_amount).exp();\n\n        let window = if let RenderTarget::Window(window_ref) = render_target {\n            window_getter.get_window(*window_ref).unwrap()\n        } else {\n            continue;\n        };\n        camera_transform.scale.x *= scale_by;\n        camera_transform.scale.y *= scale_by;\n        let Some(cursor_in_screen_pos) = window.cursor_position() else {\n            continue;\n        };\n        let Ok(new_cursor_ray) =\n            camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)\n        else {\n            continue;\n        };\n        let new_world_pos = new_cursor_ray.origin.truncate();\n        camera_transform.translation += (world_pos - new_world_pos).extend(0.0);\n    }\n    Ok(())\n}\n\n/// A position component that's edited and populated by vpeol_2d.\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol2dPosition(pub Vec2);\n\n/// A rotation component that's edited and populated by vpeol_2d.\n///\n/// The rotation is in radians around the Z axis. Editing is done with egui using degrees.\n#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol2dRotatation(pub f32);\n\n/// A scale component that's edited and populated by vpeol_2d.\n///\n/// Editing is done with egui using separate drag values for X and Y axes.\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol2dScale(pub Vec2);\n\nimpl Default for Vpeol2dScale {\n    fn default() -> Self {\n        Self(Vec2::ONE)\n    }\n}\n\nfn vpeol_2d_edit_transform_group(\n    mut ui: ResMut<YoleckUi>,\n    position_edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,\n    rotation_edit: YoleckEdit<&mut Vpeol2dRotatation>,\n    scale_edit: YoleckEdit<&mut Vpeol2dScale>,\n    passed_data: Res<YoleckPassedData>,\n) {\n    let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();\n    if !has_any {\n        return;\n    }\n\n    ui.group(|ui| {\n        ui.label(egui::RichText::new(\"Transform\").strong());\n        ui.separator();\n\n        vpeol_2d_edit_position_impl(ui, position_edit, &passed_data);\n        vpeol_2d_edit_rotation_impl(ui, rotation_edit);\n        vpeol_2d_edit_scale_impl(ui, scale_edit);\n    });\n}\n\nfn vpeol_2d_edit_position_impl(\n    ui: &mut egui::Ui,\n    mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,\n    passed_data: &YoleckPassedData,\n) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n    let mut average = DVec2::ZERO;\n    let mut num_entities = 0;\n    let mut transition = Vec2::ZERO;\n    for (entity, position) in edit.iter_matching() {\n        if let Some(pos) = passed_data.get::<Vec3>(entity) {\n            transition = pos.truncate() - position.0;\n        }\n        average += position.0.as_dvec2();\n        num_entities += 1;\n    }\n    average /= num_entities as f64;\n\n    ui.horizontal(|ui| {\n        let mut new_average = average;\n\n        ui.add(egui::Label::new(\"Position\"));\n        ui.add(egui::DragValue::new(&mut new_average.x).prefix(\"X:\"));\n        ui.add(egui::DragValue::new(&mut new_average.y).prefix(\"Y:\"));\n\n        transition += (new_average - average).as_vec2();\n    });\n\n    if transition.is_finite() && transition != Vec2::ZERO {\n        for (_, mut position) in edit.iter_matching_mut() {\n            position.0 += transition;\n        }\n    }\n}\n\nfn vpeol_2d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dRotatation>) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n\n    let mut average_rotation = 0.0_f32;\n    let mut num_entities = 0;\n\n    for rotation in edit.iter_matching() {\n        average_rotation += rotation.0;\n        num_entities += 1;\n    }\n    average_rotation /= num_entities as f32;\n\n    ui.horizontal(|ui| {\n        let mut rotation_deg = average_rotation.to_degrees();\n\n        ui.add(egui::Label::new(\"Rotation\"));\n        ui.add(\n            egui::DragValue::new(&mut rotation_deg)\n                .speed(1.0)\n                .suffix(\"°\"),\n        );\n\n        let new_rotation = rotation_deg.to_radians();\n        let transition = new_rotation - average_rotation;\n\n        if transition.is_finite() && transition != 0.0 {\n            for mut rotation in edit.iter_matching_mut() {\n                rotation.0 += transition;\n            }\n        }\n    });\n}\n\nfn vpeol_2d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dScale>) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n    let mut average = DVec2::ZERO;\n    let mut num_entities = 0;\n\n    for scale in edit.iter_matching() {\n        average += scale.0.as_dvec2();\n        num_entities += 1;\n    }\n    average /= num_entities as f64;\n\n    ui.horizontal(|ui| {\n        let mut new_average = average;\n\n        ui.add(egui::Label::new(\"Scale\"));\n        ui.vertical(|ui| {\n            ui.centered_and_justified(|ui| {\n                let axis_average = (average.x + average.y) / 2.0;\n                let mut new_axis_average = axis_average;\n                if ui\n                    .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))\n                    .dragged()\n                {\n                    // Use difference instead of ration to avoid problems when reaching/crossing\n                    // the zero.\n                    let diff = new_axis_average - axis_average;\n                    new_average.x += diff;\n                    new_average.y += diff;\n                }\n            });\n            ui.horizontal(|ui| {\n                ui.add(\n                    egui::DragValue::new(&mut new_average.x)\n                        .prefix(\"X:\")\n                        .speed(0.01),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut new_average.y)\n                        .prefix(\"Y:\")\n                        .speed(0.01),\n                );\n            });\n        });\n\n        let transition = (new_average - average).as_vec2();\n\n        if transition.is_finite() && transition != Vec2::ZERO {\n            for mut scale in edit.iter_matching_mut() {\n                scale.0 += transition;\n            }\n        }\n    });\n}\n\nfn vpeol_2d_init_position(\n    mut egui_context: EguiContexts,\n    ui: Res<YoleckUi>,\n    mut edit: YoleckEdit<&mut Vpeol2dPosition>,\n    cameras_query: Query<&VpeolCameraState>,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n) -> YoleckExclusiveSystemDirective {\n    let Ok(mut position) = edit.single_mut() else {\n        return YoleckExclusiveSystemDirective::Finished;\n    };\n\n    let Some(cursor_ray) = cameras_query\n        .iter()\n        .find_map(|camera_state| camera_state.cursor_ray)\n    else {\n        return YoleckExclusiveSystemDirective::Listening;\n    };\n\n    position.0 = cursor_ray.origin.truncate();\n\n    if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {\n        return YoleckExclusiveSystemDirective::Listening;\n    }\n\n    if mouse_buttons.just_released(MouseButton::Left) {\n        return YoleckExclusiveSystemDirective::Finished;\n    }\n\n    YoleckExclusiveSystemDirective::Listening\n}\n\nfn vpeol_2d_populate_transform(\n    mut populate: YoleckPopulate<(\n        &Vpeol2dPosition,\n        Option<&Vpeol2dRotatation>,\n        Option<&Vpeol2dScale>,\n        &YoleckBelongsToLevel,\n    )>,\n    levels_query: Query<&VpeolRepositionLevel>,\n) {\n    populate.populate(\n        |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {\n            let mut transform = Transform::from_translation(position.0.extend(0.0));\n            if let Some(Vpeol2dRotatation(rotation)) = rotation {\n                transform = transform.with_rotation(Quat::from_rotation_z(*rotation));\n            }\n            if let Some(Vpeol2dScale(scale)) = scale {\n                transform = transform.with_scale(scale.extend(1.0));\n            }\n\n            if let Ok(VpeolRepositionLevel(level_transform)) =\n                levels_query.get(belongs_to_level.level)\n            {\n                transform = *level_transform * transform;\n            }\n\n            cmd.insert((transform, GlobalTransform::from(transform)));\n        },\n    )\n}\n"
  },
  {
    "path": "src/vpeol_3d.rs",
    "content": "//! # Viewport Editing Overlay for 3D games.\n//!\n//! Use this module to implement simple 3D editing for 3D games.\n//!\n//! To use add the egui and Yoleck plugins to the Bevy app, as well as the plugin of this module:\n//!\n//! ```no_run\n//! # use bevy::prelude::*;\n//! # use bevy_yoleck::bevy_egui::EguiPlugin;\n//! # use bevy_yoleck::prelude::*;\n//! # use bevy_yoleck::vpeol::prelude::*;\n//! # let mut app = App::new();\n//! app.add_plugins(EguiPlugin::default());\n//! app.add_plugins(YoleckPluginForEditor);\n//! // - Use `Vpeol3dPluginForGame` instead when setting up for game.\n//! // - Use topdown is for games that utilize the XZ plane. There is also\n//! //   `Vpeol3dPluginForEditor::sidescroller` for games that mainly need the XY plane.\n//! app.add_plugins(Vpeol3dPluginForEditor::topdown());\n//! ```\n//!\n//! Add the following components to the camera entity:\n//! * [`VpeolCameraState`] in order to select and drag entities.\n//! * [`Vpeol3dCameraControl`] in order to control the camera with the mouse. This one can be\n//!   skipped if there are other means to control the camera inside the editor, or if no camera\n//!   control inside the editor is desired.\n//!\n//! ```no_run\n//! # use bevy::prelude::*;\n//! # use bevy_yoleck::vpeol::prelude::*;\n//! # let commands: Commands = panic!();\n//! commands\n//!     .spawn(Camera3d::default())\n//!     .insert(VpeolCameraState::default())\n//!     // Use a variant of the camera controls that fit the choice of editor plugin.\n//!     .insert(Vpeol3dCameraControl::topdown());\n//! ```\n//!\n//! ## Custom Camera Modes\n//!\n//! You can customize available camera modes using [`YoleckCameraChoices`]:\n//!\n//! ```no_run\n//! # use bevy::prelude::*;\n//! # use bevy_yoleck::vpeol::prelude::*;\n//! # let mut app = App::new();\n//! app.insert_resource(\n//!     YoleckCameraChoices::default()\n//!         .choice_with_transform(\n//!             \"Custom Camera\",\n//!             {\n//!                 let mut control = Vpeol3dCameraControl::fps();\n//!                 control.mode = Vpeol3dCameraMode::Custom(0);\n//!                 control\n//!             },\n//!             Vec3::new(10.0, 10.0, 10.0),\n//!             Vec3::ZERO,\n//!             Vec3::Y,\n//!         )\n//! );\n//!\n//! // Implement custom camera movement\n//! app.add_systems(PostUpdate, custom_camera_movement);\n//!\n//! fn custom_camera_movement(\n//!     mut cameras: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n//! ) {\n//!     for (mut transform, control) in cameras.iter_mut() {\n//!         if control.mode == Vpeol3dCameraMode::Custom(0) {\n//!             // Your custom camera logic here\n//!         }\n//!     }\n//! }\n//! ```\n//!\n//! Entity selection by clicking on it is supported by just adding the plugin. To implement\n//! dragging, there are two options:\n//!\n//! 1. Add the [`Vpeol3dPosition`] Yoleck component and use it as the source of position. Gizmo\n//!    with axis knobs (X, Y, Z) are automatically added to all entities with `Vpeol3dPosition`.\n//!    Configure them using the [`Vpeol3dTranslationGizmoConfig`] resource. Optionally add\n//!    [`Vpeol3dRotation`] (edited with Euler angles) and [`Vpeol3dScale`] (edited with X, Y, Z\n//!    values) for rotation and scale support.\n//!     ```no_run\n//!     # use bevy::prelude::*;\n//!     # use bevy_yoleck::prelude::*;\n//!     # use bevy_yoleck::vpeol::prelude::*;\n//!     # use serde::{Deserialize, Serialize};\n//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n//!     # struct Example;\n//!     # let mut app = App::new();\n//!     app.add_yoleck_entity_type({\n//!         YoleckEntityType::new(\"Example\")\n//!             .with::<Vpeol3dPosition>() // vpeol_3d dragging with axis knobs\n//!             .with::<Vpeol3dRotation>() // optional: rotation with egui (Euler angles)\n//!             .with::<Vpeol3dScale>() // optional: scale with egui\n//!             .with::<Example>() // entity's specific data and systems\n//!     });\n//!     ```\n//! 2. Use data passing. vpeol_3d will pass a `Vec3` to the entity being dragged:\n//!     ```no_run\n//!     # use bevy::prelude::*;\n//!     # use bevy_yoleck::prelude::*;\n//!     # use serde::{Deserialize, Serialize};\n//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n//!     # struct Example {\n//!     #     position: Vec3,\n//!     # }\n//!     # let mut app = App::new();\n//!     fn edit_example(mut edit: YoleckEdit<(Entity, &mut Example)>, passed_data: Res<YoleckPassedData>) {\n//!         let Ok((entity, mut example)) = edit.single_mut() else { return };\n//!         if let Some(pos) = passed_data.get::<Vec3>(entity) {\n//!             example.position = *pos;\n//!         }\n//!     }\n//!\n//!     fn populate_example(\n//!         mut populate: YoleckPopulate<&Example>,\n//!         asset_server: Res<AssetServer>\n//!     ) {\n//!         populate.populate(|_ctx, mut cmd, example| {\n//!             cmd.insert(Transform::from_translation(example.position));\n//!             cmd.insert(SceneRoot(asset_server.load(\"scene.glb#Scene0\")));\n//!         });\n//!     }\n//!     ```\n\nuse std::any::TypeId;\n\nuse crate::bevy_egui::egui;\nuse crate::editor_panels::YoleckPanelUi;\nuse crate::exclusive_systems::{\n    YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,\n};\nuse crate::vpeol::{\n    VpeolBasePlugin, VpeolCameraState, VpeolClicksOnObjectsState, VpeolDragPlane,\n    VpeolRepositionLevel, VpeolRootResolver, VpeolSystems, handle_clickable_children_system,\n    ray_intersection_with_mesh,\n};\nuse crate::{\n    YoleckBelongsToLevel, YoleckDirective, YoleckEditorTopPanelSections, YoleckSchedule, prelude::*,\n};\nuse bevy::camera::visibility::VisibleEntities;\nuse bevy::color::palettes::css;\nuse bevy::input::mouse::{MouseMotion, MouseWheel};\nuse bevy::math::DVec3;\nuse bevy::prelude::*;\nuse bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow};\nuse bevy_egui::EguiContexts;\nuse serde::{Deserialize, Serialize};\n\n/// Add the systems required for loading levels that use vpeol_3d components\npub struct Vpeol3dPluginForGame;\n\nimpl Plugin for Vpeol3dPluginForGame {\n    fn build(&self, app: &mut App) {\n        app.add_systems(\n            YoleckSchedule::OverrideCommonComponents,\n            vpeol_3d_populate_transform,\n        );\n        #[cfg(feature = \"bevy_reflect\")]\n        register_reflect_types(app);\n    }\n}\n\n#[cfg(feature = \"bevy_reflect\")]\nfn register_reflect_types(app: &mut App) {\n    app.register_type::<Vpeol3dPosition>();\n    app.register_type::<Vpeol3dRotation>();\n    app.register_type::<Vpeol3dScale>();\n    app.register_type::<Vpeol3dCameraControl>();\n}\n\n/// Add the systems required for 3D editing.\n///\n/// * 3D camera control (for cameras with [`Vpeol3dCameraControl`])\n/// * Entity selection.\n/// * Entity dragging.\n/// * Connecting nested entities.\npub struct Vpeol3dPluginForEditor {\n    /// The plane to configure the global [`VpeolDragPlane`] resource with.\n    ///\n    /// Indiviual entities can override this with their own [`VpeolDragPlane`] component.\n    ///\n    /// It is a good idea to match this to [`Vpeol3dCameraControl::plane`].\n    pub drag_plane: InfinitePlane3d,\n}\n\nimpl Vpeol3dPluginForEditor {\n    /// For sidescroller games - drag entities along the XY plane.\n    ///\n    /// Indiviual entities can override this with a [`VpeolDragPlane`] component.\n    ///\n    /// This combines well with [`Vpeol3dCameraControl::sidescroller`].\n    pub fn sidescroller() -> Self {\n        Self {\n            drag_plane: InfinitePlane3d { normal: Dir3::Z },\n        }\n    }\n\n    /// For games that are not sidescrollers - drag entities along the XZ plane.\n    ///\n    /// Indiviual entities can override this with a [`VpeolDragPlane`] component.\n    ///\n    /// This combines well with [`Vpeol3dCameraControl::topdown`].\n    pub fn topdown() -> Self {\n        Self {\n            drag_plane: InfinitePlane3d { normal: Dir3::Y },\n        }\n    }\n}\n\nimpl Plugin for Vpeol3dPluginForEditor {\n    fn build(&self, app: &mut App) {\n        app.add_plugins(VpeolBasePlugin);\n        app.add_plugins(Vpeol3dPluginForGame);\n        app.insert_resource(VpeolDragPlane(self.drag_plane));\n        app.init_resource::<Vpeol3dTranslationGizmoConfig>();\n        app.init_resource::<YoleckCameraChoices>();\n\n        let camera_mode_selector = app.register_system(vpeol_3d_camera_mode_selector);\n        app.world_mut()\n            .resource_mut::<YoleckEditorTopPanelSections>()\n            .0\n            .push(camera_mode_selector);\n        let translation_gizmo_mode_selector =\n            app.register_system(vpeol_3d_translation_gizmo_mode_selector);\n        app.world_mut()\n            .resource_mut::<YoleckEditorTopPanelSections>()\n            .0\n            .push(translation_gizmo_mode_selector);\n\n        app.add_systems(\n            Update,\n            (update_camera_status_for_models,).in_set(VpeolSystems::UpdateCameraState),\n        );\n        app.add_systems(\n            PostUpdate,\n            (\n                camera_3d_wasd_movement,\n                camera_3d_move_along_plane_normal,\n                camera_3d_rotate,\n            )\n                .run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_systems(\n            Update,\n            draw_scene_gizmo.run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_systems(\n            Update,\n            (\n                ApplyDeferred,\n                handle_clickable_children_system::<With<Mesh3d>, ()>,\n                ApplyDeferred,\n            )\n                .chain()\n                .run_if(in_state(YoleckEditorState::EditorActive)),\n        );\n        app.add_yoleck_edit_system(vpeol_3d_edit_transform_group);\n        app.world_mut()\n            .resource_mut::<YoleckEntityCreationExclusiveSystems>()\n            .on_entity_creation(|queue| queue.push_back(vpeol_3d_init_position));\n        app.add_yoleck_edit_system(vpeol_3d_edit_axis_knobs);\n    }\n}\n\nfn update_camera_status_for_models(\n    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,\n    entities_query: Query<(Entity, &GlobalTransform, &Mesh3d)>,\n    mesh_assets: Res<Assets<Mesh>>,\n    root_resolver: VpeolRootResolver,\n) {\n    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {\n        let Some(cursor_ray) = camera_state.cursor_ray else {\n            continue;\n        };\n        for (entity, global_transform, mesh) in\n            entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh3d>()))\n        {\n            let Some(mesh) = mesh_assets.get(&mesh.0) else {\n                continue;\n            };\n\n            let inverse_transform = global_transform.to_matrix().inverse();\n\n            // Note: the transform may change the ray's length, which Bevy no longer supports\n            // (since version 0.13), so we keep the ray length separately and apply it later to the\n            // distance.\n            let ray_origin = inverse_transform.transform_point3(cursor_ray.origin);\n            let ray_vector = inverse_transform.transform_vector3(*cursor_ray.direction);\n            let Ok((ray_direction, ray_length_factor)) = Dir3::new_and_length(ray_vector) else {\n                continue;\n            };\n\n            let ray_in_object_coords = Ray3d {\n                origin: ray_origin,\n                direction: ray_direction,\n            };\n\n            let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {\n                continue;\n            };\n\n            let distance = distance / ray_length_factor;\n\n            let Some(root_entity) = root_resolver.resolve_root(entity) else {\n                continue;\n            };\n            camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));\n        }\n    }\n}\n\n/// A single camera mode choice with its control settings and optional initial transform.\n///\n/// The `control` field contains the camera settings that will be applied when this mode is selected.\n/// The `initial_transform` field, if present, will reposition the camera when switching to this mode.\npub struct YoleckCameraChoice {\n    /// Display name shown in the camera mode selector UI.\n    pub name: String,\n    /// Camera control settings for this mode.\n    pub control: Vpeol3dCameraControl,\n    /// Optional initial transform (position, look_at, up) to apply when switching to this mode.\n    pub initial_transform: Option<(Vec3, Vec3, Vec3)>,\n}\n\n/// Resource that defines available camera modes in the editor.\n///\n/// This allows users to customize which camera modes are available and add custom modes.\n///\n/// # Example\n///\n/// ```no_run\n/// # use bevy::prelude::*;\n/// # use bevy_yoleck::vpeol::prelude::*;\n/// # let mut app = App::new();\n/// app.insert_resource(\n///     YoleckCameraChoices::default()\n///         .choice_with_transform(\n///             \"Isometric\",\n///             {\n///                 let mut control = Vpeol3dCameraControl::fps();\n///                 control.mode = Vpeol3dCameraMode::Custom(2);\n///                 control.allow_rotation_while_maintaining_up = None;\n///                 control\n///             },\n///             Vec3::new(10.0, 10.0, 10.0),\n///             Vec3::ZERO,\n///             Vec3::Y,\n///         )\n/// );\n/// ```\n#[derive(Resource)]\npub struct YoleckCameraChoices {\n    pub choices: Vec<YoleckCameraChoice>,\n}\n\nimpl YoleckCameraChoices {\n    pub fn new() -> Self {\n        Self {\n            choices: Vec::new(),\n        }\n    }\n\n    /// Add a camera mode choice without initial transform.\n    ///\n    /// The camera will keep its current position when switching to this mode.\n    pub fn choice(mut self, name: impl Into<String>, control: Vpeol3dCameraControl) -> Self {\n        self.choices.push(YoleckCameraChoice {\n            name: name.into(),\n            control,\n            initial_transform: None,\n        });\n        self\n    }\n\n    /// Add a camera mode choice with initial transform.\n    ///\n    /// When switching to this mode, the camera will be repositioned according to\n    /// the provided `position`, `look_at`, and `up` parameters.\n    pub fn choice_with_transform(\n        mut self,\n        name: impl Into<String>,\n        control: Vpeol3dCameraControl,\n        position: Vec3,\n        look_at: Vec3,\n        up: Vec3,\n    ) -> Self {\n        self.choices.push(YoleckCameraChoice {\n            name: name.into(),\n            control,\n            initial_transform: Some((position, look_at, up)),\n        });\n        self\n    }\n}\n\nimpl Default for YoleckCameraChoices {\n    fn default() -> Self {\n        Self::new()\n            .choice_with_transform(\n                \"FPS\",\n                Vpeol3dCameraControl::fps(),\n                Vec3::ZERO,\n                Vec3::NEG_Z,\n                Vec3::Y,\n            )\n            .choice_with_transform(\n                \"Sidescroller\",\n                Vpeol3dCameraControl::sidescroller(),\n                Vec3::new(0.0, 0.0, 10.0),\n                Vec3::ZERO,\n                Vec3::Y,\n            )\n            .choice_with_transform(\n                \"Topdown\",\n                Vpeol3dCameraControl::topdown(),\n                Vec3::new(0.0, 10.0, 0.0),\n                Vec3::ZERO,\n                Vec3::NEG_Z,\n            )\n    }\n}\n\n/// Camera mode identifier for type-safe camera mode comparisons.\n///\n/// Use this enum to identify which camera mode is currently active, instead of string comparisons.\n/// The built-in modes (`Fps`, `Sidescroller`, `Topdown`) are provided by default.\n/// Use `Custom(u32)` for user-defined camera modes with unique identifiers.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub enum Vpeol3dCameraMode {\n    /// FPS-style camera with full rotation freedom.\n    Fps,\n    /// Fixed camera for sidescroller games (XY plane).\n    Sidescroller,\n    /// Top-down camera for games using XZ plane.\n    Topdown,\n    /// Custom camera mode with a unique identifier.\n    Custom(u32),\n}\n\n/// Move and rotate a camera entity with the mouse while inside the editor.\n#[derive(Component, Clone)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol3dCameraControl {\n    /// Camera mode identifier for type-safe mode comparisons.\n    ///\n    /// Use this to identify which camera mode is active in custom camera systems.\n    pub mode: Vpeol3dCameraMode,\n    /// Defines the plane normal for mouse wheel zoom movement.\n    pub plane: InfinitePlane3d,\n    /// If `Some`, enable mouse rotation. The up direction of the camera will be the specific\n    /// direction.\n    ///\n    /// It is a good idea to match this to [`Vpeol3dPluginForEditor::drag_plane`].\n    pub allow_rotation_while_maintaining_up: Option<Dir3>,\n    /// How much to change the proximity to the plane when receiving scroll event in\n    /// `MouseScrollUnit::Line` units.\n    pub proximity_per_scroll_line: f32,\n    /// How much to change the proximity to the plane when receiving scroll event in\n    /// `MouseScrollUnit::Pixel` units.\n    pub proximity_per_scroll_pixel: f32,\n    /// Movement speed for WASD controls (units per second).\n    pub wasd_movement_speed: f32,\n    /// Mouse sensitivity for camera rotation.\n    pub mouse_sensitivity: f32,\n}\n\nimpl Vpeol3dCameraControl {\n    /// Preset for FPS-style camera control with full rotation freedom.\n    ///\n    /// This mode allows complete free-look rotation with mouse and WASD movement.\n    pub fn fps() -> Self {\n        Self {\n            mode: Vpeol3dCameraMode::Fps,\n            plane: InfinitePlane3d { normal: Dir3::Y },\n            allow_rotation_while_maintaining_up: Some(Dir3::Y),\n            proximity_per_scroll_line: 2.0,\n            proximity_per_scroll_pixel: 0.01,\n            wasd_movement_speed: 10.0,\n            mouse_sensitivity: 0.003,\n        }\n    }\n\n    /// Preset for sidescroller games, where the the game world is on the XY plane.\n    ///\n    /// With this preset, the camera stays fixed looking at the scene from the side.\n    ///\n    /// This combines well with [`Vpeol3dPluginForEditor::sidescroller`].\n    pub fn sidescroller() -> Self {\n        Self {\n            mode: Vpeol3dCameraMode::Sidescroller,\n            plane: InfinitePlane3d {\n                normal: Dir3::NEG_Z,\n            },\n            allow_rotation_while_maintaining_up: None,\n            proximity_per_scroll_line: 2.0,\n            proximity_per_scroll_pixel: 0.01,\n            wasd_movement_speed: 10.0,\n            mouse_sensitivity: 0.003,\n        }\n    }\n\n    /// Preset for games where the the game world is mainly on the XZ plane (though there can still\n    /// be verticality)\n    ///\n    /// This combines well with [`Vpeol3dPluginForEditor::topdown`].\n    pub fn topdown() -> Self {\n        Self {\n            mode: Vpeol3dCameraMode::Topdown,\n            plane: InfinitePlane3d {\n                normal: Dir3::NEG_Y,\n            },\n            allow_rotation_while_maintaining_up: None,\n            proximity_per_scroll_line: 2.0,\n            proximity_per_scroll_pixel: 0.01,\n            wasd_movement_speed: 10.0,\n            mouse_sensitivity: 0.003,\n        }\n    }\n}\n\nfn camera_3d_wasd_movement(\n    mut egui_context: EguiContexts,\n    keyboard_input: Res<ButtonInput<KeyCode>>,\n    time: Res<Time>,\n    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n) -> Result {\n    if egui_context.ctx_mut()?.wants_keyboard_input() {\n        return Ok(());\n    }\n\n    let mut direction = Vec3::ZERO;\n\n    if keyboard_input.pressed(KeyCode::KeyW) {\n        direction += Vec3::NEG_Z;\n    }\n    if keyboard_input.pressed(KeyCode::KeyS) {\n        direction += Vec3::Z;\n    }\n    if keyboard_input.pressed(KeyCode::KeyA) {\n        direction += Vec3::NEG_X;\n    }\n    if keyboard_input.pressed(KeyCode::KeyD) {\n        direction += Vec3::X;\n    }\n    if keyboard_input.pressed(KeyCode::KeyE) {\n        direction += Vec3::Y;\n    }\n    if keyboard_input.pressed(KeyCode::KeyQ) {\n        direction += Vec3::NEG_Y;\n    }\n\n    if direction == Vec3::ZERO {\n        return Ok(());\n    }\n\n    direction = direction.normalize_or_zero();\n\n    let speed_multiplier = if keyboard_input.pressed(KeyCode::ShiftLeft) {\n        2.0\n    } else {\n        1.0\n    };\n\n    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {\n        let movement = match camera_control.mode {\n            Vpeol3dCameraMode::Sidescroller => {\n                let mut world_direction = Vec3::ZERO;\n                if keyboard_input.pressed(KeyCode::KeyW) {\n                    world_direction.y += 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyS) {\n                    world_direction.y -= 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyA) {\n                    world_direction.x -= 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyD) {\n                    world_direction.x += 1.0;\n                }\n                world_direction.normalize_or_zero()\n                    * camera_control.wasd_movement_speed\n                    * speed_multiplier\n                    * time.delta_secs()\n            }\n            Vpeol3dCameraMode::Topdown => {\n                let mut world_direction = Vec3::ZERO;\n                if keyboard_input.pressed(KeyCode::KeyW) {\n                    world_direction.z -= 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyS) {\n                    world_direction.z += 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyA) {\n                    world_direction.x -= 1.0;\n                }\n                if keyboard_input.pressed(KeyCode::KeyD) {\n                    world_direction.x += 1.0;\n                }\n                world_direction.normalize_or_zero()\n                    * camera_control.wasd_movement_speed\n                    * speed_multiplier\n                    * time.delta_secs()\n            }\n            _ => {\n                camera_transform.rotation\n                    * direction\n                    * camera_control.wasd_movement_speed\n                    * speed_multiplier\n                    * time.delta_secs()\n            }\n        };\n\n        camera_transform.translation += movement;\n    }\n    Ok(())\n}\n\nfn camera_3d_move_along_plane_normal(\n    mut egui_context: EguiContexts,\n    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n    mut wheel_events_reader: MessageReader<MouseWheel>,\n) -> Result {\n    if egui_context.ctx_mut()?.is_pointer_over_area() {\n        return Ok(());\n    }\n\n    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {\n        let zoom_amount: f32 = wheel_events_reader\n            .read()\n            .map(|wheel_event| match wheel_event.unit {\n                bevy::input::mouse::MouseScrollUnit::Line => {\n                    wheel_event.y * camera_control.proximity_per_scroll_line\n                }\n                bevy::input::mouse::MouseScrollUnit::Pixel => {\n                    wheel_event.y * camera_control.proximity_per_scroll_pixel\n                }\n            })\n            .sum();\n\n        if zoom_amount == 0.0 {\n            continue;\n        }\n\n        camera_transform.translation += zoom_amount * *camera_control.plane.normal;\n    }\n    Ok(())\n}\n\nfn draw_scene_gizmo(\n    mut egui_context: EguiContexts,\n    mut cameras_query: Query<&mut Transform, With<VpeolCameraState>>,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n    mut first_frame_skipped: Local<bool>,\n    editor_viewport: Res<crate::editor_window::YoleckEditorViewportRect>,\n) -> Result {\n    if !*first_frame_skipped {\n        *first_frame_skipped = true;\n        return Ok(());\n    }\n\n    let ctx = egui_context.ctx_mut()?;\n\n    if !ctx.is_using_pointer() && ctx.input(|i| i.viewport_rect().width() == 0.0) {\n        return Ok(());\n    }\n\n    let Ok(mut camera_transform) = cameras_query.single_mut() else {\n        return Ok(());\n    };\n\n    let screen_rect = editor_viewport\n        .rect\n        .unwrap_or_else(|| ctx.input(|i| i.viewport_rect()));\n\n    if screen_rect.width() == 0.0 || screen_rect.height() == 0.0 {\n        return Ok(());\n    }\n\n    let gizmo_size = 60.0;\n    let axis_length = 25.0;\n    let margin = 20.0;\n    let click_radius = 10.0;\n\n    let center = egui::Pos2::new(\n        screen_rect.max.x - margin - gizmo_size / 2.0,\n        screen_rect.min.y + margin + gizmo_size / 2.0,\n    );\n\n    let camera_rotation = camera_transform.rotation;\n    let inv_rotation = camera_rotation.inverse();\n\n    let world_x = inv_rotation * Vec3::X;\n    let world_y = inv_rotation * Vec3::Y;\n    let world_z = inv_rotation * Vec3::Z;\n\n    let to_screen = |v: Vec3| -> egui::Pos2 {\n        let perspective_scale = 1.0 / (1.0 - v.z * 0.3);\n        let screen_x = v.x * axis_length * perspective_scale;\n        let screen_y = v.y * axis_length * perspective_scale;\n\n        let len = (screen_x * screen_x + screen_y * screen_y).sqrt();\n        let min_len = 8.0;\n        let (screen_x, screen_y) = if len < min_len && len > 0.001 {\n            let scale = min_len / len;\n            (screen_x * scale, screen_y * scale)\n        } else {\n            (screen_x, screen_y)\n        };\n\n        egui::Pos2::new(center.x + screen_x, center.y - screen_y)\n    };\n\n    let x_pos = to_screen(world_x);\n    let x_neg = to_screen(-world_x);\n    let y_pos = to_screen(world_y);\n    let y_neg = to_screen(-world_y);\n    let z_pos = to_screen(world_z);\n    let z_neg = to_screen(-world_z);\n\n    let cursor_pos = ctx.input(|i| i.pointer.hover_pos());\n    let gizmo_rect = egui::Rect::from_center_size(center, egui::Vec2::splat(gizmo_size));\n\n    if let Some(cursor) = cursor_pos\n        && mouse_buttons.just_pressed(MouseButton::Left)\n        && gizmo_rect.contains(cursor)\n    {\n        let distances = [\n            (cursor.distance(x_pos), Vec3::NEG_X, Vec3::Y),\n            (cursor.distance(x_neg), Vec3::X, Vec3::Y),\n            (cursor.distance(y_pos), Vec3::NEG_Y, Vec3::Z),\n            (cursor.distance(y_neg), Vec3::Y, Vec3::Z),\n            (cursor.distance(z_pos), Vec3::NEG_Z, Vec3::Y),\n            (cursor.distance(z_neg), Vec3::Z, Vec3::Y),\n        ];\n\n        if let Some((_, forward, up)) = distances\n            .iter()\n            .filter(|(d, _, _)| *d < click_radius)\n            .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())\n        {\n            camera_transform.look_to(*forward, *up);\n        }\n    }\n\n    #[derive(Clone, Copy)]\n    struct AxisData {\n        depth: f32,\n        color_bright: egui::Color32,\n        color_dim: egui::Color32,\n        pos_end: egui::Pos2,\n        neg_end: egui::Pos2,\n        world_dir: Vec3,\n    }\n\n    let mut axes = vec![\n        AxisData {\n            depth: world_x.z.abs(),\n            color_bright: egui::Color32::from_rgb(230, 60, 60),\n            color_dim: egui::Color32::from_rgb(120, 50, 50),\n            pos_end: x_pos,\n            neg_end: x_neg,\n            world_dir: world_x,\n        },\n        AxisData {\n            depth: world_y.z.abs(),\n            color_bright: egui::Color32::from_rgb(60, 230, 60),\n            color_dim: egui::Color32::from_rgb(50, 120, 50),\n            pos_end: y_pos,\n            neg_end: y_neg,\n            world_dir: world_y,\n        },\n        AxisData {\n            depth: world_z.z.abs(),\n            color_bright: egui::Color32::from_rgb(60, 120, 230),\n            color_dim: egui::Color32::from_rgb(50, 70, 120),\n            pos_end: z_pos,\n            neg_end: z_neg,\n            world_dir: world_z,\n        },\n    ];\n    axes.sort_by(|a, b| b.depth.partial_cmp(&a.depth).unwrap());\n\n    let painter = ctx.layer_painter(egui::LayerId::new(\n        egui::Order::Foreground,\n        egui::Id::new(\"scene_gizmo\"),\n    ));\n\n    painter.circle_filled(\n        center,\n        gizmo_size / 2.0,\n        egui::Color32::from_rgba_unmultiplied(40, 40, 40, 200),\n    );\n\n    let stroke_bright = 3.0;\n    let stroke_dim = 2.0;\n    let cone_radius_bright = 5.0;\n    let cone_radius_dim = 3.5;\n    let cone_length = 8.0;\n\n    for axis in &axes {\n        let (front_end, front_color, back_end, back_color) = if axis.world_dir.z >= 0.0 {\n            (\n                axis.pos_end,\n                axis.color_bright,\n                axis.neg_end,\n                axis.color_dim,\n            )\n        } else {\n            (\n                axis.neg_end,\n                axis.color_dim,\n                axis.pos_end,\n                axis.color_bright,\n            )\n        };\n\n        let back_dir = (back_end - center).normalized();\n        let back_line_end = back_end - back_dir * cone_length;\n        painter.line_segment(\n            [center, back_line_end],\n            egui::Stroke::new(stroke_dim, back_color),\n        );\n\n        let back_perp = egui::Vec2::new(-back_dir.y, back_dir.x);\n        let back_cone_base = back_end - back_dir * cone_length;\n        let back_cone = vec![\n            back_end,\n            back_cone_base + back_perp * cone_radius_dim,\n            back_cone_base - back_perp * cone_radius_dim,\n        ];\n        painter.add(egui::Shape::convex_polygon(\n            back_cone,\n            back_color,\n            egui::Stroke::NONE,\n        ));\n\n        let front_dir = (front_end - center).normalized();\n        let front_line_end = front_end - front_dir * cone_length;\n        painter.line_segment(\n            [center, front_line_end],\n            egui::Stroke::new(stroke_bright, front_color),\n        );\n\n        let front_perp = egui::Vec2::new(-front_dir.y, front_dir.x);\n        let front_cone_base = front_end - front_dir * cone_length;\n        let front_cone = vec![\n            front_end,\n            front_cone_base + front_perp * cone_radius_bright,\n            front_cone_base - front_perp * cone_radius_bright,\n        ];\n        painter.add(egui::Shape::convex_polygon(\n            front_cone,\n            front_color,\n            egui::Stroke::NONE,\n        ));\n    }\n\n    let label_offset = 12.0;\n    let font_id = egui::FontId::proportional(12.0);\n\n    let axis_labels = [\n        (\"X\", x_pos, egui::Color32::from_rgb(230, 60, 60), world_x.z),\n        (\"Y\", y_pos, egui::Color32::from_rgb(60, 230, 60), world_y.z),\n        (\"Z\", z_pos, egui::Color32::from_rgb(60, 120, 230), world_z.z),\n    ];\n\n    for (label, pos, color, depth) in axis_labels {\n        let dir = (pos - center).normalized();\n        let label_pos = pos + dir * label_offset;\n        let alpha = if depth >= 0.0 { 255 } else { 120 };\n        let label_color =\n            egui::Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);\n        painter.text(\n            label_pos,\n            egui::Align2::CENTER_CENTER,\n            label,\n            font_id.clone(),\n            label_color,\n        );\n    }\n\n    Ok(())\n}\n\nfn camera_3d_rotate(\n    mut egui_context: EguiContexts,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,\n    mut mouse_motion_reader: MessageReader<MouseMotion>,\n    mut cursor_options: Query<&mut CursorOptions, With<PrimaryWindow>>,\n    mut is_rotating: Local<bool>,\n) -> Result {\n    let Ok(mut cursor) = cursor_options.single_mut() else {\n        return Ok(());\n    };\n\n    if mouse_buttons.just_pressed(MouseButton::Right) {\n        if egui_context.ctx_mut()?.is_pointer_over_area() {\n            return Ok(());\n        }\n\n        let has_rotatable_camera = cameras_query\n            .iter()\n            .any(|(_, control)| control.allow_rotation_while_maintaining_up.is_some());\n\n        if has_rotatable_camera {\n            cursor.grab_mode = CursorGrabMode::Locked;\n            cursor.visible = false;\n            *is_rotating = true;\n        }\n    }\n\n    if mouse_buttons.just_released(MouseButton::Right) {\n        cursor.grab_mode = CursorGrabMode::None;\n        cursor.visible = true;\n        *is_rotating = false;\n    }\n\n    if !*is_rotating {\n        return Ok(());\n    }\n\n    let mut delta = Vec2::ZERO;\n    for motion in mouse_motion_reader.read() {\n        delta += motion.delta;\n    }\n\n    if delta == Vec2::ZERO {\n        return Ok(());\n    }\n\n    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {\n        let Some(maintaining_up) = camera_control.allow_rotation_while_maintaining_up else {\n            continue;\n        };\n\n        let yaw = -delta.x * camera_control.mouse_sensitivity;\n        let pitch = -delta.y * camera_control.mouse_sensitivity;\n\n        let yaw_rotation = Quat::from_axis_angle(*maintaining_up, yaw);\n        camera_transform.rotation = yaw_rotation * camera_transform.rotation;\n\n        let right = camera_transform.right();\n        let pitch_rotation = Quat::from_axis_angle(*right, pitch);\n        camera_transform.rotation = pitch_rotation * camera_transform.rotation;\n\n        let new_forward = camera_transform.forward();\n        camera_transform.look_to(*new_forward, *maintaining_up);\n    }\n    Ok(())\n}\n\npub fn vpeol_3d_translation_gizmo_mode_selector(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut config: ResMut<Vpeol3dTranslationGizmoConfig>,\n) -> Result {\n    let ui = &mut **ui;\n    ui.add_space(ui.available_width());\n\n    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {\n        ui.radio_value(\n            &mut config.mode,\n            Vpeol3dTranslationGizmoMode::Local,\n            \"Local\",\n        );\n        ui.radio_value(\n            &mut config.mode,\n            Vpeol3dTranslationGizmoMode::World,\n            \"World\",\n        );\n        ui.label(\"Gizmo:\");\n    });\n\n    Ok(())\n}\n\npub fn vpeol_3d_camera_mode_selector(\n    mut ui: ResMut<YoleckPanelUi>,\n    mut query: Query<(&mut Vpeol3dCameraControl, &mut Transform)>,\n    choices: Res<YoleckCameraChoices>,\n) -> Result {\n    if let Ok((mut camera_control, mut camera_transform)) = query.single_mut() {\n        let old_mode = camera_control.mode;\n\n        let current_choice = choices\n            .choices\n            .iter()\n            .find(|c| c.control.mode == camera_control.mode);\n        let selected_text = current_choice.map(|c| c.name.as_str()).unwrap_or(\"Unknown\");\n\n        egui::ComboBox::from_id_salt(\"camera_mode_selector\")\n            .selected_text(selected_text)\n            .show_ui(&mut ui, |ui| {\n                for choice in choices.choices.iter() {\n                    ui.selectable_value(\n                        &mut camera_control.mode,\n                        choice.control.mode,\n                        &choice.name,\n                    );\n                }\n            });\n\n        if old_mode != camera_control.mode\n            && let Some(choice) = choices\n                .choices\n                .iter()\n                .find(|c| c.control.mode == camera_control.mode)\n        {\n            *camera_control = choice.control.clone();\n\n            if let Some((position, look_at, up)) = choice.initial_transform {\n                camera_transform.translation = position;\n                camera_transform.look_at(look_at, up);\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// A position component that's edited and populated by vpeol_3d.\n///\n/// Editing is done with egui, or by dragging the entity on a [`VpeolDragPlane`]  that passes\n/// through the entity.\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol3dPosition(pub Vec3);\n\n/// Add this to an entity with [`Vpeol3dPosition`] to force it to be on a specific plane while\n/// editing.\n///\n/// This is useful for 2D games that use 3D graphics but don't want to allow free positioning of\n/// entities on all axes.\n///\n/// Note that this is not a [`YoleckComponent`]. Do not add it with\n/// [`insert_on_init_during_editor`](YoleckEntityType::with). The best way to add it is by using\n/// [`insert_on_init_during_editor`](YoleckEntityType::insert_on_init_during_editor) which also\n/// allows setting the data.\n#[derive(Component)]\npub struct Vpeol3dSnapToPlane {\n    pub normal: Dir3,\n    /// Offset of the plane from the origin of axes\n    pub offset: f32,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum Vpeol3dTranslationGizmoMode {\n    World,\n    Local,\n}\n\n#[derive(Resource)]\npub struct Vpeol3dTranslationGizmoConfig {\n    pub knob_distance: f32,\n    pub knob_scale: f32,\n    pub mode: Vpeol3dTranslationGizmoMode,\n}\n\nimpl Default for Vpeol3dTranslationGizmoConfig {\n    fn default() -> Self {\n        Self {\n            knob_distance: 2.0,\n            knob_scale: 0.5,\n            mode: Vpeol3dTranslationGizmoMode::World,\n        }\n    }\n}\n\n/// A rotation component that's edited and populated by vpeol_3d.\n///\n/// Editing is done with egui using Euler angles (X, Y, Z in degrees).\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol3dRotation(pub Quat);\n\nimpl Default for Vpeol3dRotation {\n    fn default() -> Self {\n        Self(Quat::IDENTITY)\n    }\n}\n\n/// A scale component that's edited and populated by vpeol_3d.\n///\n/// Editing is done with egui using separate drag values for X, Y, Z axes.\n#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]\n#[serde(transparent)]\n#[cfg_attr(feature = \"bevy_reflect\", derive(bevy::reflect::Reflect))]\npub struct Vpeol3dScale(pub Vec3);\n\nimpl Default for Vpeol3dScale {\n    fn default() -> Self {\n        Self(Vec3::ONE)\n    }\n}\n\nenum CommonDragPlane {\n    NotDecidedYet,\n    WithNormal(Vec3),\n    NoSharedPlane,\n}\n\nimpl CommonDragPlane {\n    fn consider(&mut self, normal: Vec3) {\n        *self = match self {\n            CommonDragPlane::NotDecidedYet => CommonDragPlane::WithNormal(normal),\n            CommonDragPlane::WithNormal(current_normal) => {\n                if *current_normal == normal {\n                    CommonDragPlane::WithNormal(normal)\n                } else {\n                    CommonDragPlane::NoSharedPlane\n                }\n            }\n            CommonDragPlane::NoSharedPlane => CommonDragPlane::NoSharedPlane,\n        }\n    }\n\n    fn shared_normal(&self) -> Option<Vec3> {\n        if let CommonDragPlane::WithNormal(normal) = self {\n            Some(*normal)\n        } else {\n            None\n        }\n    }\n}\n\nfn vpeol_3d_edit_transform_group(\n    mut ui: ResMut<YoleckUi>,\n    position_edit: YoleckEdit<(\n        Entity,\n        &mut Vpeol3dPosition,\n        Option<&VpeolDragPlane>,\n        Option<&Vpeol3dSnapToPlane>,\n    )>,\n    rotation_edit: YoleckEdit<&mut Vpeol3dRotation>,\n    scale_edit: YoleckEdit<&mut Vpeol3dScale>,\n    global_drag_plane: Res<VpeolDragPlane>,\n    passed_data: Res<YoleckPassedData>,\n) {\n    let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();\n    if !has_any {\n        return;\n    }\n\n    ui.group(|ui| {\n        ui.label(egui::RichText::new(\"Transform\").strong());\n        ui.separator();\n\n        vpeol_3d_edit_position_impl(ui, position_edit, &global_drag_plane, &passed_data);\n        vpeol_3d_edit_rotation_impl(ui, rotation_edit);\n        vpeol_3d_edit_scale_impl(ui, scale_edit);\n    });\n}\n\nfn vpeol_3d_edit_position_impl(\n    ui: &mut egui::Ui,\n    mut edit: YoleckEdit<(\n        Entity,\n        &mut Vpeol3dPosition,\n        Option<&VpeolDragPlane>,\n        Option<&Vpeol3dSnapToPlane>,\n    )>,\n    global_drag_plane: &VpeolDragPlane,\n    passed_data: &YoleckPassedData,\n) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n    let mut average = DVec3::ZERO;\n    let mut num_entities = 0;\n    let mut transition = Vec3::ZERO;\n\n    let mut common_drag_plane = CommonDragPlane::NotDecidedYet;\n\n    for (entity, position, drag_plane, _) in edit.iter_matching() {\n        let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane);\n        common_drag_plane.consider(*drag_plane.normal);\n\n        if let Some(pos) = passed_data.get::<Vec3>(entity) {\n            transition = *pos - position.0;\n        }\n        average += position.0.as_dvec3();\n        num_entities += 1;\n    }\n    average /= num_entities as f64;\n\n    if common_drag_plane.shared_normal().is_none() {\n        transition = Vec3::ZERO;\n        ui.label(\n            egui::RichText::new(\"Drag plane differs - cannot drag together\")\n                .color(egui::Color32::RED),\n        );\n    }\n    ui.horizontal(|ui| {\n        let mut new_average = average;\n\n        ui.add(egui::Label::new(\"Position\"));\n        ui.add(egui::DragValue::new(&mut new_average.x).prefix(\"X:\"));\n        ui.add(egui::DragValue::new(&mut new_average.y).prefix(\"Y:\"));\n        ui.add(egui::DragValue::new(&mut new_average.z).prefix(\"Z:\"));\n\n        transition += (new_average - average).as_vec3();\n    });\n\n    if transition.is_finite() && transition != Vec3::ZERO {\n        for (_, mut position, _, snap) in edit.iter_matching_mut() {\n            position.0 += transition;\n            if let Some(snap) = snap {\n                let displacement = position.0.project_onto(*snap.normal);\n                position.0 += snap.offset * snap.normal - displacement;\n            }\n        }\n    }\n}\n\nfn vpeol_3d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol3dRotation>) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n\n    let mut average_euler = Vec3::ZERO;\n    let mut num_entities = 0;\n\n    for rotation in edit.iter_matching() {\n        let (x, y, z) = rotation.0.to_euler(EulerRot::XYZ);\n        average_euler += Vec3::new(x, y, z);\n        num_entities += 1;\n    }\n    average_euler /= num_entities as f32;\n\n    ui.horizontal(|ui| {\n        let mut new_euler = average_euler;\n        let mut x_deg = new_euler.x.to_degrees();\n        let mut y_deg = new_euler.y.to_degrees();\n        let mut z_deg = new_euler.z.to_degrees();\n\n        ui.add(egui::Label::new(\"Rotation\"));\n        ui.add(\n            egui::DragValue::new(&mut x_deg)\n                .prefix(\"X:\")\n                .speed(1.0)\n                .suffix(\"°\"),\n        );\n        ui.add(\n            egui::DragValue::new(&mut y_deg)\n                .prefix(\"Y:\")\n                .speed(1.0)\n                .suffix(\"°\"),\n        );\n        ui.add(\n            egui::DragValue::new(&mut z_deg)\n                .prefix(\"Z:\")\n                .speed(1.0)\n                .suffix(\"°\"),\n        );\n\n        new_euler.x = x_deg.to_radians();\n        new_euler.y = y_deg.to_radians();\n        new_euler.z = z_deg.to_radians();\n\n        let transition = new_euler - average_euler;\n\n        if transition.is_finite() && transition != Vec3::ZERO {\n            for mut rotation in edit.iter_matching_mut() {\n                let (x, y, z) = rotation.0.to_euler(EulerRot::XYZ);\n                rotation.0 = Quat::from_euler(\n                    EulerRot::XYZ,\n                    x + transition.x,\n                    y + transition.y,\n                    z + transition.z,\n                )\n            }\n        }\n    });\n}\n\nfn vpeol_3d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol3dScale>) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n    let mut average = DVec3::ZERO;\n    let mut num_entities = 0;\n\n    for scale in edit.iter_matching() {\n        average += scale.0.as_dvec3();\n        num_entities += 1;\n    }\n    average /= num_entities as f64;\n\n    ui.horizontal(|ui| {\n        let mut new_average = average;\n\n        ui.add(egui::Label::new(\"Scale\"));\n        ui.vertical(|ui| {\n            ui.centered_and_justified(|ui| {\n                let axis_average = (average.x + average.y + average.z) / 3.0;\n                let mut new_axis_average = axis_average;\n                if ui\n                    .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))\n                    .dragged()\n                {\n                    // Use difference instead of ration to avoid problems when reaching/crossing\n                    // the zero.\n                    let diff = new_axis_average - axis_average;\n                    new_average.x += diff;\n                    new_average.y += diff;\n                    new_average.z += diff;\n                }\n            });\n            ui.horizontal(|ui| {\n                ui.add(\n                    egui::DragValue::new(&mut new_average.x)\n                        .prefix(\"X:\")\n                        .speed(0.01),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut new_average.y)\n                        .prefix(\"Y:\")\n                        .speed(0.01),\n                );\n                ui.add(\n                    egui::DragValue::new(&mut new_average.z)\n                        .prefix(\"Z:\")\n                        .speed(0.01),\n                );\n            });\n        });\n\n        let transition = (new_average - average).as_vec3();\n\n        if transition.is_finite() && transition != Vec3::ZERO {\n            for mut scale in edit.iter_matching_mut() {\n                scale.0 += transition;\n            }\n        }\n    });\n}\n\nfn vpeol_3d_init_position(\n    mut egui_context: EguiContexts,\n    ui: Res<YoleckUi>,\n    mut edit: YoleckEdit<(&mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,\n    global_drag_plane: Res<VpeolDragPlane>,\n    cameras_query: Query<&VpeolCameraState>,\n    mouse_buttons: Res<ButtonInput<MouseButton>>,\n) -> YoleckExclusiveSystemDirective {\n    let Ok((mut position, drag_plane)) = edit.single_mut() else {\n        return YoleckExclusiveSystemDirective::Finished;\n    };\n\n    let Some(cursor_ray) = cameras_query\n        .iter()\n        .find_map(|camera_state| camera_state.cursor_ray)\n    else {\n        return YoleckExclusiveSystemDirective::Listening;\n    };\n\n    let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());\n    if let Some(distance_to_plane) =\n        cursor_ray.intersect_plane(position.0, InfinitePlane3d::new(*drag_plane.normal))\n    {\n        position.0 = cursor_ray.get_point(distance_to_plane);\n    };\n\n    if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {\n        return YoleckExclusiveSystemDirective::Listening;\n    }\n\n    if mouse_buttons.just_released(MouseButton::Left) {\n        return YoleckExclusiveSystemDirective::Finished;\n    }\n\n    YoleckExclusiveSystemDirective::Listening\n}\n\n#[derive(Clone, Copy)]\nstruct AxisKnobData {\n    axis: Vec3,\n    drag_plane_normal: Dir3,\n}\n\n#[allow(clippy::type_complexity, clippy::too_many_arguments)]\nfn vpeol_3d_edit_axis_knobs(\n    mut edit: YoleckEdit<(\n        Entity,\n        &GlobalTransform,\n        &Vpeol3dPosition,\n        Option<&Vpeol3dRotation>,\n    )>,\n    translation_gizmo_config: Res<Vpeol3dTranslationGizmoConfig>,\n    mut knobs: YoleckKnobs,\n    mut mesh_assets: ResMut<Assets<Mesh>>,\n    mut material_assets: ResMut<Assets<StandardMaterial>>,\n    mut cached_assets: Local<\n        Option<(\n            Handle<Mesh>,\n            Handle<Mesh>,\n            [Handle<StandardMaterial>; 3],\n            [Handle<StandardMaterial>; 3],\n        )>,\n    >,\n    mut directives_writer: MessageWriter<YoleckDirective>,\n    cameras_query: Query<(&GlobalTransform, &VpeolCameraState)>,\n) {\n    if edit.is_empty() || edit.has_nonmatching() {\n        return;\n    }\n\n    let (camera_position, dragged_entity) = cameras_query\n        .iter()\n        .next()\n        .map(|(t, state)| {\n            let dragged = match &state.clicks_on_objects_state {\n                VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(*entity),\n                _ => None,\n            };\n            (t.translation(), dragged)\n        })\n        .unwrap_or((Vec3::ZERO, None));\n\n    let (cone_mesh, line_mesh, materials, materials_active) =\n        cached_assets.get_or_insert_with(|| {\n            (\n                mesh_assets.add(Mesh::from(Cone {\n                    radius: 0.5,\n                    height: 1.0,\n                })),\n                mesh_assets.add(Mesh::from(Cylinder {\n                    radius: 0.15,\n                    half_height: 0.5,\n                })),\n                [\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::from(css::RED),\n                        unlit: true,\n                        ..default()\n                    }),\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::from(css::GREEN),\n                        unlit: true,\n                        ..default()\n                    }),\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::from(css::BLUE),\n                        unlit: true,\n                        ..default()\n                    }),\n                ],\n                [\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::linear_rgb(1.0, 0.5, 0.5),\n                        unlit: true,\n                        ..default()\n                    }),\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::linear_rgb(0.5, 1.0, 0.5),\n                        unlit: true,\n                        ..default()\n                    }),\n                    material_assets.add(StandardMaterial {\n                        base_color: Color::linear_rgb(0.5, 0.5, 1.0),\n                        unlit: true,\n                        ..default()\n                    }),\n                ],\n            )\n        });\n\n    let world_axes = [\n        AxisKnobData {\n            axis: Vec3::X,\n            drag_plane_normal: Dir3::Z,\n        },\n        AxisKnobData {\n            axis: Vec3::Y,\n            drag_plane_normal: Dir3::X,\n        },\n        AxisKnobData {\n            axis: Vec3::Z,\n            drag_plane_normal: Dir3::Y,\n        },\n    ];\n\n    for (entity, global_transform, _, rotation) in edit.iter_matching() {\n        let entity_position = global_transform.translation();\n        let entity_scale = global_transform.to_scale_rotation_translation().0;\n        let entity_radius = entity_scale.max_element();\n\n        let distance_to_camera = (camera_position - entity_position).length();\n        let distance_scale = (distance_to_camera / 40.0).max(1.0);\n\n        let axes = match translation_gizmo_config.mode {\n            Vpeol3dTranslationGizmoMode::World => world_axes,\n            Vpeol3dTranslationGizmoMode::Local => {\n                let rot = if let Some(Vpeol3dRotation(euler_angles)) = rotation {\n                    Quat::from_euler(\n                        EulerRot::XYZ,\n                        euler_angles.x,\n                        euler_angles.y,\n                        euler_angles.z,\n                    )\n                } else {\n                    Quat::IDENTITY\n                };\n\n                let local_x = (rot * Vec3::X).normalize();\n                let local_y = (rot * Vec3::Y).normalize();\n                let local_z = (rot * Vec3::Z).normalize();\n\n                [\n                    AxisKnobData {\n                        axis: local_x,\n                        drag_plane_normal: Dir3::new_unchecked(local_z),\n                    },\n                    AxisKnobData {\n                        axis: local_y,\n                        drag_plane_normal: Dir3::new_unchecked(local_x),\n                    },\n                    AxisKnobData {\n                        axis: local_z,\n                        drag_plane_normal: Dir3::new_unchecked(local_y),\n                    },\n                ]\n            }\n        };\n\n        for (axis_idx, axis_data) in axes.iter().enumerate() {\n            let knob_name = match axis_idx {\n                0 => \"vpeol-3d-axis-knob-x\",\n                1 => \"vpeol-3d-axis-knob-y\",\n                _ => \"vpeol-3d-axis-knob-z\",\n            };\n\n            let line_name = match axis_idx {\n                0 => \"vpeol-3d-axis-line-x\",\n                1 => \"vpeol-3d-axis-line-y\",\n                _ => \"vpeol-3d-axis-line-z\",\n            };\n\n            let scaled_knob_scale = translation_gizmo_config.knob_scale * distance_scale;\n            let base_distance = translation_gizmo_config.knob_distance + entity_radius;\n            let scaled_distance = base_distance * (1.0 + (distance_scale - 1.0) * 0.3);\n\n            let knob_offset = scaled_distance * axis_data.axis;\n            let knob_position = entity_position + knob_offset;\n            let knob_transform = Transform {\n                translation: knob_position,\n                rotation: Quat::from_rotation_arc(Vec3::Y, axis_data.axis),\n                scale: scaled_knob_scale * Vec3::ONE,\n            };\n\n            let line_length = scaled_distance - scaled_knob_scale * 0.5;\n            let line_center = entity_position + axis_data.axis * line_length * 0.5;\n            let line_transform = Transform {\n                translation: line_center,\n                rotation: Quat::from_rotation_arc(Vec3::Y, axis_data.axis),\n                scale: Vec3::new(scaled_knob_scale, line_length, scaled_knob_scale),\n            };\n\n            let line_knob = knobs.knob((entity, line_name));\n            let line_knob_id = line_knob.cmd.id();\n            drop(line_knob);\n\n            let knob = knobs.knob((entity, knob_name));\n            let knob_id = knob.cmd.id();\n            let passed_pos = knob.get_passed_data::<Vec3>().copied();\n            drop(knob);\n\n            let is_active = dragged_entity == Some(line_knob_id) || dragged_entity == Some(knob_id);\n\n            let material = if is_active {\n                &materials_active[axis_idx]\n            } else {\n                &materials[axis_idx]\n            };\n\n            let mut line_knob = knobs.knob((entity, line_name));\n            line_knob.cmd.insert((\n                Mesh3d(line_mesh.clone()),\n                MeshMaterial3d(material.clone()),\n                line_transform,\n                GlobalTransform::from(line_transform),\n            ));\n\n            let mut knob = knobs.knob((entity, knob_name));\n            knob.cmd.insert(VpeolDragPlane(InfinitePlane3d {\n                normal: axis_data.drag_plane_normal,\n            }));\n            knob.cmd.insert((\n                Mesh3d(cone_mesh.clone()),\n                MeshMaterial3d(material.clone()),\n                knob_transform,\n                GlobalTransform::from(knob_transform),\n            ));\n\n            if let Some(pos) = passed_pos {\n                let vector_from_entity = pos - knob_offset - entity_position;\n                let along_axis = vector_from_entity.dot(axis_data.axis);\n                let new_position = entity_position + along_axis * axis_data.axis;\n                directives_writer.write(YoleckDirective::pass_to_entity(entity, new_position));\n            }\n        }\n    }\n}\n\nfn vpeol_3d_populate_transform(\n    mut populate: YoleckPopulate<(\n        &Vpeol3dPosition,\n        Option<&Vpeol3dRotation>,\n        Option<&Vpeol3dScale>,\n        &YoleckBelongsToLevel,\n    )>,\n    levels_query: Query<&VpeolRepositionLevel>,\n) {\n    populate.populate(\n        |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {\n            let mut transform = Transform::from_translation(position.0);\n            if let Some(Vpeol3dRotation(quat)) = rotation {\n                transform = transform.with_rotation(*quat);\n            }\n            if let Some(Vpeol3dScale(scale)) = scale {\n                transform = transform.with_scale(*scale);\n            }\n\n            if let Ok(VpeolRepositionLevel(level_transform)) =\n                levels_query.get(belongs_to_level.level)\n            {\n                transform = *level_transform * transform;\n            }\n\n            cmd.insert((transform, GlobalTransform::from(transform)));\n        },\n    )\n}\n"
  },
  {
    "path": "tests/upgrade_level_file.rs",
    "content": "use bevy_yoleck::level_files_upgrading::upgrade_level_file;\n\n#[test]\nfn test_upgrade_v1_to_v2() {\n    let orig_level = serde_json::json!([\n        {\n            \"format_version\": 1,\n        },\n        {},\n        [\n            [\n                {\n                    \"type\": \"Foo\",\n                    \"name\": \"\",\n                },\n                {\n                    \"foo\": 42,\n                },\n            ]\n        ],\n    ]);\n    let upgraded_level = upgrade_level_file(orig_level).unwrap();\n    assert_eq!(\n        upgraded_level,\n        serde_json::json!([\n            {\n                \"format_version\": 2,\n                \"app_format_version\": 0,\n            },\n            {},\n            [\n                [\n                    {\n                        \"type\": \"Foo\",\n                        \"name\": \"\",\n                    },\n                    {\n                        \"Foo\": {\n                            \"foo\": 42,\n                        },\n                    },\n                ]\n            ],\n        ])\n    );\n}\n"
  }
]