Compare commits
No commits in common. "3931de1f0a241c9acb90baae006364962ed9f8c0" and "0553a85220a25bdb31b8b6737ba698b3c2a9d6c5" have entirely different histories.
3931de1f0a
...
0553a85220
@ -7,6 +7,4 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
[alias]
|
||||
clean-target = "run -p xtask --bin clean-target --"
|
||||
validate-levels = "run -p xtask --features validate-levels --bin validate-levels --"
|
||||
bake-navigation = "run -p xtask --features validate-levels --bin bake-navigation --"
|
||||
package-project = "run -p xtask --features validate-levels --bin package-project --"
|
||||
upgrade-project = "run -p xtask --features validate-levels --bin upgrade-project --"
|
||||
|
||||
@ -21,23 +21,9 @@ performance, soak, regression, and first-hour workflows must pass against a repr
|
||||
`#43`; M6 implementation is complete.
|
||||
4. Completed: authoritative content validation `#45` and the validation-gated packaging/Build
|
||||
workflow `#44`.
|
||||
5. Completed: audio `#47`, animation `#46`, and navigation `#48` passed production acceptance.
|
||||
Source-control safety `#49` has complete implementation and source/headless acceptance; live
|
||||
visual acceptance remains before the readiness gate `#50`.
|
||||
|
||||
## Implemented Slice - Collaborative Authored-File Safety
|
||||
|
||||
- Exact BLAKE3 baselines for scene tabs, staged Material/Material Instance documents, and Project
|
||||
Settings.
|
||||
- One guarded atomic publication path for scene save/export, prefab source Apply/history, and
|
||||
editable materials, including missing-file and immediate pre-rename race protection.
|
||||
- Non-destructive background Git porcelain scanning with optional off-thread ownership providers;
|
||||
Git/provider absence is quiet.
|
||||
- Compact active-scene and selected-asset state plus Reload, metadata comparison, Save As, and
|
||||
Cancel conflict recovery. No force-overwrite action exists.
|
||||
- Focused race, read-only, provider-lock, parser, non-repository, modal-render, scene, and material
|
||||
tests pass. Live debug-editor visual acceptance remains pending for a desktop-capable QA session;
|
||||
packaged acceptance remains owner-deferred.
|
||||
5. Completed: audio source/listener/bus authoring and spatial preview passed production acceptance
|
||||
in `#47`. Next: animation `#46`, navigation `#48`, source-control safety `#49`, then readiness
|
||||
gate `#50`.
|
||||
|
||||
## M6 - Reliability, Recovery, And Project Workflow
|
||||
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
# Material Library And Targeted Viewport Drop
|
||||
|
||||
Working implementation plan for Gitea issues
|
||||
[`#16`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/16) and
|
||||
[`#18`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/18).
|
||||
The shared Material/Material Instance and stable renderer-slot foundation from `#51` is already
|
||||
present; this slice completes the production-facing catalog and drag/drop workflow.
|
||||
|
||||
## Status
|
||||
|
||||
Source audit complete. Implementation starts on a clean branch after collaborative-safety `#49` is
|
||||
published, because both slices touch the Asset Browser and should remain independently reviewable.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Material assets are edited inside generic Asset Browser Details; no dedicated Material Library or
|
||||
scene-usage view exists.
|
||||
- Viewport material/texture drops act on the existing selection instead of the surface under the
|
||||
pointer.
|
||||
- Multi-slot renderers silently assign all slots; the hydrated draw hit already carries
|
||||
`HydratedRendererMaterialBinding`, but the drop path does not use it.
|
||||
- Primitive and brush-face targeting is not explicit, and unsupported authored actors can receive a
|
||||
generic `MaterialDesc` instead of an actionable rejection.
|
||||
- Drag UI describes an eventual action but does not preview the actual affected surface or restore a
|
||||
preview on cancel/target change.
|
||||
|
||||
## Material Library
|
||||
|
||||
1. Add a dockable `Material Library` panel, opened from Window and placed in the existing bottom dock
|
||||
without adding another permanent toolbar.
|
||||
2. Present project Materials and Material Instances with search, Material/Instance filter, thumbnail
|
||||
grid/list modes, base/dependency health, and source-control status.
|
||||
3. Add a scene-usage section that counts renderer-slot, primitive, and brush-face references and can
|
||||
select/locate users. Do not revive ambiguous hidden scene-local material clones.
|
||||
4. Reuse the existing guarded Material/Instance drafts for Apply, Revert, Create Instance, and
|
||||
texture-parameter editing. Shared-file edits remain explicit; scene assignment changes use
|
||||
history.
|
||||
5. Material and Instance cells are first-class drag sources and preserve the current pointer-following
|
||||
visual identity.
|
||||
|
||||
## Targeted Drop Contract
|
||||
|
||||
Introduce a frame-updated `ViewportAssetDropTarget` resolved by the existing mesh-picking path:
|
||||
|
||||
- A hydrated static/skinned draw maps through `HydratedRendererMaterialBinding` to the authored actor
|
||||
and exact stable slot ID.
|
||||
- A primitive maps to its authored actor-level `MaterialDesc`.
|
||||
- A brush performs authored face intersection and identifies the stable face ID.
|
||||
- An authored but unsupported target remains a visible invalid target with a specific reason.
|
||||
- Empty space remains placement-only for placeable assets and invalid for Material/Texture payloads.
|
||||
|
||||
Payload matrix:
|
||||
|
||||
| Payload | Renderer slot | Primitive | Brush face |
|
||||
|---------|---------------|-----------|------------|
|
||||
| Material / Material Instance | Assign exact hit slot; one-slot actors may assign directly | Replace actor material reference/values | Replace the hit face material reference |
|
||||
| Texture | Reject with guidance to create/edit a Material Instance | Set base-color texture | Set hit-face base-color texture/material binding |
|
||||
|
||||
An explicit `Apply All Slots` command remains available from the renderer inspector/library usage
|
||||
menu; a viewport hit never silently broadens from one slot to all slots.
|
||||
|
||||
## Preview, Commit, And Cancel
|
||||
|
||||
1. Start a persistent drop-preview session when a supported Material/Texture payload enters the
|
||||
viewport. Snapshot only the target's affected authored state.
|
||||
2. Apply a transient visual preview without pushing history or marking the scene dirty.
|
||||
3. When the pointer changes target, restore the previous snapshot before previewing the new target.
|
||||
4. On release, restore the transient state first, then commit exactly one typed history/operator
|
||||
transaction to the identified slot, primitive, or face.
|
||||
5. On Escape, drag cancellation, leaving the viewport, invalid target, or missing source, restore the
|
||||
exact snapshot and remove every preview/helper marker.
|
||||
6. The drag card and target outline name the payload, actor, slot/face, action, and invalid reason.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit-test ray-hit to authored target/slot mapping, primitive/face targeting, and invalid reasons.
|
||||
- Use `OperatorInvariantHarness` for preview target changes, release commit, Escape/outside cancel,
|
||||
dirty-state preservation, helper cleanup, and one-step undo/redo.
|
||||
- Cover static one/multi-slot, skinned multi-slot, primitive, brush face, Texture rejection on a
|
||||
renderer slot, missing material, and linked/locked prefab boundaries.
|
||||
- Verify Material Library filtering, scene-usage counts, dependency diagnostics, and drag sources in
|
||||
headless egui tests.
|
||||
- Run full source/headless checks. Packaged testing remains deferred until the project owner requests
|
||||
it again; live visual acceptance is still required before closing `#16`/`#18`.
|
||||
@ -1,73 +0,0 @@
|
||||
# Navigation Mesh Authoring, Bake Diagnostics, And Path Preview
|
||||
|
||||
Working implementation plan for Gitea issue
|
||||
[`#48`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/48).
|
||||
This is the navigation production loop required by the M7 content-production milestone.
|
||||
|
||||
## Status
|
||||
|
||||
Architecture, dependency compatibility, composed-source resolution, persisted path samples,
|
||||
corrective source-level QA, and live debug-editor acceptance are complete. Packaged/release
|
||||
acceptance remains explicitly deferred until the project owner requests another packaged pass.
|
||||
|
||||
## Outcome
|
||||
|
||||
A level designer can author navigation bounds, obstacles, areas, links, and agent profiles; bake a
|
||||
deterministic versioned artifact; inspect stale or invalid data; preview a path in the viewport; and
|
||||
use the same artifact through a game-owned runtime query API and headless release validation.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Shared reflected components own bounds, obstacles, area volumes, links, and scene bake settings.
|
||||
- The scene crate owns deterministic composed-source resolution, source fingerprinting, bake
|
||||
artifact IO, the engine-independent query core, and validation.
|
||||
- Rerecast performs the 3D walkable-surface bake without coupling navigation to a Bevy plugin
|
||||
release. Polyanya performs proven any-angle runtime and editor-preview path queries.
|
||||
- Overlapping primitive and additive-brush triangles enter the deterministic bake source; distant
|
||||
geometry and navigation authoring are excluded from each bounds fingerprint. No synthetic bounds
|
||||
floor is added.
|
||||
- The game crate adapts the shared query core to Bevy types and exposes renderer-free artifact
|
||||
validation. Editor preview and project validation do not implement separate pathfinders.
|
||||
- Generated artifacts live under `assets/navigation/generated/`, are project-relative runtime
|
||||
dependencies, and never serialize viewport helpers into authored scenes.
|
||||
|
||||
## Implementation Sequence
|
||||
|
||||
1. Add reflected authoring schema, versioned bake artifact, source fingerprint, validation, and
|
||||
exact agent-profile settings.
|
||||
2. Add deterministic Rerecast bake input extraction for authored bounds/obstacles and headless bake
|
||||
command coverage.
|
||||
3. Add game-owned artifact loading and Polyanya path-query API, including authored off-mesh links.
|
||||
4. Add typed inspectors, create commands, bake/stale status, viewport overlay, and start/end path
|
||||
preview.
|
||||
5. Add owner-attributed project validation for invalid links, missing/stale artifacts, unreachable
|
||||
preview samples, and bake failures.
|
||||
6. Commit a small sample scene/artifact and run automated, headless, and live debug-editor
|
||||
acceptance. PIE and packaged runtime acceptance remain deferred by project-owner direction.
|
||||
|
||||
## Acceptance Gates
|
||||
|
||||
- A designer can create bounds and obstacles, bake, view the resulting mesh, and preview a valid
|
||||
start-to-end path.
|
||||
- Agent radius, height, climb, and slope settings deterministically affect the bake fingerprint.
|
||||
- Relevant geometry or navigation-authoring changes mark the artifact stale; unrelated scene
|
||||
metadata does not.
|
||||
- Invalid or dangling links, samples outside the mesh, isolated regions, and bake failures identify
|
||||
the owning actor or setting and provide a repair action.
|
||||
- Runtime path queries and editor preview use the same game-owned API and versioned artifact.
|
||||
- Helper meshes, lines, endpoints, and bake state are transient and never serialize.
|
||||
- Headless bake and project validation pass for the committed sample fixture, and package dependency
|
||||
collection includes the current artifact.
|
||||
- Visible subscenes and nested prefab contributors resolve identically from editor snapshots and
|
||||
headless tooling; unsupported structural overrides block with an apply/unpack repair action.
|
||||
|
||||
## Deliberate Boundaries
|
||||
|
||||
- V1 supports static baked navigation plus explicit links. Dynamic crowd avoidance and runtime tile
|
||||
carving are separate work.
|
||||
- Full partial-tile regeneration waits for upstream Rerecast tiling support. V1 records affected
|
||||
source bounds and debounces a deterministic full bake while reporting the dirty region.
|
||||
- Area costs are authored and validated in V1; path-cost weighting beyond walkable/blocked areas is
|
||||
deferred until the runtime exposes per-polygon cost callbacks.
|
||||
- Imported static meshes remain obstacle-authored in V1 because their normalized manifests do not
|
||||
yet expose deterministic collision triangles to the headless scene crate.
|
||||
@ -1,37 +0,0 @@
|
||||
# Production-Readiness Acceptance Gate
|
||||
|
||||
Working plan for Gitea issue
|
||||
[`#50`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/50).
|
||||
This gate is broader than the M7 feature list: it proves the complete daily authoring, content,
|
||||
recovery, performance, and release workflow against one exact release-candidate commit.
|
||||
|
||||
## Status
|
||||
|
||||
Audit in progress. The versioned acceptance matrix exists, but no release candidate is nominated and
|
||||
the gate is not signed off. Packaged/release testing remains explicitly deferred until the project
|
||||
owner requests it again.
|
||||
|
||||
## Sequence
|
||||
|
||||
1. Keep one evidence matrix under `docs/editor/evaluations/production-readiness/`; historical H1-H6
|
||||
notes remain context only.
|
||||
2. Close implementation blockers before nominating a candidate: collaborative safety `#49`, the
|
||||
remaining Material/drag-drop contract in `#16`, `#18`, and `#51`, terrain `#22`-`#24`, physics
|
||||
placement/diagnostics `#25`-`#26`, and their regression fixtures.
|
||||
3. Complete the representative regression project, mutation-invariant coverage, performance budgets,
|
||||
and first-hour workflow tracked by `#32`-`#36`.
|
||||
4. Nominate one exact commit, validate it from a clean checkout, and record source/headless results.
|
||||
5. When packaged testing is re-enabled, run the candidate's package/build and packaged-runtime
|
||||
matrix without substituting older artifacts.
|
||||
6. Run the eight-hour soak and measured performance protocol, consolidate limitations by severity
|
||||
and workaround, and obtain independent first-hour/recovery sign-off.
|
||||
7. Post the signed Gitea milestone comment linking the exact commit and immutable evidence. Close
|
||||
`#50` only when every required row passes and no P0 blocker remains.
|
||||
|
||||
## Gate Rules
|
||||
|
||||
- Feature presence is not acceptance evidence.
|
||||
- A local dirty worktree is not a release candidate.
|
||||
- Results from another commit or an old package do not transfer to the candidate.
|
||||
- Deferred, missing, partial, and implementer-only evidence are not passes.
|
||||
- Every failure records an owner ticket or a concrete rerun condition.
|
||||
@ -1,43 +0,0 @@
|
||||
# Renderer, material, and component foundation
|
||||
|
||||
## Goal
|
||||
|
||||
Separate static and skinned renderer ownership, restore deterministic skinned edit poses, add shared
|
||||
material/material-instance slots, and make authoring components registry-driven and composable.
|
||||
|
||||
## Component foundation milestone
|
||||
|
||||
- Give authoring components immutable IDs distinct from Rust type paths.
|
||||
- Split persisted active state from inspector ordering with legacy-state migration.
|
||||
- Route presence, persistence, add/remove/reset/copy/paste, and component history through Bevy
|
||||
reflection and atomic transactions.
|
||||
- Treat `ActorKind` as a derived compatibility/display hint; validate component data and explicit
|
||||
requirements/conflicts.
|
||||
- Provide one statically linked extension registration helper and reflection round-trip fixture.
|
||||
- Verify registry integrity, transaction undo/redo, scene save inclusion, composition, and legacy
|
||||
enable-state preservation.
|
||||
|
||||
## Related renderer/material milestone
|
||||
|
||||
- Keep `StaticMeshRenderer` and `SkinnedMeshRenderer` as separate authoring and hydration paths.
|
||||
- Give both renderer types stable imported-source/explicit Material slots and preserve orphaned
|
||||
assignments by ID across source changes.
|
||||
- Add versioned Material and direct-base Material Instance assets, sparse editor overrides, shared
|
||||
handle live refresh, and runtime-only property blocks.
|
||||
- Capture and restore imported Transform/MorphWeights baselines; sample only the explicitly chosen
|
||||
default edit clip and never reload a rig for a material-only change.
|
||||
- Compose one validated Surface evaluator into raster and Solari, including normal/emissive/unlit
|
||||
fields and exact cutout candidate acceptance. Safely omit skinned/morph geometry from Solari until
|
||||
instance-owned deformed vertices and dynamic BLAS updates exist.
|
||||
- Upgrade scenes/materials explicitly to schema v4 with dry-run, staged writes, backup, rollback,
|
||||
stable slot normalization, and active-state separation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Shared, scene, game, game-hot, editor, Surface, and maintained Solari-fork tests pass.
|
||||
- `cargo --locked validate-levels` reports zero blocking findings.
|
||||
- A second project-upgrade dry run reports no changes after apply.
|
||||
- Editor evidence shows the stable component cards, static/skinned material slots, Material Instance
|
||||
workflow, deterministic animated actor pose, and Surface/Solari diagnostics.
|
||||
- Gitea implementation issue and related roadmap issues link the documentation and screenshot
|
||||
evidence without claiming unsupported dynamic deformed BLAS behavior.
|
||||
@ -1,58 +0,0 @@
|
||||
# Source-Control Status And Collaborative File Safety
|
||||
|
||||
Working implementation plan for Gitea issue
|
||||
[`#49`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/49).
|
||||
This is the final M7 implementation slice before the production-readiness acceptance gate.
|
||||
|
||||
## Status
|
||||
|
||||
Implementation, focused source tests, full editor tests, strict editor clippy, and headless checks
|
||||
are complete. Live debug-editor visual acceptance remains pending for a desktop-capable QA session.
|
||||
Packaged/release acceptance is explicitly deferred until the project owner requests another
|
||||
packaged pass.
|
||||
|
||||
## Outcome
|
||||
|
||||
External edits cannot be silently overwritten by the editor, common Git/read-only/ownership states
|
||||
are visible without dominating the shell, and projects without Git or an ownership provider behave
|
||||
normally.
|
||||
|
||||
## Implementation Sequence
|
||||
|
||||
1. Add exact file revisions, comparison metadata, guarded atomic replacement, and deterministic
|
||||
pre-rename race coverage.
|
||||
2. Capture baselines for scene tabs, material/material-instance drafts, and Project Settings;
|
||||
protect prefab source Apply and exact-file undo/redo with the same guard.
|
||||
3. Add a non-destructive asynchronous Git scanner and optional ownership-provider registry.
|
||||
4. Add compact active-scene and selected-asset status plus a recovery modal with reload, compare
|
||||
metadata, save-as, and cancel.
|
||||
5. Cover missing/present/read-only/external-change races, porcelain parsing, provider absence, and
|
||||
document-specific conflict recovery with focused source tests.
|
||||
6. Run format, strict affected-crate clippy, source tests, headless validation, and live debug-editor
|
||||
QA. Do not run packaged tests until requested by the project owner.
|
||||
|
||||
## Acceptance Gates
|
||||
|
||||
- A loaded authored file changed by another process is never replaced by Save, Apply to source, or
|
||||
prefab source history.
|
||||
- File creation is also conditional: a file that appears after a save-as baseline is captured is
|
||||
not overwritten.
|
||||
- Conflict and read-only UI identifies the path and recovery choices without a force-overwrite
|
||||
escape hatch.
|
||||
- Active-scene and selected-asset Git status remain compact and explain themselves on hover.
|
||||
- Git absence, a non-repository project, and an empty provider registry are quiet normal states.
|
||||
- Git commands are read-only and never alter index, worktree, commits, branches, or locks.
|
||||
- Provider state can block an authored write without coupling core editor code to a vendor.
|
||||
- Source tests simulate a writer changing the target immediately before rename and prove the old or
|
||||
external content survives.
|
||||
|
||||
## Deliberate Boundaries
|
||||
|
||||
- Generated registries, imports, thumbnails, navigation bakes, recovery snapshots, and packages are
|
||||
regenerative/system-owned and do not open authored-file recovery UI.
|
||||
- V1 compares metadata rather than rendering a text diff; external diff-tool integration is future
|
||||
work.
|
||||
- V1 observes ownership/locks. Acquiring, releasing, or stealing locks belongs in a provider-specific
|
||||
follow-up.
|
||||
- Git status is project-local and advisory. The content revision guard remains authoritative even
|
||||
when Git is absent or stale.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,7 +14,6 @@
|
||||
# Editor/runtime generated session state
|
||||
/assets/levels/.pie_session.scn.ron
|
||||
/assets/.trash/
|
||||
/.blacksite/backups/
|
||||
|
||||
# Backup, temporary, and log files
|
||||
**/*.rs.bk
|
||||
|
||||
709
Cargo.lock
generated
709
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/blacksite_surface",
|
||||
"crates/game",
|
||||
"crates/game_hot",
|
||||
"crates/shared",
|
||||
@ -37,7 +36,6 @@ bevy = { version = "0.19", features = [
|
||||
] }
|
||||
bevy_core_pipeline = "0.19"
|
||||
bevy_solari = "0.19"
|
||||
blacksite_surface = { path = "crates/blacksite_surface" }
|
||||
bevy_ufbx = "0.18.1-rc.1"
|
||||
bevy_egui = "0.40"
|
||||
bevy-inspector-egui = "0.37"
|
||||
@ -45,9 +43,6 @@ egui_dock = { version = "0.19.1", features = ["serde"] }
|
||||
egui_phosphor_icons = { version = "0.3.1", default-features = false }
|
||||
transform-gizmo-bevy = "0.9"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
nav_glam = { package = "glam", version = "=0.30.10", features = ["serde"] }
|
||||
polyanya = { version = "=0.16.1", default-features = false, features = ["recast", "serde"] }
|
||||
rerecast = { version = "=0.3.2", default-features = false, features = ["std", "serialize"] }
|
||||
shared = { path = "crates/shared" }
|
||||
game = { path = "crates/game" }
|
||||
game_hot = { path = "crates/game_hot" }
|
||||
@ -57,8 +52,6 @@ settings = { path = "crates/settings" }
|
||||
scene = { path = "crates/scene" }
|
||||
|
||||
[patch.crates-io]
|
||||
# Blacksite Surface ABI and deformation-safety integration for Solari.
|
||||
bevy_solari = { path = "third_party/bevy_solari" }
|
||||
# Local Bevy 0.19 compatibility patch until upstream publishes a matching FBX loader.
|
||||
bevy_ufbx = { path = "third_party/bevy_ufbx" }
|
||||
# Lossless RON editing fixes used by transactional prefab source edits.
|
||||
|
||||
64
README.md
64
README.md
@ -27,9 +27,6 @@ cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
||||
cargo test --workspace
|
||||
cargo validate-levels
|
||||
cargo bake-navigation --project . --check
|
||||
# Validate one artifact without opening a game window
|
||||
cargo run -p game -- --validate-navigation assets/navigation/generated/navigation_showcase_humanoid.nav.ron
|
||||
# Machine-readable project dependency and finding report
|
||||
cargo validate-levels --json
|
||||
cargo package-project --profile development
|
||||
@ -152,7 +149,6 @@ deep-stale variants.
|
||||
| Asset Browser project/file views | Browse `assets/`, search/filter/sort models, textures, materials, audio clips, levels, and prefabs; switch grid/list; expand model subassets; inspect file details; audition audio; drag supported assets/submeshes into the viewport |
|
||||
| Drag audio clip into viewport | Create an authored audio source; when an audio source is selected, assign the clip instead |
|
||||
| Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` |
|
||||
| Static/Skinned Mesh Renderer material slots | Assign a shared Material/Material Instance per slot; Browse/Select/Locate the reference, or Clear the override to restore the imported source material |
|
||||
| `Ctrl+P` | Centered command palette; search human labels or stable command IDs, use arrow keys to select, Enter to run |
|
||||
| `F7` | While paused in Play: advance one sim tick |
|
||||
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
||||
@ -161,8 +157,6 @@ deep-stale variants.
|
||||
| Hierarchy lock | Excludes the actor from selection, gizmos, multi-drag, structural drops, and mutating context actions |
|
||||
| Hierarchy context | Group selection, create authored local children below linked prefab roots, remove/reparent generated members through same-layer structural overrides, or unparent |
|
||||
| File menu | New, Open, transactional Save/Save As, recovery restore/keep-copy/discard when available, Import Assets, Export Selection, Save Selection As Prefab (including linked-root variants), Recent Scenes |
|
||||
| Status strip / Asset Details source-state chip | Inspect compact clean, modified, untracked, conflicted, read-only, and optional ownership status; hover for the source path and provider details |
|
||||
| Authored File Not Saved dialog | Resolve an external edit, read-only target, or ownership lock with Reload, Compare Metadata, Save As, or Cancel; the editor never offers force overwrite |
|
||||
| Main toolbar, right side | Switch or close independent scene tabs, create an untitled tab, and manage loaded/locked composed subscenes |
|
||||
| Prefab Instance inspector | Inspect/recover source health, Apply/Revert overrides by scope, Apply overrides to source, create a variant, **Unpack Layer**, or recursively **Convert to Local** |
|
||||
| Inspector component card | Collapse with caret, toggle active with status dot, or use triple-dot menu for reset/copy/paste/move/remove actions |
|
||||
@ -236,22 +230,10 @@ Viewport shortcut keys require the pointer to be in the viewport and are suspend
|
||||
details when you need full `WorldAssetRoot` scene data instead of normalized static slots.
|
||||
Expanding a model in the Asset Browser exposes normalized mesh/material/texture plus generated
|
||||
skeleton/animation-clip subassets. Dragging a mesh places that part through the static mesh path;
|
||||
dragging a clip creates a dedicated skinned renderer or assigns an exact-rig-compatible state to
|
||||
the selected skinned actor. **Default animation** in Model Import Settings optionally selects the
|
||||
exact clip sampled and paused as the edit-rest pose; **Imported rest pose** never guesses a clip.
|
||||
Preview stop and PIE exit restore imported transforms and morph weights before resampling that
|
||||
explicit default. The Animation Controller inspector edits state IDs, clips,
|
||||
dragging a clip creates an animated scene instance or assigns an exact-rig-compatible state to
|
||||
the selected imported model. The Animation Controller inspector edits state IDs, clips,
|
||||
range/loop/speed/default/crossfade and provides non-dirty play/pause/stop/scrub preview. See the
|
||||
[animation authoring guide](docs/editor/animation-authoring.md) and [ADR 0031](docs/adr/0031-animation-authoring-runtime-contract.md).
|
||||
- Navigation bounds, obstacles, areas, and links are created from **Scene > Navigation** or the path
|
||||
icon in the existing horizontal toolbar. Bounds bake versioned Rerecast artifacts under
|
||||
`assets/navigation/generated/`; overlapping primitive and additive-brush triangles participate
|
||||
in the bake fingerprint while distant authoring is excluded. Visible composed subscenes and linked
|
||||
prefab sources resolve through the same deterministic bake path used by CI. The Inspector reports
|
||||
stale/current state, pins named validation paths, and provides a Polyanya-backed path test whose
|
||||
mesh, links, and route render in the viewport. Use
|
||||
`cargo bake-navigation --project . --check` in CI. See the
|
||||
[navigation authoring guide](docs/editor/navigation-authoring.md) and [ADR 0032](docs/adr/0032-versioned-navigation-bake-and-runtime-query.md).
|
||||
- Brush actors are persisted as `ActorKind::Brush + BrushDesc`; valid convex faces hydrate into
|
||||
generated preview meshes. Vertex, edge, and face selections use the standard transform gizmo,
|
||||
face material/UV fields are undoable, and clip/intersect/merge/subtract provide conservative
|
||||
@ -263,13 +245,6 @@ Viewport shortcut keys require the pointer to be in the viewport and are suspend
|
||||
without changing the scene.
|
||||
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
||||
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
||||
- Loaded scenes, prefab source Apply/history, Materials, Material Instances, and Project Settings
|
||||
retain exact BLAKE3 disk revisions. Save verifies the revision again immediately before atomic
|
||||
replacement; an external edit, create race, read-only target, or provider lock leaves the
|
||||
existing file untouched and opens the collaborative recovery dialog. Git status is observational
|
||||
and optional. See the
|
||||
[collaborative file-safety guide](docs/editor/collaborative-file-safety.md) and
|
||||
[ADR 0037](docs/adr/0037-collaborative-authored-file-safety.md).
|
||||
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
||||
`LevelObject` edits made during PIE **remain** in the scene (the level may show as dirty).
|
||||
- Play mode swaps the unified viewport between the player camera (possessed) and editor fly camera
|
||||
@ -332,16 +307,6 @@ identity, project and active-scene paths, dirty flags, aggregate validation coun
|
||||
state, and the bounded Scene I/O log. It excludes scene and asset contents, environment values,
|
||||
host/user identity, credentials, access tokens, and modal tool state.
|
||||
|
||||
### External file changes and read-only assets
|
||||
|
||||
The active scene status strip and selected asset header show compact Git/read-only/ownership state
|
||||
when available. If Save or Apply detects a different disk revision, Blacksite leaves that revision
|
||||
untouched and opens **Authored File Not Saved**. Use **Compare Metadata** to inspect both revisions,
|
||||
**Save As** to preserve the editor copy elsewhere, **Reload** to take the disk version, or **Cancel**
|
||||
to keep the local draft unsaved. Git and ownership providers are optional; the status scanner never
|
||||
stages, commits, resets, checks out, restores, or discards files. See
|
||||
[collaborative-file-safety.md](docs/editor/collaborative-file-safety.md).
|
||||
|
||||
## Cursor / VSCode Setup
|
||||
|
||||
The `.vscode/` folder is preconfigured:
|
||||
@ -364,8 +329,6 @@ The `.vscode/` folder is preconfigured:
|
||||
- [ADR 0014: Unified Viewport Model](docs/adr/0014-unified-viewport-model.md)
|
||||
- [ADR 0016: Unified Rendering Contract](docs/adr/0016-unified-rendering-contract.md)
|
||||
- [ADR 0017: Normalized Static Mesh Assets](docs/adr/0017-normalized-static-mesh-assets.md)
|
||||
- [ADR 0034: Registry-driven Authoring Components](docs/adr/0034-registry-driven-authoring-components.md)
|
||||
- [ADR 0037: Collaborative Authored-File Safety](docs/adr/0037-collaborative-authored-file-safety.md)
|
||||
|
||||
## Project Layout
|
||||
|
||||
@ -412,14 +375,12 @@ crates/
|
||||
- [x] Audio clip catalog/import foundation for Ogg, WAV, MP3, and FLAC with dedicated filtering, file details, and stable runtime-resolvable asset references
|
||||
- [x] Audio source/listener authoring, non-dirty spatial audition, viewport icons/range gizmos, stable buses, PIE/runtime parity, device diagnostics, and shared release validation ([ADR 0030](docs/adr/0030-audio-authoring-and-bus-schema.md); production acceptance completed in [Gitea #47](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/47))
|
||||
- [x] glTF/GLB skeletal animation manifests, stable controller states, non-dirty preview, PIE/runtime hydration, and exact-signature compatibility validation ([ADR 0031](docs/adr/0031-animation-authoring-runtime-contract.md); production acceptance completed in [Gitea #46](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/46))
|
||||
- [x] Navigation bounds/obstacles/areas/links, persisted validation samples, composed-source resolution, deterministic stale-checked bake artifacts, viewport path preview, headless bake, and shared game/runtime query API ([ADR 0032](docs/adr/0032-versioned-navigation-bake-and-runtime-query.md); [evaluation](docs/editor/evaluations/navigation-authoring/); [Gitea #48](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/48))
|
||||
- [x] Exact authored-file revision guards for scenes, prefab source Apply/history, Materials, Material Instances, and Project Settings; compact observational Git/read-only/ownership state; and explicit Reload/Compare Metadata/Save As/Cancel recovery without force overwrite ([ADR 0037](docs/adr/0037-collaborative-authored-file-safety.md), [collaboration guide](docs/editor/collaborative-file-safety.md), [Gitea #49](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/49))
|
||||
- [x] Prefab instances (`PrefabInstance`) + save-as-prefab + unpack
|
||||
- [x] Independent dirty-tab close confirmation and all-tab save guard when switching projects
|
||||
- [x] Transactional scene writes + bounded user-local recovery snapshots ([ADR 0023](docs/adr/0023-transactional-scene-persistence-and-recovery.md))
|
||||
- [x] Versioned clean/crash session resume + privacy-bounded diagnostic bundle ([ADR 0024](docs/adr/0024-versioned-editor-session-state.md))
|
||||
- [x] Hierarchy multi-select, reparent undo, multi-entity gizmo transform
|
||||
- [x] Typed inspector undo (light, rigid body, collider, primitive, material, static mesh renderer) + registry-driven Add Component footer; skinned renderer sources are asset-owned and inspectable
|
||||
- [x] Typed inspector undo (light, rigid body, collider, primitive, material, static mesh renderer) + registry-driven Add Component footer
|
||||
- [x] Gameplay authoring markers + visualizers (`WeaponSpawn`, `TriggerVolume`, etc.)
|
||||
- [x] Command palette execution; PIE sim step (F7); `xtask validate-levels`
|
||||
- [x] ADRs 0005–0012 (prefab/registry, scene schema, EditorPlugin, editor structure, authoring/hydration, ActorKind, sun policy, zero-debt)
|
||||
@ -431,19 +392,16 @@ crates/
|
||||
- [x] Project Browser UI, strict manifest validation, `--project` startup activation, recent filtering, sandbox scaffolding, clean process handoff, and desktop launcher action ([ADR 0025](docs/adr/0025-project-root-is-a-startup-boundary.md))
|
||||
- [x] Independent scene tabs + stable subscene composition, recursive validation, ownership locks, active-world PIE consistency, and per-saved-tab recovery ([ADR 0026](docs/adr/0026-stable-scene-composition-and-active-document.md))
|
||||
- [x] Editor lib/bin split + `EditorPluginGroup`; game EditorPlugin dogfood panel
|
||||
- [x] FBX/glTF model import + normalized static/skinned renderer routing; explicit generic scene-instance load via `bevy_ufbx` / `ModelRef`
|
||||
- [x] FBX/glTF model import + normalized `StaticMeshRenderer` placement; explicit scene-instance load via `bevy_ufbx` / `ModelRef`
|
||||
- [x] Asset browser model thumbnails (unified `assets/thumbnails/` pipeline; `ThumbnailState` cache; FBX via `FbxThumbnailSource`)
|
||||
- [x] Shared Material/Material Instance assets, stable static/skinned renderer material slots, imported-source fallback, orphan preservation, and runtime-only property blocks ([ADR 0035](docs/adr/0035-shared-material-assets-and-renderer-slots.md), [material-system guide](docs/editor/material-system.md))
|
||||
- [x] Material assets (`assets/materials/*.ron`, inspector picker, drag-drop to selection)
|
||||
- [x] Prefab v2 core: shared stable nested override paths, property/component/structural scopes, recursive graph validation, linked-root variants, conflict recovery, transactional source Apply, and undoable unpack/convert ([ADR 0027](docs/adr/0027-stable-prefab-ownership-and-variants.md))
|
||||
- [x] Prefab v2 production acceptance: committed base/nested/variant fixtures pass workspace tests, recursive headless validation, packaged release startup, and live editor placement/inspection regression coverage ([Gitea #43](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/43))
|
||||
- [x] Shared project validation: editor Diagnostics and `cargo validate-levels --json` use one owner-attributed dependency/finding report across project settings, registry/import artifacts, materials, shaders, scenes, prefabs, brushes, and colliders; valid/missing/cyclic/incompatible fixtures fail on blocking content errors ([ADR 0028](docs/adr/0028-authoritative-project-content-validation.md), [Gitea #45](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/45))
|
||||
- [x] Validation-gated package foundation: versioned development/QA/release profiles, deterministic runtime exclusions, stable input snapshots, transactional staged publication, Cargo-reported artifacts, BLAKE3 metadata, authored default-scene startup, and a non-blocking editor Build panel with live logs/cancel/run/reveal ([ADR 0029](docs/adr/0029-validation-gated-build-profiles-and-packaging.md), [build guide](docs/editor/build-and-package.md), [Gitea #44](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/44))
|
||||
- [x] Advanced rendering: `GiMode`, post-process volumes, Rendering panel, requested/effective render stack, Solari integration, emissive materials, post FX assets ([ADR 0013](docs/adr/0013-rendering-tiers-and-post-process-volumes.md), [ADR 0016](docs/adr/0016-unified-rendering-contract.md), [rendering guide](docs/editor/rendering.md))
|
||||
- [x] Surface ABI v1 with shared raster/Solari evaluator dispatch, packed typed parameters/textures, exact cutout ray-candidate evaluation, last-good shader fallback, and explicit exclusion of skinned/morph-deformed Solari geometry ([ADR 0036](docs/adr/0036-surface-abi-and-solari-parity.md), [material-system guide](docs/editor/material-system.md))
|
||||
- [x] Static mesh renderer component, generated normalized mesh artifacts, source/one-actor hierarchy placement, and inspector renderer slots ([ADR 0017](docs/adr/0017-normalized-static-mesh-assets.md))
|
||||
- [x] Dedicated `SkinnedMeshRenderer`/`SkinnedMesh` actor path, skinned-part exclusion from static slots, dedicated runtime hierarchy hydration, and v2-to-v3 animated-scene migration ([ADR 0033](docs/adr/0033-dedicated-skinned-mesh-renderer.md))
|
||||
- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware actor material data, and texture picker/drop refs ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md))
|
||||
- [x] Stable authoring component IDs, registry-built save filtering, reflected atomic add/remove/reset/copy/paste history, independent active state, static extension registration, and derived composable `ActorKind` hints ([ADR 0034](docs/adr/0034-registry-driven-authoring-components.md), [extensibility guide](docs/editor/extensibility.md))
|
||||
- [x] Brush authoring schema MVP with `ActorKind::Brush`, cube `BrushDesc`, generated mesh hydration, scene migration, and inspector Add Component support ([ADR 0021](docs/adr/0021-brush-authoring-schema.md))
|
||||
- [x] Brush draw, vertex/edge/face gizmo editing, face material/UV authoring, clip, and bounds-based CSG preview/commit workflow ([brush guide](docs/editor/brushes.md))
|
||||
- [x] Searchable command palette with human labels/stable IDs and a status bar that exposes scene I/O, tool, history, mode, and selection feedback
|
||||
@ -456,12 +414,10 @@ crates/
|
||||
untextured models), material sphere thumbnails, search/filter/sort controls, expandable model
|
||||
subasset shelves, and a staged details pane. **File → Import Assets**
|
||||
accepts glTF/GLB and **FBX** (binary; copies sibling `.fbm` texture folders when present).
|
||||
Model assets generate normalized model manifests under `assets/meshes/generated/`; drag/drop uses
|
||||
**Renderable Asset (Auto)**. Unrigged sources use `StaticMeshRenderer` with imported asset refs
|
||||
and optional separate static mesh colliders. Skin-bound or animated sources and their subasset placement use
|
||||
`SkinnedMeshRenderer`, preserve the source joint hierarchy, and never create static slots or
|
||||
automatic triangle colliders. Expanded mesh subassets generate independent thumbnails. Asset details can switch
|
||||
placement to **Scene Instance** for generic `ModelRef`/`WorldAssetRoot` scenes, shared material assets can be
|
||||
Model assets generate normalized static mesh manifests under `assets/meshes/generated/`; drag/drop
|
||||
uses `StaticMeshRenderer` by default with imported asset refs and optional separate static mesh
|
||||
collider components. Expanded mesh subassets generate independent thumbnails and can be placed independently. Asset details can switch
|
||||
placement to **Scene Instance** for `ModelRef`/`WorldAssetRoot` playback, shared material assets can be
|
||||
edited from the browser, and delete actions move files to `assets/.trash/`. Animation authoring
|
||||
supports glTF/GLB rig and clip manifests, stable controller refs, clip drag assignment/placement,
|
||||
inspector preview, PIE/runtime playback, and headless compatibility diagnostics. Animated or
|
||||
@ -476,7 +432,7 @@ crates/
|
||||
**Unpack Layer** preserves nested links; **Convert to Local** recursively removes them. Current
|
||||
authoring UI coverage and production-acceptance gaps are tracked in
|
||||
[prefab-authoring.md](docs/editor/prefab-authoring.md).
|
||||
- Material and direct-base Material Instance assets live under `assets/materials/`; shader schemas live under `assets/shaders/`. In the Asset Browser, select a Material and use **Create Instance** for sparse inherited variants. Static and skinned renderers own stable per-draw material slots, with explicit assignments taking precedence over imported source defaults; shared edits propagate through live-updated handles without reloading skinned geometry. Custom Surface evaluators share one constrained ABI between raster and Solari-eligible non-deformed geometry. Dynamic skinned/morph Solari geometry and a persisted property-block authoring workflow remain future work; see the [material-system guide](docs/editor/material-system.md).
|
||||
- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. The Asset Browser material details editor can apply shader schemas, edit typed parameters/textures, and regenerate sphere thumbnails. Actor inspector material edits live on the actor `MaterialDesc`; static mesh source-material refs remain imported defaults and shared material assets are edited from the Asset Browser.
|
||||
- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path.
|
||||
- The authoring/hydration layer is intentionally small so richer asset workflows (terrain,
|
||||
material graphs, lighting profiles) can be added without changing the scene format foundation.
|
||||
|
||||
@ -12,8 +12,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -30,8 +28,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -48,8 +44,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -66,8 +60,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -84,44 +76,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: Some("assets/meshes/generated/b98ef565-3500-49e7-9935-f685fa9b2594.static_mesh.ron"),
|
||||
animation_manifest_path: Some("assets/animations/generated/b98ef565-3500-49e7-9935-f685fa9b2594.animation.ron"),
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("113f74df-e39c-41d4-9b5b-e48efe541f7f"),
|
||||
path: "assets/models/RobotExpressive.glb",
|
||||
label: "RobotExpressive",
|
||||
kind_tag: "Model",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: SceneInstance,
|
||||
hierarchy_mode: SourceHierarchy,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: Some("assets/meshes/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.static_mesh.ron"),
|
||||
animation_manifest_path: Some("assets/animations/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.animation.ron"),
|
||||
default_animation_clip_id: Some("animation:clip:8:standing"),
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("3f63f359-45eb-4cb2-8970-71921cbd7bd0"),
|
||||
path: "assets/models/robot_expressive.glb",
|
||||
label: "robot_expressive",
|
||||
kind_tag: "Model",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: Some("assets/meshes/generated/3f63f359-45eb-4cb2-8970-71921cbd7bd0.static_mesh.ron"),
|
||||
animation_manifest_path: Some("assets/animations/generated/3f63f359-45eb-4cb2-8970-71921cbd7bd0.animation.ron"),
|
||||
default_animation_clip_id: Some("animation:clip:8:standing"),
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -138,8 +92,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -156,8 +108,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -174,8 +124,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -192,26 +140,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("706e604e-1a63-4e97-b35d-eeb1d88fe3a9"),
|
||||
path: "assets/levels/navigation_authoring_showcase.scn.ron",
|
||||
label: "navigation_authoring_showcase.scn",
|
||||
kind_tag: "Level",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -228,8 +156,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -246,8 +172,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -264,8 +188,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -282,8 +204,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -300,8 +220,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -318,8 +236,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -336,62 +252,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("04a4e00e-4732-43db-8e99-9cd99b94667b"),
|
||||
path: "assets/animations/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.animation.ron",
|
||||
label: "113f74df-e39c-41d4-9b5b-e48efe541f7f.animation",
|
||||
kind_tag: "Level",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("4937e63f-923b-4270-bdfc-298d63bcb814"),
|
||||
path: "assets/animations/generated/3f63f359-45eb-4cb2-8970-71921cbd7bd0.animation.ron",
|
||||
label: "3f63f359-45eb-4cb2-8970-71921cbd7bd0.animation",
|
||||
kind_tag: "Level",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("28f1df44-6d24-4722-bf75-6969879995a4"),
|
||||
path: "assets/animations/generated/b98ef565-3500-49e7-9935-f685fa9b2594.animation.ron",
|
||||
label: "b98ef565-3500-49e7-9935-f685fa9b2594.animation",
|
||||
kind_tag: "Level",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -408,8 +268,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -426,26 +284,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("264e2010-0693-4d74-8440-99373c2cedb3"),
|
||||
path: "assets/navigation/generated/navigation_showcase_humanoid.nav.ron",
|
||||
label: "navigation_showcase_humanoid.nav",
|
||||
kind_tag: "Level",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -462,26 +300,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("cc2769da-c9a5-4cb3-866a-cc81d0688ee2"),
|
||||
path: "assets/materials/surface_tint_instance.ron",
|
||||
label: "surface_tint_instance",
|
||||
kind_tag: "Material",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -498,26 +316,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("d6cb4151-7124-4237-aaf9-f7f8abd5fb76"),
|
||||
path: "assets/materials/surface_tint.ron",
|
||||
label: "surface_tint",
|
||||
kind_tag: "Material",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -534,8 +332,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -552,8 +348,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -570,8 +364,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -588,8 +380,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
@ -606,26 +396,6 @@
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
(
|
||||
id: ("a72943d6-3e9d-4629-8887-5c02d99d4f92"),
|
||||
path: "assets/shaders/surface_tint.shader.ron",
|
||||
label: "surface_tint.shader",
|
||||
kind_tag: "ShaderSchema",
|
||||
import_settings: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
|
||||
@ -1,236 +0,0 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
asset_id: "113f74df-e39c-41d4-9b5b-e48efe541f7f",
|
||||
label: "RobotExpressive",
|
||||
default_animation_clip_id: Some("animation:clip:8:standing"),
|
||||
source: (
|
||||
path: "assets/models/RobotExpressive.glb",
|
||||
format: "glb",
|
||||
fingerprint: (
|
||||
byte_len: 463988,
|
||||
modified_unix_secs: 1783799272,
|
||||
content_hash: "5869cb813e6a6093eda7af2824a9c13a582750e0443e88ce7c991a1f83415521",
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
runtime_supported: true,
|
||||
skeletons: [
|
||||
(
|
||||
id: "animation:skeleton:0:skeleton_0",
|
||||
label: "Skeleton 0",
|
||||
source_index: 0,
|
||||
signature: ("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b"),
|
||||
joint_paths: [
|
||||
"RootNode/RobotArmature/Bone",
|
||||
"RootNode/RobotArmature/Bone/Foot.L",
|
||||
"RootNode/RobotArmature/Bone/Body",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L/Middle2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L/Thumb2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L/Index2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L/Ring2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R/Middle2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R/Thumb2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R/Index2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R/Ring2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.L",
|
||||
"RootNode/RobotArmature/Bone/Foot.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.R",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "animation:skeleton:1:skeleton_1",
|
||||
label: "Skeleton 1",
|
||||
source_index: 1,
|
||||
signature: ("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b"),
|
||||
joint_paths: [
|
||||
"RootNode/RobotArmature/Bone",
|
||||
"RootNode/RobotArmature/Bone/Foot.L",
|
||||
"RootNode/RobotArmature/Bone/Body",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L/Middle2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L/Thumb2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L/Index2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L/Ring2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R/Middle2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R/Thumb2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R/Index2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R/Ring2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.L",
|
||||
"RootNode/RobotArmature/Bone/Foot.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.R",
|
||||
],
|
||||
),
|
||||
],
|
||||
clips: [
|
||||
(
|
||||
id: "animation:clip:0:dance",
|
||||
label: "Dance",
|
||||
source_index: 0,
|
||||
duration_seconds: 3.3333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:1:death",
|
||||
label: "Death",
|
||||
source_index: 1,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:2:idle",
|
||||
label: "Idle",
|
||||
source_index: 2,
|
||||
duration_seconds: 3.3333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:3:jump",
|
||||
label: "Jump",
|
||||
source_index: 3,
|
||||
duration_seconds: 0.7083333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:4:no",
|
||||
label: "No",
|
||||
source_index: 4,
|
||||
duration_seconds: 1.6666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:5:punch",
|
||||
label: "Punch",
|
||||
source_index: 5,
|
||||
duration_seconds: 0.8333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:6:running",
|
||||
label: "Running",
|
||||
source_index: 6,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:7:sitting",
|
||||
label: "Sitting",
|
||||
source_index: 7,
|
||||
duration_seconds: 0.41666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:8:standing",
|
||||
label: "Standing",
|
||||
source_index: 8,
|
||||
duration_seconds: 0.41666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:9:thumbsup",
|
||||
label: "ThumbsUp",
|
||||
source_index: 9,
|
||||
duration_seconds: 1.5833334,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:10:walking",
|
||||
label: "Walking",
|
||||
source_index: 10,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:11:walkjump",
|
||||
label: "WalkJump",
|
||||
source_index: 11,
|
||||
duration_seconds: 0.8333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:12:wave",
|
||||
label: "Wave",
|
||||
source_index: 12,
|
||||
duration_seconds: 1.8333334,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:13:yes",
|
||||
label: "Yes",
|
||||
source_index: 13,
|
||||
duration_seconds: 1.6666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
],
|
||||
diagnostics: [],
|
||||
)
|
||||
@ -1,236 +0,0 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
asset_id: "3f63f359-45eb-4cb2-8970-71921cbd7bd0",
|
||||
label: "robot_expressive",
|
||||
default_animation_clip_id: Some("animation:clip:8:standing"),
|
||||
source: (
|
||||
path: "assets/models/robot_expressive.glb",
|
||||
format: "glb",
|
||||
fingerprint: (
|
||||
byte_len: 463988,
|
||||
modified_unix_secs: 1783750957,
|
||||
content_hash: "5869cb813e6a6093eda7af2824a9c13a582750e0443e88ce7c991a1f83415521",
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
runtime_supported: true,
|
||||
skeletons: [
|
||||
(
|
||||
id: "animation:skeleton:0:skeleton_0",
|
||||
label: "Skeleton 0",
|
||||
source_index: 0,
|
||||
signature: ("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b"),
|
||||
joint_paths: [
|
||||
"RootNode/RobotArmature/Bone",
|
||||
"RootNode/RobotArmature/Bone/Foot.L",
|
||||
"RootNode/RobotArmature/Bone/Body",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L/Middle2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L/Thumb2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L/Index2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L/Ring2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R/Middle2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R/Thumb2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R/Index2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R/Ring2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.L",
|
||||
"RootNode/RobotArmature/Bone/Foot.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.R",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "animation:skeleton:1:skeleton_1",
|
||||
label: "Skeleton 1",
|
||||
source_index: 1,
|
||||
signature: ("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b"),
|
||||
joint_paths: [
|
||||
"RootNode/RobotArmature/Bone",
|
||||
"RootNode/RobotArmature/Bone/Foot.L",
|
||||
"RootNode/RobotArmature/Bone/Body",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm2.L/Middle1.L/Middle2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Thumb.L/Thumb2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm1.L/Index.L/Index2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/LowerArm.L/Palm3.L/Ring1.L/Ring2.L",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm2.R/Middle1.R/Middle2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Thumb.R/Thumb2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm1.R/Index.R/Index2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R",
|
||||
"RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/LowerArm.R/Palm3.R/Ring1.R/Ring2.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R",
|
||||
"RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.L",
|
||||
"RootNode/RobotArmature/Bone/Foot.R",
|
||||
"RootNode/RobotArmature/Bone/PoleTarget.R",
|
||||
],
|
||||
),
|
||||
],
|
||||
clips: [
|
||||
(
|
||||
id: "animation:clip:0:dance",
|
||||
label: "Dance",
|
||||
source_index: 0,
|
||||
duration_seconds: 3.3333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:1:death",
|
||||
label: "Death",
|
||||
source_index: 1,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:2:idle",
|
||||
label: "Idle",
|
||||
source_index: 2,
|
||||
duration_seconds: 3.3333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:3:jump",
|
||||
label: "Jump",
|
||||
source_index: 3,
|
||||
duration_seconds: 0.7083333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:4:no",
|
||||
label: "No",
|
||||
source_index: 4,
|
||||
duration_seconds: 1.6666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:5:punch",
|
||||
label: "Punch",
|
||||
source_index: 5,
|
||||
duration_seconds: 0.8333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:6:running",
|
||||
label: "Running",
|
||||
source_index: 6,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:7:sitting",
|
||||
label: "Sitting",
|
||||
source_index: 7,
|
||||
duration_seconds: 0.41666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:8:standing",
|
||||
label: "Standing",
|
||||
source_index: 8,
|
||||
duration_seconds: 0.41666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:9:thumbsup",
|
||||
label: "ThumbsUp",
|
||||
source_index: 9,
|
||||
duration_seconds: 1.5833334,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:10:walking",
|
||||
label: "Walking",
|
||||
source_index: 10,
|
||||
duration_seconds: 0.9583333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:11:walkjump",
|
||||
label: "WalkJump",
|
||||
source_index: 11,
|
||||
duration_seconds: 0.8333333,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:12:wave",
|
||||
label: "Wave",
|
||||
source_index: 12,
|
||||
duration_seconds: 1.8333334,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
(
|
||||
id: "animation:clip:13:yes",
|
||||
label: "Yes",
|
||||
source_index: 13,
|
||||
duration_seconds: 1.6666666,
|
||||
target_skeleton_signature: Some(("e9761f2a198d6a280afd0fc2aca6e8783dc6237bd85c90b8bc07a2e11271dc2b")),
|
||||
events: [],
|
||||
),
|
||||
],
|
||||
diagnostics: [],
|
||||
)
|
||||
@ -1,20 +0,0 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
asset_id: "b98ef565-3500-49e7-9935-f685fa9b2594",
|
||||
label: "painted_wooden_chair_02_2k",
|
||||
default_animation_clip_id: None,
|
||||
source: (
|
||||
path: "assets/models/painted_wooden_chair_02_2k.fbx",
|
||||
format: "fbx",
|
||||
fingerprint: (
|
||||
byte_len: 59964,
|
||||
modified_unix_secs: 1780713434,
|
||||
content_hash: "b12973a62dcb44589e380ea833eade726ae98a86c81084c842ee3801866d6a46",
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
runtime_supported: false,
|
||||
skeletons: [],
|
||||
clips: [],
|
||||
diagnostics: [],
|
||||
)
|
||||
@ -1,16 +1,18 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
4294969001: (components: {
|
||||
"bevy_ecs::name::Name": "Audition Tone Source",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
(schema_version: 2,
|
||||
resources: {},
|
||||
entities: {
|
||||
4294969001: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Audition Tone Source",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 1.5, -4.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorId": ("audio-showcase-source"),
|
||||
"shared::components::ActorKind": AudioSource,
|
||||
"shared::components::AudioSourceDesc": (
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorId": ("audio-showcase-source"),
|
||||
"shared::components::ActorKind": AudioSource,
|
||||
"shared::components::AudioSourceDesc": (
|
||||
clip: Some((
|
||||
asset_id: "c0a7d10e-53c1-4de0-b0dc-34a66f44ba77",
|
||||
sub_asset_id: "audio:source",
|
||||
@ -30,23 +32,25 @@
|
||||
),
|
||||
bus: "sfx",
|
||||
),
|
||||
"shared::components::LevelObject": (),
|
||||
}),
|
||||
4294969002: (components: {
|
||||
"bevy_ecs::name::Name": "Showcase Listener",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
},
|
||||
),
|
||||
4294969002: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Showcase Listener",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 1.7, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorId": ("audio-showcase-listener"),
|
||||
"shared::components::ActorKind": AudioListener,
|
||||
"shared::components::AudioListenerDesc": (
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorId": ("audio-showcase-listener"),
|
||||
"shared::components::ActorKind": AudioListener,
|
||||
"shared::components::AudioListenerDesc": (
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
ear_gap: 0.2,
|
||||
),
|
||||
"shared::components::LevelObject": (),
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,73 +0,0 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
1: (components: {
|
||||
"bevy_ecs::name::Name": "Navigation Bounds",
|
||||
"bevy_transform::components::transform::Transform": (translation: (0.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0), scale: (1.0, 1.0, 1.0)),
|
||||
"shared::components::ActorId": ("navigation-bounds-main"),
|
||||
"shared::components::ActorKind": Navigation,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::HierarchySiblingIndex": (0),
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::navigation::NavigationBounds": (
|
||||
half_extents: (8.0, 2.0, 8.0),
|
||||
agent: (
|
||||
id: "humanoid",
|
||||
radius: 0.35,
|
||||
height: 1.8,
|
||||
max_climb: 0.45,
|
||||
max_slope_deg: 45.0,
|
||||
cell_size_fraction: 3.0,
|
||||
cell_height_fraction: 6.0,
|
||||
min_region_size: 1,
|
||||
merge_region_size: 2,
|
||||
),
|
||||
artifact_path: "assets/navigation/generated/navigation_showcase_humanoid.nav.ron",
|
||||
auto_bake: false,
|
||||
validation_samples: [
|
||||
(id: "around-center-obstacle", start: (-6.0, 0.0, -5.0), end: (6.0, 0.0, -5.0), enabled: true),
|
||||
],
|
||||
),
|
||||
}),
|
||||
2: (components: {
|
||||
"bevy_ecs::name::Name": "Center Obstacle",
|
||||
"bevy_transform::components::transform::Transform": (translation: (0.0, 0.5, 0.0), rotation: (0.0, 0.0, 0.0, 1.0), scale: (1.0, 1.0, 1.0)),
|
||||
"shared::components::ActorId": ("navigation-obstacle-center"),
|
||||
"shared::components::ActorKind": Navigation,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::HierarchySiblingIndex": (1),
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::navigation::NavigationObstacle": (half_extents: (1.0, 1.0, 2.5), dynamic: false),
|
||||
}),
|
||||
3: (components: {
|
||||
"bevy_ecs::name::Name": "Slow Area",
|
||||
"bevy_transform::components::transform::Transform": (translation: (-4.0, 0.5, -4.0), rotation: (0.0, 0.0, 0.0, 1.0), scale: (1.0, 1.0, 1.0)),
|
||||
"shared::components::ActorId": ("navigation-area-slow"),
|
||||
"shared::components::ActorKind": Navigation,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::HierarchySiblingIndex": (2),
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::navigation::NavigationArea": (id: "slow", half_extents: (1.5, 1.0, 1.5), cost: 1.5, walkable: true),
|
||||
}),
|
||||
4: (components: {
|
||||
"bevy_ecs::name::Name": "Obstacle Link",
|
||||
"bevy_transform::components::transform::Transform": (translation: (0.0, 0.0, 0.0), rotation: (0.0, 0.0, 0.0, 1.0), scale: (1.0, 1.0, 1.0)),
|
||||
"shared::components::ActorId": ("navigation-link-center"),
|
||||
"shared::components::ActorKind": Navigation,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::HierarchySiblingIndex": (3),
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::navigation::NavigationLink": (start: (-1.5, 0.0, 0.0), end: (1.5, 0.0, 0.0), bidirectional: true, cost: 1.2, enabled: true),
|
||||
}),
|
||||
5: (components: {
|
||||
"bevy_ecs::name::Name": "Walkable Floor Geometry",
|
||||
"bevy_transform::components::transform::Transform": (translation: (0.0, -0.25, 0.0), rotation: (0.0, 0.0, 0.0, 1.0), scale: (1.0, 1.0, 1.0)),
|
||||
"shared::components::ActorId": ("navigation-floor-geometry"),
|
||||
"shared::components::ActorKind": StaticMesh,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::HierarchySiblingIndex": (4),
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::Primitive": (shape: Box, size: (16.0, 0.5, 16.0)),
|
||||
}),
|
||||
},
|
||||
)
|
||||
@ -1,16 +1,17 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
4294968001: (components: {
|
||||
"bevy_ecs::name::Name": "Fog Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
(schema_version: 2,
|
||||
resources: {},
|
||||
entities: {
|
||||
4294968001: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Fog Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 2.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
half_extents: (6.0, 3.0, 6.0),
|
||||
priority: 0,
|
||||
blend_distance: 2.0,
|
||||
@ -22,17 +23,19 @@
|
||||
profile: None,
|
||||
label: Some("Foggy courtyard"),
|
||||
),
|
||||
}),
|
||||
4294968002: (components: {
|
||||
"bevy_ecs::name::Name": "Dark Exposure Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
},
|
||||
),
|
||||
4294968002: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Dark Exposure Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (14.0, 2.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
half_extents: (4.0, 2.5, 4.0),
|
||||
priority: 5,
|
||||
blend_distance: 1.5,
|
||||
@ -44,17 +47,19 @@
|
||||
profile: Some("assets/rendering_profiles/cave_dark.ron"),
|
||||
label: Some("Cave mouth"),
|
||||
),
|
||||
}),
|
||||
4294968003: (components: {
|
||||
"bevy_ecs::name::Name": "Vignette FX Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
},
|
||||
),
|
||||
4294968003: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Vignette FX Volume",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (-12.0, 2.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorKind": PostProcessVolume,
|
||||
"shared::components::PostProcessVolumeDesc": (
|
||||
half_extents: (5.0, 2.0, 5.0),
|
||||
priority: 2,
|
||||
blend_distance: 2.0,
|
||||
@ -63,6 +68,7 @@
|
||||
profile: None,
|
||||
label: Some("Vignette demo"),
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -1,40 +1,12 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Concrete",
|
||||
shader: None,
|
||||
shader_ref: None,
|
||||
render_state: (
|
||||
alpha_mode: Opaque,
|
||||
alpha_cutoff: 0.5,
|
||||
double_sided: false,
|
||||
),
|
||||
material: (
|
||||
shader: (
|
||||
kind: StandardLit,
|
||||
schema_path: None,
|
||||
shader_path: None,
|
||||
),
|
||||
base_color: (
|
||||
r: 0.55,
|
||||
g: 0.54,
|
||||
b: 0.52,
|
||||
a: 1.0,
|
||||
),
|
||||
base_color: (r: 0.55, g: 0.54, b: 0.52, a: 1.0),
|
||||
metallic: 0.0,
|
||||
roughness: 0.85,
|
||||
emissive_color: (
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
),
|
||||
emissive_intensity: 0.0,
|
||||
base_color_texture: None,
|
||||
emissive_texture: None,
|
||||
normal_map_texture: None,
|
||||
metallic_roughness_texture: None,
|
||||
material_asset_path: None,
|
||||
parameters: [],
|
||||
textures: [],
|
||||
),
|
||||
)
|
||||
@ -1,40 +1,15 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Emissive Panel",
|
||||
shader: None,
|
||||
shader_ref: None,
|
||||
render_state: (
|
||||
alpha_mode: Opaque,
|
||||
alpha_cutoff: 0.5,
|
||||
double_sided: false,
|
||||
),
|
||||
material: (
|
||||
shader: (
|
||||
kind: StandardLit,
|
||||
schema_path: None,
|
||||
shader_path: None,
|
||||
),
|
||||
base_color: (
|
||||
r: 0.15,
|
||||
g: 0.22,
|
||||
b: 0.28,
|
||||
a: 1.0,
|
||||
),
|
||||
base_color: (r: 0.15, g: 0.22, b: 0.28, a: 1.0),
|
||||
metallic: 0.0,
|
||||
roughness: 0.35,
|
||||
emissive_color: (
|
||||
r: 0.55,
|
||||
g: 0.85,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
),
|
||||
emissive_color: (r: 0.55, g: 0.85, b: 1.0, a: 1.0),
|
||||
emissive_intensity: 2500.0,
|
||||
base_color_texture: None,
|
||||
emissive_texture: None,
|
||||
normal_map_texture: None,
|
||||
metallic_roughness_texture: None,
|
||||
material_asset_path: None,
|
||||
parameters: [],
|
||||
textures: [],
|
||||
),
|
||||
)
|
||||
@ -1,71 +0,0 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Surface Tint",
|
||||
shader: Some("assets/shaders/surface_tint.shader.ron"),
|
||||
shader_ref: None,
|
||||
render_state: (
|
||||
alpha_mode: Opaque,
|
||||
alpha_cutoff: 0.5,
|
||||
double_sided: false,
|
||||
),
|
||||
material: (
|
||||
shader: (
|
||||
kind: Custom,
|
||||
schema_path: Some("assets/shaders/surface_tint.shader.ron"),
|
||||
shader_path: Some("assets/shaders/surface_tint.wgsl"),
|
||||
),
|
||||
base_color: (
|
||||
r: 0.12,
|
||||
g: 0.42,
|
||||
b: 0.95,
|
||||
a: 1.0,
|
||||
),
|
||||
metallic: 0.15,
|
||||
roughness: 0.28,
|
||||
emissive_color: (
|
||||
r: 0.02,
|
||||
g: 0.08,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
),
|
||||
emissive_intensity: 3.0,
|
||||
base_color_texture: None,
|
||||
emissive_texture: None,
|
||||
normal_map_texture: None,
|
||||
metallic_roughness_texture: None,
|
||||
material_asset_path: None,
|
||||
parameters: [
|
||||
(
|
||||
name: "tint",
|
||||
value: Color((
|
||||
r: 0.12,
|
||||
g: 0.42,
|
||||
b: 0.95,
|
||||
a: 1.0,
|
||||
)),
|
||||
),
|
||||
(
|
||||
name: "roughness",
|
||||
value: Float(0.28),
|
||||
),
|
||||
(
|
||||
name: "metallic",
|
||||
value: Float(0.15),
|
||||
),
|
||||
(
|
||||
name: "emissive",
|
||||
value: Color((
|
||||
r: 0.02,
|
||||
g: 0.08,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
)),
|
||||
),
|
||||
(
|
||||
name: "emissive_intensity",
|
||||
value: Float(3.0),
|
||||
),
|
||||
],
|
||||
textures: [],
|
||||
),
|
||||
)
|
||||
@ -1,12 +0,0 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "surface_tint Instance",
|
||||
base: ((
|
||||
asset_id: "d6cb4151-7124-4237-aaf9-f7f8abd5fb76",
|
||||
sub_asset_id: "material:source",
|
||||
label: "surface_tint",
|
||||
source_path: Some("assets/materials/surface_tint.ron"),
|
||||
)),
|
||||
parameters: [],
|
||||
textures: [],
|
||||
)
|
||||
@ -1,360 +0,0 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
asset_id: "113f74df-e39c-41d4-9b5b-e48efe541f7f",
|
||||
label: "RobotExpressive",
|
||||
source: (
|
||||
path: "assets/models/RobotExpressive.glb",
|
||||
format: "glb",
|
||||
fingerprint: (
|
||||
byte_len: 463988,
|
||||
modified_unix_secs: 1783799272,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
import: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: SceneInstance,
|
||||
hierarchy_mode: SourceHierarchy,
|
||||
material_policy: SourceMaterials,
|
||||
),
|
||||
metadata: (
|
||||
mesh_count: 14,
|
||||
material_count: 3,
|
||||
node_count: 74,
|
||||
animation_count: 14,
|
||||
skin_count: 2,
|
||||
light_count: 0,
|
||||
camera_count: 0,
|
||||
),
|
||||
parts: [
|
||||
(
|
||||
id: "draw:scene0:node4:mesh0:primitive0",
|
||||
name: "Foot.L / Primitive 0",
|
||||
mesh_label: "Mesh0/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (0.62405795, 0.39766362, 0.19111444),
|
||||
rotation: (0.7071068, 0.00000005228401, 0.000000015504241, -0.70710665),
|
||||
scale: (100.0, 99.999985, 100.0),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Foot.L/Foot.L"),
|
||||
source_mesh: Some("Mesh0"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node7:mesh1:primitive0",
|
||||
name: "Torso / Primitive 0",
|
||||
mesh_label: "Mesh1/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.0027156367, 2.1737509, -0.019581214),
|
||||
rotation: (0.70710677, 0.000000000000031351603, 0.000000000000055267226, -0.7071067),
|
||||
scale: (100.0, 99.99998, 99.99998),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Torso"),
|
||||
source_mesh: Some("Mesh1"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node7:mesh1:primitive1",
|
||||
name: "Torso / Primitive 1",
|
||||
mesh_label: "Mesh1/Primitive1",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.0027156367, 2.1737509, -0.019581214),
|
||||
rotation: (0.70710677, 0.000000000000031351603, 0.000000000000055267226, -0.7071067),
|
||||
scale: (100.0, 99.99998, 99.99998),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Torso"),
|
||||
source_mesh: Some("Mesh1"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node61:mesh10:primitive0",
|
||||
name: "Leg.R / Primitive 0",
|
||||
mesh_label: "Mesh10/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.6746981, 1.1586236, 0.046261936),
|
||||
rotation: (0.7693698, 0.080835514, 0.110868365, -0.6238942),
|
||||
scale: (99.99999, 99.99886, 100.00024),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.R/Leg.R"),
|
||||
source_mesh: Some("Mesh10"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node63:mesh11:primitive0",
|
||||
name: "LowerLeg.R / Primitive 0",
|
||||
mesh_label: "Mesh11/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.73626167, 0.34982973, 0.22594015),
|
||||
rotation: (-0.65091395, -0.099058665, -0.09436109, 0.7467224),
|
||||
scale: (99.99999, 99.998604, 100.00058),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R/LowerLeg.R"),
|
||||
source_mesh: Some("Mesh11"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node72:mesh12:primitive0",
|
||||
name: "Hand.R / Primitive 0",
|
||||
mesh_label: "Mesh12/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.R"),
|
||||
source_mesh: Some("Mesh12"),
|
||||
source_material: Some("Main"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node72:mesh12:primitive1",
|
||||
name: "Hand.R / Primitive 1",
|
||||
mesh_label: "Mesh12/Primitive1",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.R"),
|
||||
source_mesh: Some("Mesh12"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node73:mesh13:primitive0",
|
||||
name: "Hand.L / Primitive 0",
|
||||
mesh_label: "Mesh13/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.L"),
|
||||
source_mesh: Some("Mesh13"),
|
||||
source_material: Some("Main"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node73:mesh13:primitive1",
|
||||
name: "Hand.L / Primitive 1",
|
||||
mesh_label: "Mesh13/Primitive1",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.L"),
|
||||
source_mesh: Some("Mesh13"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive0",
|
||||
name: "Head / Primitive 0",
|
||||
mesh_label: "Mesh2/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive1",
|
||||
name: "Head / Primitive 1",
|
||||
mesh_label: "Mesh2/Primitive1",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive2",
|
||||
name: "Head / Primitive 2",
|
||||
mesh_label: "Mesh2/Primitive2",
|
||||
material_id: Some("material:material2"),
|
||||
material_slot_name: "Black",
|
||||
material_label: Some("Material2"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Black"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node68:mesh3:primitive0",
|
||||
name: "Foot.R / Primitive 0",
|
||||
mesh_label: "Mesh3/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.637997, 0.3976636, 0.19111452),
|
||||
rotation: (0.70710677, 0.00000005201811, 0.000000015770217, -0.7071067),
|
||||
scale: (100.0, 99.999985, 100.0),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Foot.R/Foot.R"),
|
||||
source_mesh: Some("Mesh3"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node16:mesh4:primitive0",
|
||||
name: "Shoulder.L / Primitive 0",
|
||||
mesh_label: "Mesh4/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.58268076, 2.3724597, -0.020994142),
|
||||
rotation: (-0.59886926, 0.37597278, -0.37597266, 0.59886944),
|
||||
scale: (99.99996, 99.999954, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/Shoulder.L"),
|
||||
source_mesh: Some("Mesh4"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node18:mesh5:primitive0",
|
||||
name: "Arm.L / Primitive 0",
|
||||
mesh_label: "Mesh5/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.26500192, 2.8489676, 0.13069445),
|
||||
rotation: (-0.5164575, 0.4218856, -0.32294926, 0.67155623),
|
||||
scale: (99.99994, 99.99994, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/Arm.L"),
|
||||
source_mesh: Some("Mesh5"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node36:mesh6:primitive0",
|
||||
name: "Shoulder.R / Primitive 0",
|
||||
mesh_label: "Mesh6/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.6369861, 2.3718858, -0.020994166),
|
||||
rotation: (0.6017577, 0.37133226, -0.37133226, -0.6017577),
|
||||
scale: (99.999954, 99.99997, 99.99994),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/Shoulder.R"),
|
||||
source_mesh: Some("Mesh6"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node38:mesh7:primitive0",
|
||||
name: "Arm.R / Primitive 0",
|
||||
mesh_label: "Mesh7/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.82568955, 1.9825263, 0.040243305),
|
||||
rotation: (0.6276481, 0.35463694, -0.3873087, -0.5747021),
|
||||
scale: (22.70405, 22.704054, 22.704046),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/Arm.R"),
|
||||
source_mesh: Some("Mesh7"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node56:mesh8:primitive0",
|
||||
name: "Leg.L / Primitive 0",
|
||||
mesh_label: "Mesh8/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.6488191, 1.1620699, 0.057466835),
|
||||
rotation: (0.76387787, -0.05410902, -0.070370995, -0.6392268),
|
||||
scale: (99.99999, 100.00079, 100.00024),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.L/Leg.L"),
|
||||
source_mesh: Some("Mesh8"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node58:mesh9:primitive0",
|
||||
name: "LowerLeg.L / Primitive 0",
|
||||
mesh_label: "Mesh9/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.68805873, 0.3497491, 0.23465614),
|
||||
rotation: (-0.6547612, 0.06408298, 0.061960325, 0.7505613),
|
||||
scale: (100.000015, 100.0006, 100.000496),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L/LowerLeg.L"),
|
||||
source_mesh: Some("Mesh9"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
],
|
||||
warnings: [
|
||||
"Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer.",
|
||||
"Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer.",
|
||||
],
|
||||
)
|
||||
@ -1,360 +0,0 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
asset_id: "3f63f359-45eb-4cb2-8970-71921cbd7bd0",
|
||||
label: "robot_expressive",
|
||||
source: (
|
||||
path: "assets/models/robot_expressive.glb",
|
||||
format: "glb",
|
||||
fingerprint: (
|
||||
byte_len: 463988,
|
||||
modified_unix_secs: 1783750957,
|
||||
),
|
||||
dependencies: [],
|
||||
),
|
||||
import: (
|
||||
scale: 1.0,
|
||||
generate_collider: true,
|
||||
lod0_only: true,
|
||||
placement_mode: StaticAsset,
|
||||
hierarchy_mode: SingleActor,
|
||||
material_policy: SourceMaterials,
|
||||
),
|
||||
metadata: (
|
||||
mesh_count: 14,
|
||||
material_count: 3,
|
||||
node_count: 74,
|
||||
animation_count: 14,
|
||||
skin_count: 2,
|
||||
light_count: 0,
|
||||
camera_count: 0,
|
||||
),
|
||||
parts: [
|
||||
(
|
||||
id: "draw:scene0:node4:mesh0:primitive0",
|
||||
name: "Foot.L / Primitive 0",
|
||||
mesh_label: "Mesh0/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (0.62405795, 0.39766362, 0.19111444),
|
||||
rotation: (0.7071068, 0.00000005228401, 0.000000015504241, -0.70710665),
|
||||
scale: (100.0, 99.999985, 100.0),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Foot.L/Foot.L"),
|
||||
source_mesh: Some("Mesh0"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node7:mesh1:primitive0",
|
||||
name: "Torso / Primitive 0",
|
||||
mesh_label: "Mesh1/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.0027156367, 2.1737509, -0.019581214),
|
||||
rotation: (0.70710677, 0.000000000000031351603, 0.000000000000055267226, -0.7071067),
|
||||
scale: (100.0, 99.99998, 99.99998),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Torso"),
|
||||
source_mesh: Some("Mesh1"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node7:mesh1:primitive1",
|
||||
name: "Torso / Primitive 1",
|
||||
mesh_label: "Mesh1/Primitive1",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.0027156367, 2.1737509, -0.019581214),
|
||||
rotation: (0.70710677, 0.000000000000031351603, 0.000000000000055267226, -0.7071067),
|
||||
scale: (100.0, 99.99998, 99.99998),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Torso"),
|
||||
source_mesh: Some("Mesh1"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node61:mesh10:primitive0",
|
||||
name: "Leg.R / Primitive 0",
|
||||
mesh_label: "Mesh10/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.6746981, 1.1586236, 0.046261936),
|
||||
rotation: (0.7693698, 0.080835514, 0.110868365, -0.6238942),
|
||||
scale: (99.99999, 99.99886, 100.00024),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.R/Leg.R"),
|
||||
source_mesh: Some("Mesh10"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node63:mesh11:primitive0",
|
||||
name: "LowerLeg.R / Primitive 0",
|
||||
mesh_label: "Mesh11/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.73626167, 0.34982973, 0.22594015),
|
||||
rotation: (-0.65091395, -0.099058665, -0.09436109, 0.7467224),
|
||||
scale: (99.99999, 99.998604, 100.00058),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.R/LowerLeg.R/LowerLeg.R"),
|
||||
source_mesh: Some("Mesh11"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node72:mesh12:primitive0",
|
||||
name: "Hand.R / Primitive 0",
|
||||
mesh_label: "Mesh12/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.R"),
|
||||
source_mesh: Some("Mesh12"),
|
||||
source_material: Some("Main"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node72:mesh12:primitive1",
|
||||
name: "Hand.R / Primitive 1",
|
||||
mesh_label: "Mesh12/Primitive1",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.R"),
|
||||
source_mesh: Some("Mesh12"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node73:mesh13:primitive0",
|
||||
name: "Hand.L / Primitive 0",
|
||||
mesh_label: "Mesh13/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.L"),
|
||||
source_mesh: Some("Mesh13"),
|
||||
source_material: Some("Main"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node73:mesh13:primitive1",
|
||||
name: "Hand.L / Primitive 1",
|
||||
mesh_label: "Mesh13/Primitive1",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.002715637, 2.3702974, -0.020994147),
|
||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
||||
scale: (100.0, 99.99999, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/Hand.L"),
|
||||
source_mesh: Some("Mesh13"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: true,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive0",
|
||||
name: "Head / Primitive 0",
|
||||
mesh_label: "Mesh2/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive1",
|
||||
name: "Head / Primitive 1",
|
||||
mesh_label: "Mesh2/Primitive1",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node13:mesh2:primitive2",
|
||||
name: "Head / Primitive 2",
|
||||
mesh_label: "Mesh2/Primitive2",
|
||||
material_id: Some("material:material2"),
|
||||
material_slot_name: "Black",
|
||||
material_label: Some("Material2"),
|
||||
local_transform: (
|
||||
translation: (-0.05334633, 3.6175368, -0.0068152454),
|
||||
rotation: (0.7249889, 0.0852804, 0.05460569, -0.6812757),
|
||||
scale: (99.99997, 99.99996, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Neck/Head/Head"),
|
||||
source_mesh: Some("Mesh2"),
|
||||
source_material: Some("Black"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node68:mesh3:primitive0",
|
||||
name: "Foot.R / Primitive 0",
|
||||
mesh_label: "Mesh3/Primitive0",
|
||||
material_id: Some("material:material0"),
|
||||
material_slot_name: "Grey",
|
||||
material_label: Some("Material0"),
|
||||
local_transform: (
|
||||
translation: (-0.637997, 0.3976636, 0.19111452),
|
||||
rotation: (0.70710677, 0.00000005201811, 0.000000015770217, -0.7071067),
|
||||
scale: (100.0, 99.999985, 100.0),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Foot.R/Foot.R"),
|
||||
source_mesh: Some("Mesh3"),
|
||||
source_material: Some("Grey"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node16:mesh4:primitive0",
|
||||
name: "Shoulder.L / Primitive 0",
|
||||
mesh_label: "Mesh4/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.58268076, 2.3724597, -0.020994142),
|
||||
rotation: (-0.59886926, 0.37597278, -0.37597266, 0.59886944),
|
||||
scale: (99.99996, 99.999954, 99.99997),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/Shoulder.L"),
|
||||
source_mesh: Some("Mesh4"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node18:mesh5:primitive0",
|
||||
name: "Arm.L / Primitive 0",
|
||||
mesh_label: "Mesh5/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.26500192, 2.8489676, 0.13069445),
|
||||
rotation: (-0.5164575, 0.4218856, -0.32294926, 0.67155623),
|
||||
scale: (99.99994, 99.99994, 99.99999),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.L/UpperArm.L/Arm.L"),
|
||||
source_mesh: Some("Mesh5"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node36:mesh6:primitive0",
|
||||
name: "Shoulder.R / Primitive 0",
|
||||
mesh_label: "Mesh6/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.6369861, 2.3718858, -0.020994166),
|
||||
rotation: (0.6017577, 0.37133226, -0.37133226, -0.6017577),
|
||||
scale: (99.999954, 99.99997, 99.99994),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/Shoulder.R"),
|
||||
source_mesh: Some("Mesh6"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node38:mesh7:primitive0",
|
||||
name: "Arm.R / Primitive 0",
|
||||
mesh_label: "Mesh7/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (-0.82568955, 1.9825263, 0.040243305),
|
||||
rotation: (0.6276481, 0.35463694, -0.3873087, -0.5747021),
|
||||
scale: (22.70405, 22.704054, 22.704046),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/Hips/Abdomen/Torso/Shoulder.R/UpperArm.R/Arm.R"),
|
||||
source_mesh: Some("Mesh7"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node56:mesh8:primitive0",
|
||||
name: "Leg.L / Primitive 0",
|
||||
mesh_label: "Mesh8/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.6488191, 1.1620699, 0.057466835),
|
||||
rotation: (0.76387787, -0.05410902, -0.070370995, -0.6392268),
|
||||
scale: (99.99999, 100.00079, 100.00024),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.L/Leg.L"),
|
||||
source_mesh: Some("Mesh8"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
(
|
||||
id: "draw:scene0:node58:mesh9:primitive0",
|
||||
name: "LowerLeg.L / Primitive 0",
|
||||
mesh_label: "Mesh9/Primitive0",
|
||||
material_id: Some("material:material1"),
|
||||
material_slot_name: "Main",
|
||||
material_label: Some("Material1"),
|
||||
local_transform: (
|
||||
translation: (0.68805873, 0.3497491, 0.23465614),
|
||||
rotation: (-0.6547612, 0.06408298, 0.061960325, 0.7505613),
|
||||
scale: (100.000015, 100.0006, 100.000496),
|
||||
),
|
||||
source_node: Some("RootNode/RobotArmature/Bone/Body/UpperLeg.L/LowerLeg.L/LowerLeg.L"),
|
||||
source_mesh: Some("Mesh9"),
|
||||
source_material: Some("Main"),
|
||||
skinned: false,
|
||||
),
|
||||
],
|
||||
warnings: [
|
||||
"Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer.",
|
||||
"Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer.",
|
||||
],
|
||||
)
|
||||
@ -1,5 +1,5 @@
|
||||
(
|
||||
schema_version: 3,
|
||||
schema_version: 1,
|
||||
asset_id: "b98ef565-3500-49e7-9935-f685fa9b2594",
|
||||
label: "painted_wooden_chair_02_2k",
|
||||
source: (
|
||||
@ -30,7 +30,7 @@
|
||||
),
|
||||
parts: [
|
||||
(
|
||||
id: "draw:scene0:node1:material0",
|
||||
id: "mesh:mesh1000",
|
||||
name: "painted_wooden_chair_02 / Material 0",
|
||||
mesh_label: "Mesh1000",
|
||||
material_id: Some("material:material0"),
|
||||
@ -44,7 +44,6 @@
|
||||
source_node: Some("Node1"),
|
||||
source_mesh: Some("Mesh1"),
|
||||
source_material: Some("painted_wooden_chair_02"),
|
||||
skinned: false,
|
||||
),
|
||||
],
|
||||
warnings: [],
|
||||
|
||||
BIN
assets/models/RobotExpressive.glb
(Stored with Git LFS)
BIN
assets/models/RobotExpressive.glb
(Stored with Git LFS)
Binary file not shown.
@ -1,977 +0,0 @@
|
||||
(
|
||||
schema_version: 2,
|
||||
generator: "blacksite-nav-2/rerecast-0.3.2/polyanya-0.16.1/glam-0.30.10",
|
||||
source_scene: "assets/levels/navigation_authoring_showcase.scn.ron",
|
||||
source_fingerprint: "15649e3f7c8f22d642ea318ba1dafb5a3ada82e2713219cb60bbb518c5e45195",
|
||||
payload_hash: "180b5a4698102e8185ae182bc30cf847466c4e9d65fe7834c315f6d1a03504c2",
|
||||
bounds_actor_id: "navigation-bounds-main",
|
||||
center: (0.0, 0.0, 0.0),
|
||||
half_extents: (8.0, 2.0, 8.0),
|
||||
agent: (
|
||||
id: "humanoid",
|
||||
radius: 0.35,
|
||||
height: 1.8,
|
||||
max_climb: 0.45,
|
||||
max_slope_deg: 45.0,
|
||||
cell_size_fraction: 3.0,
|
||||
cell_height_fraction: 6.0,
|
||||
min_region_size: 1,
|
||||
merge_region_size: 2,
|
||||
),
|
||||
mesh: (
|
||||
layers: [
|
||||
(
|
||||
vertices: [
|
||||
(
|
||||
coords: (-1.0, -2.516667),
|
||||
polygons: [
|
||||
0,
|
||||
4294967295,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.3000002, -2.516667),
|
||||
polygons: [
|
||||
4,
|
||||
4294967295,
|
||||
0,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, -7.3),
|
||||
polygons: [
|
||||
5,
|
||||
4,
|
||||
0,
|
||||
2,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, -1.5833335),
|
||||
polygons: [
|
||||
3,
|
||||
1,
|
||||
10,
|
||||
12,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -1.5833335),
|
||||
polygons: [
|
||||
10,
|
||||
1,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -2.516667),
|
||||
polygons: [
|
||||
0,
|
||||
4294967295,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -2.516667),
|
||||
polygons: [
|
||||
0,
|
||||
4294967295,
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, -7.3),
|
||||
polygons: [
|
||||
5,
|
||||
4,
|
||||
0,
|
||||
2,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, -7.3),
|
||||
polygons: [
|
||||
2,
|
||||
3,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, -1.5833335),
|
||||
polygons: [
|
||||
3,
|
||||
1,
|
||||
10,
|
||||
12,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.3000002, -2.516667),
|
||||
polygons: [
|
||||
4,
|
||||
4294967295,
|
||||
0,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.2833333, -2.516667),
|
||||
polygons: [
|
||||
6,
|
||||
4294967295,
|
||||
4,
|
||||
5,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, -7.3),
|
||||
polygons: [
|
||||
8,
|
||||
6,
|
||||
5,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, -7.3),
|
||||
polygons: [
|
||||
5,
|
||||
4,
|
||||
0,
|
||||
2,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, -7.3),
|
||||
polygons: [
|
||||
8,
|
||||
6,
|
||||
5,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.2833333, -2.516667),
|
||||
polygons: [
|
||||
6,
|
||||
4294967295,
|
||||
4,
|
||||
5,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -2.516667),
|
||||
polygons: [
|
||||
8,
|
||||
9,
|
||||
7,
|
||||
4294967295,
|
||||
6,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -2.516667),
|
||||
polygons: [
|
||||
8,
|
||||
9,
|
||||
7,
|
||||
4294967295,
|
||||
6,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -1.5833335),
|
||||
polygons: [
|
||||
7,
|
||||
13,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, -1.5833335),
|
||||
polygons: [
|
||||
15,
|
||||
13,
|
||||
7,
|
||||
9,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -2.516667),
|
||||
polygons: [
|
||||
8,
|
||||
9,
|
||||
7,
|
||||
4294967295,
|
||||
6,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, -1.5833335),
|
||||
polygons: [
|
||||
15,
|
||||
13,
|
||||
7,
|
||||
9,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, -7.3),
|
||||
polygons: [
|
||||
9,
|
||||
8,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, -7.3),
|
||||
polygons: [
|
||||
8,
|
||||
6,
|
||||
5,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -0.0666666),
|
||||
polygons: [
|
||||
11,
|
||||
12,
|
||||
10,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -1.5833335),
|
||||
polygons: [
|
||||
10,
|
||||
1,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, -1.5833335),
|
||||
polygons: [
|
||||
3,
|
||||
1,
|
||||
10,
|
||||
12,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, 1.5666666),
|
||||
polygons: [
|
||||
12,
|
||||
11,
|
||||
17,
|
||||
19,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, 1.5666666),
|
||||
polygons: [
|
||||
17,
|
||||
11,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, -0.0666666),
|
||||
polygons: [
|
||||
11,
|
||||
12,
|
||||
10,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, -1.5833335),
|
||||
polygons: [
|
||||
3,
|
||||
1,
|
||||
10,
|
||||
12,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, -1.5833335),
|
||||
polygons: [
|
||||
15,
|
||||
13,
|
||||
7,
|
||||
9,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -1.5833335),
|
||||
polygons: [
|
||||
7,
|
||||
13,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -0.0666666),
|
||||
polygons: [
|
||||
13,
|
||||
15,
|
||||
14,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, 1.5666666),
|
||||
polygons: [
|
||||
23,
|
||||
21,
|
||||
14,
|
||||
15,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, -1.5833335),
|
||||
polygons: [
|
||||
15,
|
||||
13,
|
||||
7,
|
||||
9,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, -0.0666666),
|
||||
polygons: [
|
||||
13,
|
||||
15,
|
||||
14,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, 1.5666666),
|
||||
polygons: [
|
||||
14,
|
||||
21,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, 7.283333),
|
||||
polygons: [
|
||||
24,
|
||||
25,
|
||||
4294967295,
|
||||
18,
|
||||
16,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.3000002, 2.5),
|
||||
polygons: [
|
||||
24,
|
||||
16,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, 2.5),
|
||||
polygons: [
|
||||
16,
|
||||
18,
|
||||
19,
|
||||
17,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, 2.5),
|
||||
polygons: [
|
||||
16,
|
||||
18,
|
||||
19,
|
||||
17,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, 1.5666666),
|
||||
polygons: [
|
||||
17,
|
||||
11,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, 1.5666666),
|
||||
polygons: [
|
||||
12,
|
||||
11,
|
||||
17,
|
||||
19,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, 7.283333),
|
||||
polygons: [
|
||||
19,
|
||||
18,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, 7.283333),
|
||||
polygons: [
|
||||
24,
|
||||
25,
|
||||
4294967295,
|
||||
18,
|
||||
16,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-1.0, 2.5),
|
||||
polygons: [
|
||||
16,
|
||||
18,
|
||||
19,
|
||||
17,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-7.3, 1.5666666),
|
||||
polygons: [
|
||||
12,
|
||||
11,
|
||||
17,
|
||||
19,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, 2.5),
|
||||
polygons: [
|
||||
21,
|
||||
23,
|
||||
22,
|
||||
20,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.2833333, 2.5),
|
||||
polygons: [
|
||||
20,
|
||||
25,
|
||||
24,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, 7.283333),
|
||||
polygons: [
|
||||
20,
|
||||
22,
|
||||
4294967295,
|
||||
25,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, 1.5666666),
|
||||
polygons: [
|
||||
23,
|
||||
21,
|
||||
14,
|
||||
15,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, 1.5666666),
|
||||
polygons: [
|
||||
14,
|
||||
21,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, 2.5),
|
||||
polygons: [
|
||||
21,
|
||||
23,
|
||||
22,
|
||||
20,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.9833336, 2.5),
|
||||
polygons: [
|
||||
21,
|
||||
23,
|
||||
22,
|
||||
20,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, 7.283333),
|
||||
polygons: [
|
||||
20,
|
||||
22,
|
||||
4294967295,
|
||||
25,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, 7.283333),
|
||||
polygons: [
|
||||
22,
|
||||
23,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (7.283333, 1.5666666),
|
||||
polygons: [
|
||||
23,
|
||||
21,
|
||||
14,
|
||||
15,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.41666698, 7.283333),
|
||||
polygons: [
|
||||
24,
|
||||
25,
|
||||
4294967295,
|
||||
18,
|
||||
16,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.39999962, 7.283333),
|
||||
polygons: [
|
||||
20,
|
||||
22,
|
||||
4294967295,
|
||||
25,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (0.2833333, 2.5),
|
||||
polygons: [
|
||||
20,
|
||||
25,
|
||||
24,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
(
|
||||
coords: (-0.3000002, 2.5),
|
||||
polygons: [
|
||||
24,
|
||||
16,
|
||||
4294967295,
|
||||
],
|
||||
is_corner: true,
|
||||
),
|
||||
],
|
||||
polygons: [
|
||||
(
|
||||
vertices: [
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
4,
|
||||
3,
|
||||
5,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
6,
|
||||
8,
|
||||
7,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
6,
|
||||
9,
|
||||
8,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
13,
|
||||
11,
|
||||
10,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
13,
|
||||
12,
|
||||
11,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
15,
|
||||
14,
|
||||
16,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
18,
|
||||
17,
|
||||
19,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
22,
|
||||
20,
|
||||
23,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
22,
|
||||
21,
|
||||
20,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
25,
|
||||
24,
|
||||
26,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
27,
|
||||
29,
|
||||
28,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
27,
|
||||
30,
|
||||
29,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
32,
|
||||
31,
|
||||
33,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
36,
|
||||
34,
|
||||
37,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
36,
|
||||
35,
|
||||
34,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
40,
|
||||
39,
|
||||
38,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
42,
|
||||
41,
|
||||
43,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
44,
|
||||
46,
|
||||
45,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
44,
|
||||
47,
|
||||
46,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
49,
|
||||
48,
|
||||
50,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
52,
|
||||
51,
|
||||
53,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
54,
|
||||
56,
|
||||
55,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
54,
|
||||
57,
|
||||
56,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
60,
|
||||
58,
|
||||
61,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
(
|
||||
vertices: [
|
||||
60,
|
||||
59,
|
||||
58,
|
||||
],
|
||||
is_one_way: false,
|
||||
),
|
||||
],
|
||||
offset: (0.0, 0.0),
|
||||
baked_polygons: None,
|
||||
islands: None,
|
||||
height: [
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
0.10000008,
|
||||
],
|
||||
),
|
||||
],
|
||||
search_delta: 0.1,
|
||||
search_steps: 2,
|
||||
),
|
||||
links: [
|
||||
(
|
||||
actor_id: "navigation-link-center",
|
||||
start: (-1.5, 0.0, 0.0),
|
||||
end: (1.5, 0.0, 0.0),
|
||||
bidirectional: true,
|
||||
cost: 1.2,
|
||||
enabled: true,
|
||||
),
|
||||
],
|
||||
areas: [
|
||||
(
|
||||
actor_id: "navigation-area-slow",
|
||||
id: "slow",
|
||||
center: (-4.0, 0.5, -4.0),
|
||||
half_extents: (1.5, 1.0, 1.5),
|
||||
cost: 1.5,
|
||||
walkable: true,
|
||||
),
|
||||
],
|
||||
samples: [
|
||||
(
|
||||
id: "around-center-obstacle",
|
||||
start: (-6.0, 0.0, -5.0),
|
||||
end: (6.0, 0.0, -5.0),
|
||||
enabled: true,
|
||||
),
|
||||
],
|
||||
diagnostics: (
|
||||
polygon_count: 26,
|
||||
island_count: 1,
|
||||
effective_component_count: 1,
|
||||
),
|
||||
)
|
||||
@ -1,17 +1,19 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
1: (components: {
|
||||
"bevy_ecs::name::Name": "Example Base Mesh",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
(schema_version: 2,
|
||||
resources: {},
|
||||
entities: {
|
||||
1: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Example Base Mesh",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 0.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorId": ("base-mesh"),
|
||||
"shared::components::ActorKind": Empty,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::LevelObject": (),
|
||||
}),
|
||||
},
|
||||
"shared::components::ActorId": ("base-mesh"),
|
||||
"shared::components::ActorKind": Empty,
|
||||
"shared::components::EditorVisibility": (visible: true),
|
||||
"shared::components::LevelObject": (),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -1,21 +1,23 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
1: (components: {
|
||||
"bevy_ecs::name::Name": "Example Base Link",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
(schema_version: 2,
|
||||
resources: {},
|
||||
entities: {
|
||||
1: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Example Base Link",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 0.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorId": ("base-link"),
|
||||
"shared::components::ActorKind": PrefabAnchor,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PrefabInstance": (
|
||||
"shared::components::ActorId": ("base-link"),
|
||||
"shared::components::ActorKind": PrefabAnchor,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PrefabInstance": (
|
||||
asset_id: "11111111-1111-4111-8111-111111111111",
|
||||
source_path: "assets/prefabs/example_base.scn.ron",
|
||||
overrides_ron: None,
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -1,21 +1,23 @@
|
||||
(schema_version: 4,resources: {
|
||||
},
|
||||
entities: {
|
||||
1: (components: {
|
||||
"bevy_ecs::name::Name": "Example Nested Variant Link",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
(schema_version: 2,
|
||||
resources: {},
|
||||
entities: {
|
||||
1: (
|
||||
components: {
|
||||
"bevy_ecs::name::Name": "Example Nested Variant Link",
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (0.0, 0.0, 0.0),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (1.0, 1.0, 1.0),
|
||||
),
|
||||
"shared::components::ActorId": ("nested-link"),
|
||||
"shared::components::ActorKind": PrefabAnchor,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PrefabInstance": (
|
||||
"shared::components::ActorId": ("nested-link"),
|
||||
"shared::components::ActorKind": PrefabAnchor,
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::PrefabInstance": (
|
||||
asset_id: "22222222-2222-4222-8222-222222222222",
|
||||
source_path: "assets/prefabs/example_nested.scn.ron",
|
||||
overrides_ron: Some("(format_version:2,source_revision:None,transform:None,material:None,child_visibility:{},properties:[],components:[(target:(instance_chain:[\"base-link\"],actor_id:\"base-mesh\"),component_type:\"shared::components::EditorVisibility\",base_component_ron:Some(\"(visible:true)\"),value_component_ron:Some(\"(visible:false)\"))],structural:[])"),
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -1,42 +0,0 @@
|
||||
#import bevy_pbr::{
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
pbr_functions::alpha_discard,
|
||||
}
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::{
|
||||
prepass_io::{VertexOutput, FragmentOutput},
|
||||
pbr_deferred_functions::deferred_output,
|
||||
}
|
||||
#else
|
||||
#import bevy_pbr::{
|
||||
forward_io::{VertexOutput, FragmentOutput},
|
||||
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
|
||||
}
|
||||
#endif
|
||||
|
||||
struct SurfaceUniform {
|
||||
shader_id: u32,
|
||||
flags: u32,
|
||||
alpha_cutoff: f32,
|
||||
abi_version: u32,
|
||||
params: array<vec4<f32>, 16>,
|
||||
uv_transforms: array<vec4<f32>, 8>,
|
||||
}
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> surface_uniform: SurfaceUniform;
|
||||
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
||||
#ifdef PREPASS_PIPELINE
|
||||
return deferred_output(in, pbr_input);
|
||||
#else
|
||||
var out: FragmentOutput;
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1,72 +1,17 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Standard Lit",
|
||||
kind: StandardLit,
|
||||
wgsl_path: None,
|
||||
parameters: [
|
||||
(
|
||||
name: "base_color",
|
||||
display_name: "Base Color",
|
||||
group: "Surface",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "metallic",
|
||||
display_name: "Metallic",
|
||||
group: "Surface",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(1.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
name: "roughness",
|
||||
display_name: "Roughness",
|
||||
group: "Surface",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(1.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
name: "emissive_color",
|
||||
display_name: "Emissive Color",
|
||||
group: "Emission",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "emissive_intensity",
|
||||
display_name: "Emissive Nits",
|
||||
group: "Emission",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(20000.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
name: "base_color_texture",
|
||||
display_name: "Base Color Texture",
|
||||
group: "Textures",
|
||||
property_type: Texture,
|
||||
),
|
||||
(
|
||||
name: "normal_map_texture",
|
||||
display_name: "Normal Map",
|
||||
group: "Textures",
|
||||
property_type: Texture,
|
||||
),
|
||||
(
|
||||
name: "metallic_roughness_texture",
|
||||
display_name: "Metallic/Roughness Texture",
|
||||
group: "Textures",
|
||||
property_type: Texture,
|
||||
),
|
||||
(
|
||||
name: "emissive_texture",
|
||||
display_name: "Emissive Texture",
|
||||
group: "Textures",
|
||||
property_type: Texture,
|
||||
),
|
||||
(name: "base_color", display_name: "Base Color", group: "Surface", property_type: Color),
|
||||
(name: "metallic", display_name: "Metallic", group: "Surface", property_type: Float(min: Some(0.0), max: Some(1.0))),
|
||||
(name: "roughness", display_name: "Roughness", group: "Surface", property_type: Float(min: Some(0.0), max: Some(1.0))),
|
||||
(name: "emissive_color", display_name: "Emissive Color", group: "Emission", property_type: Color),
|
||||
(name: "emissive_intensity", display_name: "Emissive Nits", group: "Emission", property_type: Float(min: Some(0.0), max: Some(20000.0))),
|
||||
(name: "base_color_texture", display_name: "Base Color Texture", group: "Textures", property_type: Texture),
|
||||
(name: "normal_map_texture", display_name: "Normal Map", group: "Textures", property_type: Texture),
|
||||
(name: "metallic_roughness_texture", display_name: "Metallic/Roughness Texture", group: "Textures", property_type: Texture),
|
||||
(name: "emissive_texture", display_name: "Emissive Texture", group: "Textures", property_type: Texture),
|
||||
],
|
||||
default_values: [],
|
||||
default_textures: [],
|
||||
|
||||
@ -1,80 +0,0 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Surface Tint",
|
||||
kind: Custom,
|
||||
wgsl_path: Some("assets/shaders/surface_tint.wgsl"),
|
||||
parameters: [
|
||||
(
|
||||
name: "tint",
|
||||
display_name: "Tint",
|
||||
group: "Surface",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "roughness",
|
||||
display_name: "Roughness",
|
||||
group: "Surface",
|
||||
property_type: Float(
|
||||
min: Some(0.001),
|
||||
max: Some(1.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
name: "metallic",
|
||||
display_name: "Metallic",
|
||||
group: "Surface",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(1.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
name: "emissive",
|
||||
display_name: "Emissive",
|
||||
group: "Emission",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "emissive_intensity",
|
||||
display_name: "Emissive Nits",
|
||||
group: "Emission",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(20000.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
default_values: [
|
||||
(
|
||||
name: "tint",
|
||||
value: Color((
|
||||
r: 0.12,
|
||||
g: 0.42,
|
||||
b: 0.95,
|
||||
a: 1.0,
|
||||
)),
|
||||
),
|
||||
(
|
||||
name: "roughness",
|
||||
value: Float(0.28),
|
||||
),
|
||||
(
|
||||
name: "metallic",
|
||||
value: Float(0.15),
|
||||
),
|
||||
(
|
||||
name: "emissive",
|
||||
value: Color((
|
||||
r: 0.02,
|
||||
g: 0.08,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
)),
|
||||
),
|
||||
(
|
||||
name: "emissive_intensity",
|
||||
value: Float(3.0),
|
||||
),
|
||||
],
|
||||
default_textures: [],
|
||||
)
|
||||
@ -1,14 +0,0 @@
|
||||
fn evaluate(
|
||||
input: SurfaceInput,
|
||||
params: SurfaceParams,
|
||||
samples: SurfaceSamples,
|
||||
) -> Surface {
|
||||
var surface = surface_default();
|
||||
surface.base_color = params.lanes[0];
|
||||
surface.perceptual_roughness = params.lanes[1].x;
|
||||
surface.metallic = params.lanes[2].x;
|
||||
surface.emissive = params.lanes[3].rgb * params.lanes[4].x;
|
||||
surface.reflectance = 0.5;
|
||||
surface.occlusion = 1.0;
|
||||
return surface;
|
||||
}
|
||||
@ -1,36 +1,12 @@
|
||||
(
|
||||
schema_version: 1,
|
||||
label: "Unlit",
|
||||
kind: Unlit,
|
||||
wgsl_path: None,
|
||||
parameters: [
|
||||
(
|
||||
name: "base_color",
|
||||
display_name: "Base Color",
|
||||
group: "Surface",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "base_color_texture",
|
||||
display_name: "Base Color Texture",
|
||||
group: "Textures",
|
||||
property_type: Texture,
|
||||
),
|
||||
(
|
||||
name: "emissive_color",
|
||||
display_name: "Emissive Color",
|
||||
group: "Emission",
|
||||
property_type: Color,
|
||||
),
|
||||
(
|
||||
name: "emissive_intensity",
|
||||
display_name: "Emissive Nits",
|
||||
group: "Emission",
|
||||
property_type: Float(
|
||||
min: Some(0.0),
|
||||
max: Some(20000.0),
|
||||
),
|
||||
),
|
||||
(name: "base_color", display_name: "Base Color", group: "Surface", property_type: Color),
|
||||
(name: "base_color_texture", display_name: "Base Color Texture", group: "Textures", property_type: Texture),
|
||||
(name: "emissive_color", display_name: "Emissive Color", group: "Emission", property_type: Color),
|
||||
(name: "emissive_intensity", display_name: "Emissive Nits", group: "Emission", property_type: Float(min: Some(0.0), max: Some(20000.0))),
|
||||
],
|
||||
default_values: [],
|
||||
default_textures: [],
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "blacksite_surface"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Blacksite raster and ray-tracing Surface ABI material runtime"
|
||||
|
||||
[dependencies]
|
||||
bevy.workspace = true
|
||||
naga = { version = "29", features = ["wgsl-in"] }
|
||||
ron = "0.8"
|
||||
shared.workspace = true
|
||||
uuid = "1"
|
||||
@ -1,762 +0,0 @@
|
||||
//! Blacksite Surface ABI v1.
|
||||
//!
|
||||
//! The authoring layer persists named values. This crate validates and packs them into a fixed GPU
|
||||
//! contract and supplies the same surface evaluator source to raster and Solari integrations.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
|
||||
use bevy::mesh::MeshVertexBufferLayoutRef;
|
||||
use bevy::pbr::{
|
||||
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::extract_resource::ExtractResource;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::{Shader, ShaderRef};
|
||||
use shared::{
|
||||
asset_server_path, material_from_desc, HydratedRendererMaterialBinding, MaterialAsset,
|
||||
MaterialInstanceAsset, MaterialParameterValue, MaterialRef, ShaderPropertyType,
|
||||
ShaderSchemaAsset, MATERIAL_INSTANCE_SCHEMA_VERSION,
|
||||
};
|
||||
|
||||
pub const SURFACE_ABI_VERSION: u32 = 1;
|
||||
pub const SURFACE_PARAMETER_LANES: usize = 16;
|
||||
pub const SURFACE_TEXTURE_SLOTS: usize = 8;
|
||||
pub const DEFAULT_SURFACE_SHADER_PATH: &str = "shaders/blacksite_surface.wgsl";
|
||||
|
||||
#[derive(ShaderType, Reflect, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SurfaceUniform {
|
||||
pub shader_id: u32,
|
||||
pub flags: u32,
|
||||
pub alpha_cutoff: f32,
|
||||
pub abi_version: u32,
|
||||
pub params: [Vec4; SURFACE_PARAMETER_LANES],
|
||||
pub uv_transforms: [Vec4; SURFACE_TEXTURE_SLOTS],
|
||||
}
|
||||
|
||||
impl Default for SurfaceUniform {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shader_id: 0,
|
||||
flags: 0,
|
||||
alpha_cutoff: 0.5,
|
||||
abi_version: SURFACE_ABI_VERSION,
|
||||
params: [Vec4::ZERO; SURFACE_PARAMETER_LANES],
|
||||
uv_transforms: [Vec4::new(1.0, 1.0, 0.0, 0.0); SURFACE_TEXTURE_SLOTS],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SurfaceExtensionKey {
|
||||
shader: AssetId<Shader>,
|
||||
}
|
||||
|
||||
impl From<&SurfaceExtension> for SurfaceExtensionKey {
|
||||
fn from(extension: &SurfaceExtension) -> Self {
|
||||
Self {
|
||||
shader: extension.shader.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)]
|
||||
#[bind_group_data(SurfaceExtensionKey)]
|
||||
pub struct SurfaceExtension {
|
||||
#[uniform(100)]
|
||||
pub uniform: SurfaceUniform,
|
||||
#[texture(101)]
|
||||
#[sampler(102)]
|
||||
pub texture0: Option<Handle<Image>>,
|
||||
#[texture(103)]
|
||||
#[sampler(104)]
|
||||
pub texture1: Option<Handle<Image>>,
|
||||
#[texture(105)]
|
||||
#[sampler(106)]
|
||||
pub texture2: Option<Handle<Image>>,
|
||||
#[texture(107)]
|
||||
#[sampler(108)]
|
||||
pub texture3: Option<Handle<Image>>,
|
||||
#[texture(109)]
|
||||
#[sampler(110)]
|
||||
pub texture4: Option<Handle<Image>>,
|
||||
#[texture(111)]
|
||||
#[sampler(112)]
|
||||
pub texture5: Option<Handle<Image>>,
|
||||
#[texture(113)]
|
||||
#[sampler(114)]
|
||||
pub texture6: Option<Handle<Image>>,
|
||||
#[texture(115)]
|
||||
#[sampler(116)]
|
||||
pub texture7: Option<Handle<Image>>,
|
||||
#[reflect(ignore)]
|
||||
pub shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl SurfaceExtension {
|
||||
pub fn set_texture(&mut self, index: usize, handle: Option<Handle<Image>>) {
|
||||
match index {
|
||||
0 => self.texture0 = handle,
|
||||
1 => self.texture1 = handle,
|
||||
2 => self.texture2 = handle,
|
||||
3 => self.texture3 = handle,
|
||||
4 => self.texture4 = handle,
|
||||
5 => self.texture5 = handle,
|
||||
6 => self.texture6 = handle,
|
||||
7 => self.texture7 = handle,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaterialExtension for SurfaceExtension {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
DEFAULT_SURFACE_SHADER_PATH.into()
|
||||
}
|
||||
|
||||
fn deferred_fragment_shader() -> ShaderRef {
|
||||
DEFAULT_SURFACE_SHADER_PATH.into()
|
||||
}
|
||||
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
DEFAULT_SURFACE_SHADER_PATH.into()
|
||||
}
|
||||
|
||||
fn specialize(
|
||||
_pipeline: &MaterialExtensionPipeline,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
_layout: &MeshVertexBufferLayoutRef,
|
||||
key: MaterialExtensionKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
if let (AssetId::Uuid { uuid }, Some(fragment)) =
|
||||
(key.bind_group_data.shader, descriptor.fragment.as_mut())
|
||||
{
|
||||
fragment.shader = Handle::Uuid(uuid, Default::default());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type SurfaceMaterial = ExtendedMaterial<StandardMaterial, SurfaceExtension>;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct SurfaceMaterialCache {
|
||||
handles: HashMap<MaterialRef, Handle<SurfaceMaterial>>,
|
||||
standard_handles: HashMap<MaterialRef, Handle<StandardMaterial>>,
|
||||
revisions: HashMap<MaterialRef, u64>,
|
||||
standard_only: HashMap<MaterialRef, u64>,
|
||||
dependencies: HashMap<MaterialRef, Vec<String>>,
|
||||
failed_revisions: HashMap<MaterialRef, u64>,
|
||||
}
|
||||
|
||||
enum BuiltRendererMaterial {
|
||||
Standard(Box<StandardMaterial>),
|
||||
Surface(Box<SurfaceMaterial>),
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone)]
|
||||
pub struct SurfaceDiagnostics(pub Vec<String>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SurfaceEvaluatorRecord {
|
||||
pub shader_id: u32,
|
||||
pub evaluator_source: String,
|
||||
pub revision: u64,
|
||||
pub alpha_mode: shared::MaterialAlphaMode,
|
||||
}
|
||||
|
||||
/// Active, validated evaluator generation consumed by both raster and Solari render worlds.
|
||||
#[derive(Resource, ExtractResource, Default, Debug, Clone)]
|
||||
pub struct SurfaceEvaluatorRegistry {
|
||||
pub evaluators: HashMap<u32, SurfaceEvaluatorRecord>,
|
||||
}
|
||||
|
||||
pub struct SurfaceMaterialPlugin;
|
||||
|
||||
impl Plugin for SurfaceMaterialPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(MaterialPlugin::<SurfaceMaterial>::default())
|
||||
.init_resource::<SurfaceMaterialCache>()
|
||||
.init_resource::<SurfaceDiagnostics>()
|
||||
.init_resource::<SurfaceEvaluatorRegistry>()
|
||||
.add_systems(Update, sync_surface_material_bindings);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
fn sync_surface_material_bindings(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut shaders: ResMut<Assets<Shader>>,
|
||||
mut standard_materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut surface_materials: ResMut<Assets<SurfaceMaterial>>,
|
||||
mut cache: ResMut<SurfaceMaterialCache>,
|
||||
mut diagnostics: ResMut<SurfaceDiagnostics>,
|
||||
mut evaluator_registry: ResMut<SurfaceEvaluatorRegistry>,
|
||||
bindings: Query<(
|
||||
Entity,
|
||||
&HydratedRendererMaterialBinding,
|
||||
Option<&MeshMaterial3d<StandardMaterial>>,
|
||||
Option<&MeshMaterial3d<SurfaceMaterial>>,
|
||||
)>,
|
||||
) {
|
||||
for (entity, binding, standard_handle, surface_handle) in &bindings {
|
||||
let Some(reference) = binding.effective_material.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(path) = reference.0.source_path.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let dependencies = cache
|
||||
.dependencies
|
||||
.get(reference)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| vec![path.to_string()]);
|
||||
let revision = dependency_revision(&dependencies);
|
||||
if cache.failed_revisions.get(reference) == Some(&revision) {
|
||||
continue;
|
||||
}
|
||||
if cache.standard_only.get(reference) == Some(&revision) {
|
||||
if let Some(handle) = cache.standard_handles.get(reference).cloned() {
|
||||
if standard_handle.is_none_or(|current| current.0 != handle)
|
||||
|| surface_handle.is_some()
|
||||
{
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<MeshMaterial3d<SurfaceMaterial>>()
|
||||
.insert(MeshMaterial3d(handle));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let stale = cache.revisions.get(reference) != Some(&revision);
|
||||
let handle = if !stale {
|
||||
cache.handles.get(reference).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let handle = match handle {
|
||||
Some(handle) => handle,
|
||||
None => match build_surface_material(
|
||||
reference,
|
||||
path,
|
||||
&asset_server,
|
||||
&mut shaders,
|
||||
&mut standard_materials,
|
||||
standard_handle,
|
||||
&mut evaluator_registry,
|
||||
) {
|
||||
Ok(BuiltRendererMaterial::Surface(material)) => {
|
||||
let material = *material;
|
||||
cache.standard_only.remove(reference);
|
||||
cache.standard_handles.remove(reference);
|
||||
cache.failed_revisions.remove(reference);
|
||||
if let Some(existing) = cache.handles.get(reference).cloned() {
|
||||
if let Some(mut slot) = surface_materials.get_mut(&existing) {
|
||||
*slot = material;
|
||||
}
|
||||
existing
|
||||
} else {
|
||||
let handle = surface_materials.add(material);
|
||||
cache.handles.insert(reference.clone(), handle.clone());
|
||||
handle
|
||||
}
|
||||
}
|
||||
Ok(BuiltRendererMaterial::Standard(material)) => {
|
||||
let material = *material;
|
||||
// Plain Material assets stay on Bevy's StandardMaterial path, while still
|
||||
// sharing one live-updated handle across every renderer slot.
|
||||
let handle = standard_handle
|
||||
.map(|handle| handle.0.clone())
|
||||
.or_else(|| cache.standard_handles.get(reference).cloned())
|
||||
.unwrap_or_else(|| standard_materials.add(material.clone()));
|
||||
if let Some(mut slot) = standard_materials.get_mut(&handle) {
|
||||
*slot = material;
|
||||
}
|
||||
cache.handles.remove(reference);
|
||||
cache.revisions.remove(reference);
|
||||
cache
|
||||
.standard_handles
|
||||
.insert(reference.clone(), handle.clone());
|
||||
let next_dependencies =
|
||||
material_dependency_paths(path).unwrap_or_else(|_| vec![path.to_string()]);
|
||||
let next_revision = dependency_revision(&next_dependencies);
|
||||
cache
|
||||
.dependencies
|
||||
.insert(reference.clone(), next_dependencies);
|
||||
cache.standard_only.insert(reference.clone(), next_revision);
|
||||
cache.failed_revisions.remove(reference);
|
||||
evaluator_registry
|
||||
.evaluators
|
||||
.remove(&stable_shader_id(reference));
|
||||
if standard_handle.is_none_or(|current| current.0 != handle)
|
||||
|| surface_handle.is_some()
|
||||
{
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<MeshMaterial3d<SurfaceMaterial>>()
|
||||
.insert(MeshMaterial3d(handle));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
cache.failed_revisions.insert(reference.clone(), revision);
|
||||
diagnostics.0.push(format!(
|
||||
"material {} could not build Surface ABI: {error}",
|
||||
reference.0.label
|
||||
));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
let next_dependencies =
|
||||
material_dependency_paths(path).unwrap_or_else(|_| vec![path.to_string()]);
|
||||
let next_revision = dependency_revision(&next_dependencies);
|
||||
cache
|
||||
.dependencies
|
||||
.insert(reference.clone(), next_dependencies);
|
||||
cache.revisions.insert(reference.clone(), next_revision);
|
||||
if surface_handle.is_none_or(|current| current.0 != handle) {
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<MeshMaterial3d<StandardMaterial>>()
|
||||
.insert(MeshMaterial3d(handle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_surface_material(
|
||||
reference: &MaterialRef,
|
||||
path: &str,
|
||||
asset_server: &AssetServer,
|
||||
shaders: &mut Assets<Shader>,
|
||||
standard_materials: &mut Assets<StandardMaterial>,
|
||||
existing_standard: Option<&MeshMaterial3d<StandardMaterial>>,
|
||||
evaluator_registry: &mut SurfaceEvaluatorRegistry,
|
||||
) -> Result<BuiltRendererMaterial, String> {
|
||||
let (mut asset, instance) = match MaterialAsset::load_from_path(path) {
|
||||
Ok(asset) => (asset, None),
|
||||
Err(_) => {
|
||||
let instance = MaterialInstanceAsset::load_from_path(path)?;
|
||||
if instance.schema_version != MATERIAL_INSTANCE_SCHEMA_VERSION {
|
||||
return Err(format!(
|
||||
"unsupported material-instance schema {}",
|
||||
instance.schema_version
|
||||
));
|
||||
}
|
||||
let base_path = instance
|
||||
.base
|
||||
.0
|
||||
.source_path
|
||||
.as_deref()
|
||||
.ok_or_else(|| "material instance base has no loadable path".to_string())?;
|
||||
(MaterialAsset::load_from_path(base_path)?, Some(instance))
|
||||
}
|
||||
};
|
||||
if let Some(instance) = instance.as_ref() {
|
||||
instance.apply_to(&mut asset.material);
|
||||
}
|
||||
let schema_path = asset
|
||||
.shader_ref
|
||||
.as_ref()
|
||||
.and_then(|reference| reference.source_path.as_deref())
|
||||
.or(asset.material.shader.schema_path.as_deref())
|
||||
.or(asset
|
||||
.shader
|
||||
.as_deref()
|
||||
.filter(|path| path.ends_with(".ron")));
|
||||
let schema = schema_path
|
||||
.map(ShaderSchemaAsset::load_from_path)
|
||||
.transpose()?;
|
||||
let evaluator_source = schema
|
||||
.as_ref()
|
||||
.and_then(|schema| schema.wgsl_path.as_deref())
|
||||
.map(fs::read_to_string)
|
||||
.transpose()
|
||||
.map_err(|error| format!("could not read surface WGSL: {error}"))?;
|
||||
|
||||
let mut base = existing_standard
|
||||
.and_then(|handle| standard_materials.get(&handle.0).cloned())
|
||||
.unwrap_or_else(|| material_from_desc(asset_server, &asset.material));
|
||||
base.alpha_mode = match asset.render_state.alpha_mode {
|
||||
shared::MaterialAlphaMode::Opaque => AlphaMode::Opaque,
|
||||
shared::MaterialAlphaMode::Cutout => AlphaMode::Mask(asset.render_state.alpha_cutoff),
|
||||
};
|
||||
base.cull_mode =
|
||||
(!asset.render_state.double_sided).then_some(bevy::render::render_resource::Face::Back);
|
||||
|
||||
let Some(evaluator) = evaluator_source.as_deref() else {
|
||||
return Ok(BuiltRendererMaterial::Standard(Box::new(base)));
|
||||
};
|
||||
validate_surface_evaluator(evaluator)?;
|
||||
let runtime_shader_id = stable_shader_id(reference);
|
||||
evaluator_registry.evaluators.insert(
|
||||
runtime_shader_id,
|
||||
SurfaceEvaluatorRecord {
|
||||
shader_id: runtime_shader_id,
|
||||
evaluator_source: evaluator.to_string(),
|
||||
revision: evaluator_revision(evaluator),
|
||||
alpha_mode: asset.render_state.alpha_mode,
|
||||
},
|
||||
);
|
||||
let virtual_path = format!("generated://surface/{}.wgsl", reference.0.asset_id);
|
||||
let shader_uuid = shader_uuid_for_material(reference);
|
||||
let shader_id = AssetId::<Shader>::Uuid { uuid: shader_uuid };
|
||||
shaders
|
||||
.insert(
|
||||
shader_id,
|
||||
Shader::from_wgsl(compose_surface_shader(evaluator), virtual_path),
|
||||
)
|
||||
.map_err(|error| format!("could not replace generated surface shader: {error}"))?;
|
||||
let shader = Handle::Uuid(shader_uuid, Default::default());
|
||||
|
||||
let mut extension = SurfaceExtension {
|
||||
shader,
|
||||
..default()
|
||||
};
|
||||
extension.uniform.alpha_cutoff = asset.render_state.alpha_cutoff;
|
||||
extension.uniform.shader_id = runtime_shader_id;
|
||||
if let Some(schema) = schema.as_ref() {
|
||||
pack_schema_values(
|
||||
&mut extension,
|
||||
schema,
|
||||
&asset.material,
|
||||
instance.as_ref(),
|
||||
asset_server,
|
||||
)?;
|
||||
}
|
||||
Ok(BuiltRendererMaterial::Surface(Box::new(ExtendedMaterial {
|
||||
base,
|
||||
extension,
|
||||
})))
|
||||
}
|
||||
|
||||
fn material_dependency_paths(path: &str) -> Result<Vec<String>, String> {
|
||||
let mut dependencies = vec![path.to_string()];
|
||||
let asset = match MaterialAsset::load_from_path(path) {
|
||||
Ok(asset) => asset,
|
||||
Err(_) => {
|
||||
let instance = MaterialInstanceAsset::load_from_path(path)?;
|
||||
let base_path = instance
|
||||
.base
|
||||
.0
|
||||
.source_path
|
||||
.as_deref()
|
||||
.ok_or_else(|| "material instance base has no loadable path".to_string())?;
|
||||
dependencies.push(base_path.to_string());
|
||||
MaterialAsset::load_from_path(base_path)?
|
||||
}
|
||||
};
|
||||
let schema_path = asset
|
||||
.shader_ref
|
||||
.as_ref()
|
||||
.and_then(|reference| reference.source_path.as_deref())
|
||||
.or(asset.material.shader.schema_path.as_deref())
|
||||
.or(asset
|
||||
.shader
|
||||
.as_deref()
|
||||
.filter(|path| path.ends_with(".ron")));
|
||||
if let Some(schema_path) = schema_path {
|
||||
dependencies.push(schema_path.to_string());
|
||||
if let Some(wgsl_path) = ShaderSchemaAsset::load_from_path(schema_path)?.wgsl_path {
|
||||
dependencies.push(wgsl_path);
|
||||
}
|
||||
}
|
||||
dependencies.sort();
|
||||
dependencies.dedup();
|
||||
Ok(dependencies)
|
||||
}
|
||||
|
||||
fn dependency_revision(paths: &[String]) -> u64 {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for path in paths {
|
||||
for byte in path.bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
match fs::metadata(path) {
|
||||
Ok(metadata) => {
|
||||
for byte in metadata.len().to_le_bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
|
||||
for byte in duration.as_nanos().to_le_bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
hash ^= u64::MAX;
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn stable_shader_id(reference: &MaterialRef) -> u32 {
|
||||
let mut hash = 0x811c9dc5u32;
|
||||
for byte in reference.0.asset_id.bytes() {
|
||||
hash ^= u32::from(byte);
|
||||
hash = hash.wrapping_mul(0x01000193);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn evaluator_revision(source: &str) -> u64 {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for byte in source.bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn shader_uuid_for_material(reference: &MaterialRef) -> uuid::Uuid {
|
||||
if let Ok(source) = uuid::Uuid::parse_str(&reference.0.asset_id) {
|
||||
let mixed = source.as_u128() ^ 0x7f8c_4e90_f9ab_4f28_9b44_47c1_b2d5_5ac1u128;
|
||||
return uuid::Uuid::from_u128(mixed);
|
||||
}
|
||||
let mut bytes = [0u8; 16];
|
||||
for (index, byte) in reference.0.asset_id.bytes().enumerate() {
|
||||
bytes[index % 16] = bytes[index % 16].wrapping_mul(31).wrapping_add(byte);
|
||||
}
|
||||
uuid::Uuid::from_bytes(bytes)
|
||||
}
|
||||
|
||||
fn pack_schema_values(
|
||||
extension: &mut SurfaceExtension,
|
||||
schema: &ShaderSchemaAsset,
|
||||
material: &shared::MaterialDesc,
|
||||
instance: Option<&MaterialInstanceAsset>,
|
||||
asset_server: &AssetServer,
|
||||
) -> Result<(), String> {
|
||||
let mut numeric_index = 0usize;
|
||||
let mut texture_index = 0usize;
|
||||
for property in &schema.parameters {
|
||||
if matches!(property.property_type, ShaderPropertyType::Texture) {
|
||||
if texture_index >= SURFACE_TEXTURE_SLOTS {
|
||||
return Err(format!(
|
||||
"surface schema exceeds {SURFACE_TEXTURE_SLOTS} textures"
|
||||
));
|
||||
}
|
||||
let binding = instance
|
||||
.and_then(|instance| {
|
||||
instance
|
||||
.textures
|
||||
.iter()
|
||||
.find(|value| value.name == property.name)
|
||||
})
|
||||
.or_else(|| {
|
||||
material
|
||||
.textures
|
||||
.iter()
|
||||
.find(|value| value.name == property.name)
|
||||
});
|
||||
let handle = binding
|
||||
.and_then(|binding| binding.texture.as_ref())
|
||||
.and_then(|reference| reference.source_path.as_deref())
|
||||
.map(asset_server_path)
|
||||
.map(|path| asset_server.load(path));
|
||||
extension.set_texture(texture_index, handle);
|
||||
texture_index += 1;
|
||||
continue;
|
||||
}
|
||||
if numeric_index >= SURFACE_PARAMETER_LANES {
|
||||
return Err(format!(
|
||||
"surface schema exceeds {SURFACE_PARAMETER_LANES} parameter lanes"
|
||||
));
|
||||
}
|
||||
let value = instance
|
||||
.and_then(|instance| {
|
||||
instance
|
||||
.parameters
|
||||
.iter()
|
||||
.find(|value| value.name == property.name)
|
||||
})
|
||||
.or_else(|| {
|
||||
material
|
||||
.parameters
|
||||
.iter()
|
||||
.find(|value| value.name == property.name)
|
||||
})
|
||||
.or_else(|| {
|
||||
schema
|
||||
.default_values
|
||||
.iter()
|
||||
.find(|value| value.name == property.name)
|
||||
});
|
||||
extension.uniform.params[numeric_index] = value
|
||||
.map(|value| parameter_lane(&value.value, &property.property_type))
|
||||
.transpose()?
|
||||
.unwrap_or(Vec4::ZERO);
|
||||
numeric_index += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parameter_lane(
|
||||
value: &MaterialParameterValue,
|
||||
property_type: &ShaderPropertyType,
|
||||
) -> Result<Vec4, String> {
|
||||
match (value, property_type) {
|
||||
(MaterialParameterValue::Bool(value), ShaderPropertyType::Bool) => {
|
||||
Ok(Vec4::new(u32::from(*value) as f32, 0.0, 0.0, 0.0))
|
||||
}
|
||||
(MaterialParameterValue::Float(value), ShaderPropertyType::Float { .. }) => {
|
||||
Ok(Vec4::new(*value, 0.0, 0.0, 0.0))
|
||||
}
|
||||
(MaterialParameterValue::Vec2(value), ShaderPropertyType::Vec2) => {
|
||||
Ok(Vec4::new(value.x, value.y, 0.0, 0.0))
|
||||
}
|
||||
(MaterialParameterValue::Vec3(value), ShaderPropertyType::Vec3) => Ok(value.extend(0.0)),
|
||||
(MaterialParameterValue::Color(value), ShaderPropertyType::Color) => {
|
||||
Ok(Vec4::new(value.r, value.g, value.b, value.a))
|
||||
}
|
||||
(MaterialParameterValue::Enum(value), ShaderPropertyType::Enum { options }) => {
|
||||
let index = options
|
||||
.iter()
|
||||
.position(|option| option == value)
|
||||
.ok_or_else(|| format!("enum value `{value}` is not declared by the schema"))?;
|
||||
Ok(Vec4::new(index as f32, 0.0, 0.0, 0.0))
|
||||
}
|
||||
_ => Err("material value does not match shader schema type".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_surface_evaluator(source: &str) -> Result<(), String> {
|
||||
if !source.contains("fn evaluate(") {
|
||||
return Err("surface WGSL must define `fn evaluate(`".into());
|
||||
}
|
||||
for forbidden in [
|
||||
"@group",
|
||||
"@binding",
|
||||
"@vertex",
|
||||
"@fragment",
|
||||
"@compute",
|
||||
"var<",
|
||||
"rayQuery",
|
||||
"dpdx",
|
||||
"dpdy",
|
||||
"fwidth",
|
||||
"discard",
|
||||
"Barrier",
|
||||
"subgroup",
|
||||
"textureStore",
|
||||
"atomic",
|
||||
] {
|
||||
if source.contains(forbidden) {
|
||||
return Err(format!(
|
||||
"surface WGSL uses forbidden construct `{forbidden}`"
|
||||
));
|
||||
}
|
||||
}
|
||||
let validation_source = format!("{EVALUATOR_VALIDATION_PRELUDE}\n{source}");
|
||||
let module = naga::front::wgsl::parse_str(&validation_source)
|
||||
.map_err(|error| format!("surface WGSL syntax error: {error}"))?;
|
||||
naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::empty(),
|
||||
)
|
||||
.validate(&module)
|
||||
.map_err(|error| format!("surface WGSL validation error: {error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const EVALUATOR_VALIDATION_PRELUDE: &str = r#"
|
||||
struct SurfaceInput {
|
||||
uv0: vec2<f32>,
|
||||
world_position: vec3<f32>,
|
||||
world_normal: vec3<f32>,
|
||||
}
|
||||
struct SurfaceParams { lanes: array<vec4<f32>, 16>, }
|
||||
struct SurfaceSamples { values: array<vec4<f32>, 8>, }
|
||||
struct Surface {
|
||||
base_color: vec4<f32>,
|
||||
normal_ts: vec3<f32>,
|
||||
emissive: vec3<f32>,
|
||||
metallic: f32,
|
||||
perceptual_roughness: f32,
|
||||
reflectance: f32,
|
||||
occlusion: f32,
|
||||
model: u32,
|
||||
}
|
||||
fn surface_default() -> Surface {
|
||||
var surface: Surface;
|
||||
surface.base_color = vec4<f32>(1.0);
|
||||
surface.normal_ts = vec3<f32>(0.0, 0.0, 1.0);
|
||||
surface.emissive = vec3<f32>(0.0);
|
||||
surface.metallic = 0.0;
|
||||
surface.perceptual_roughness = 0.5;
|
||||
surface.reflectance = 0.5;
|
||||
surface.occlusion = 1.0;
|
||||
surface.model = 0u;
|
||||
return surface;
|
||||
}
|
||||
"#;
|
||||
|
||||
pub fn compose_surface_shader(evaluator: &str) -> String {
|
||||
format!(
|
||||
"{}\n{}\n{}",
|
||||
include_str!("surface_header.wgsl"),
|
||||
evaluator,
|
||||
include_str!("surface_footer.wgsl")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_resource_bindings_and_missing_entry() {
|
||||
assert!(validate_surface_evaluator("fn nope() {}").is_err());
|
||||
assert!(validate_surface_evaluator(
|
||||
"@group(0) @binding(0) var t: texture_2d<f32>; fn evaluate() {}"
|
||||
)
|
||||
.is_err());
|
||||
for compute_incompatible in [
|
||||
"dpdx(input.uv0.x)",
|
||||
"dpdy(input.uv0.y)",
|
||||
"fwidth(input.uv0.x)",
|
||||
"discard",
|
||||
"workgroupBarrier()",
|
||||
"subgroupAdd(1u)",
|
||||
] {
|
||||
let source = format!(
|
||||
"fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface {{ let invalid = {compute_incompatible}; return surface_default(); }}"
|
||||
);
|
||||
assert!(
|
||||
validate_surface_evaluator(&source).is_err(),
|
||||
"accepted compute-incompatible evaluator: {compute_incompatible}"
|
||||
);
|
||||
}
|
||||
assert!(validate_surface_evaluator(
|
||||
"fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface { return missing_symbol; }"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_surface_only_module_and_composes_wrapper() {
|
||||
let source = "fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface { return surface_default(); }";
|
||||
validate_surface_evaluator(source).unwrap();
|
||||
let composed = compose_surface_shader(source);
|
||||
assert!(composed.contains(source));
|
||||
assert!(composed.contains("@fragment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_limits_are_stable() {
|
||||
assert_eq!(SURFACE_PARAMETER_LANES, 16);
|
||||
assert_eq!(SURFACE_TEXTURE_SLOTS, 8);
|
||||
assert_eq!(std::mem::size_of::<SurfaceUniform>(), 400);
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
#ifdef VERTEX_UVS_A
|
||||
let uv0 = in.uv;
|
||||
#else
|
||||
let uv0 = vec2<f32>(0.0);
|
||||
#endif
|
||||
let input = SurfaceInput(uv0, pbr_input.world_position.xyz, pbr_input.N);
|
||||
let params = SurfaceParams(surface_uniform.params);
|
||||
let surface = evaluate(input, params, surface_samples(uv0));
|
||||
pbr_input.material.base_color = surface.base_color;
|
||||
pbr_input.material.emissive = vec4<f32>(surface.emissive, 1.0);
|
||||
pbr_input.material.metallic = clamp(surface.metallic, 0.0, 1.0);
|
||||
pbr_input.material.perceptual_roughness = clamp(surface.perceptual_roughness, 0.001, 1.0);
|
||||
pbr_input.material.reflectance = vec3<f32>(clamp(surface.reflectance, 0.0, 1.0));
|
||||
pbr_input.diffuse_occlusion = vec3<f32>(clamp(surface.occlusion, 0.0, 1.0));
|
||||
#ifdef VERTEX_TANGENTS
|
||||
let surface_tbn = calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent);
|
||||
let surface_nt = normalize(surface.normal_ts);
|
||||
pbr_input.N = normalize(
|
||||
surface_nt.x * surface_tbn[0]
|
||||
+ surface_nt.y * surface_tbn[1]
|
||||
+ surface_nt.z * surface_tbn[2]
|
||||
);
|
||||
#endif
|
||||
if surface.model == BLACKSITE_SURFACE_UNLIT {
|
||||
pbr_input.material.flags |= STANDARD_MATERIAL_FLAGS_UNLIT_BIT;
|
||||
} else {
|
||||
pbr_input.material.flags &= ~STANDARD_MATERIAL_FLAGS_UNLIT_BIT;
|
||||
}
|
||||
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
||||
#ifdef PREPASS_PIPELINE
|
||||
return deferred_output(in, pbr_input);
|
||||
#else
|
||||
var out: FragmentOutput;
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
#import bevy_pbr::{
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
pbr_functions::{alpha_discard, calculate_tbn_mikktspace},
|
||||
}
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::{
|
||||
prepass_io::{VertexOutput, FragmentOutput},
|
||||
pbr_deferred_functions::deferred_output,
|
||||
}
|
||||
#else
|
||||
#import bevy_pbr::{
|
||||
forward_io::{VertexOutput, FragmentOutput},
|
||||
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
|
||||
}
|
||||
#endif
|
||||
|
||||
const BLACKSITE_SURFACE_ABI_VERSION: u32 = 1u;
|
||||
const BLACKSITE_SURFACE_UNLIT: u32 = 1u;
|
||||
const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 1u << 5u;
|
||||
|
||||
struct SurfaceUniform {
|
||||
shader_id: u32,
|
||||
flags: u32,
|
||||
alpha_cutoff: f32,
|
||||
abi_version: u32,
|
||||
params: array<vec4<f32>, 16>,
|
||||
uv_transforms: array<vec4<f32>, 8>,
|
||||
}
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> surface_uniform: SurfaceUniform;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var surface_texture0: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var surface_sampler0: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var surface_texture1: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var surface_sampler1: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(105) var surface_texture2: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(106) var surface_sampler2: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(107) var surface_texture3: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(108) var surface_sampler3: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(109) var surface_texture4: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(110) var surface_sampler4: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(111) var surface_texture5: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(112) var surface_sampler5: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(113) var surface_texture6: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(114) var surface_sampler6: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(115) var surface_texture7: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(116) var surface_sampler7: sampler;
|
||||
|
||||
struct SurfaceInput {
|
||||
uv0: vec2<f32>,
|
||||
world_position: vec3<f32>,
|
||||
world_normal: vec3<f32>,
|
||||
}
|
||||
|
||||
struct SurfaceParams {
|
||||
lanes: array<vec4<f32>, 16>,
|
||||
}
|
||||
|
||||
struct SurfaceSamples {
|
||||
values: array<vec4<f32>, 8>,
|
||||
}
|
||||
|
||||
struct Surface {
|
||||
base_color: vec4<f32>,
|
||||
normal_ts: vec3<f32>,
|
||||
emissive: vec3<f32>,
|
||||
metallic: f32,
|
||||
perceptual_roughness: f32,
|
||||
reflectance: f32,
|
||||
occlusion: f32,
|
||||
model: u32,
|
||||
}
|
||||
|
||||
fn surface_default() -> Surface {
|
||||
var surface: Surface;
|
||||
surface.base_color = vec4<f32>(1.0);
|
||||
surface.normal_ts = vec3<f32>(0.0, 0.0, 1.0);
|
||||
surface.emissive = vec3<f32>(0.0);
|
||||
surface.metallic = 0.0;
|
||||
surface.perceptual_roughness = 0.5;
|
||||
surface.reflectance = 0.5;
|
||||
surface.occlusion = 1.0;
|
||||
surface.model = 0u;
|
||||
return surface;
|
||||
}
|
||||
|
||||
fn surface_uv(index: u32, uv: vec2<f32>) -> vec2<f32> {
|
||||
let transform = surface_uniform.uv_transforms[index];
|
||||
return uv * transform.xy + transform.zw;
|
||||
}
|
||||
|
||||
fn surface_samples(uv: vec2<f32>) -> SurfaceSamples {
|
||||
var samples: SurfaceSamples;
|
||||
samples.values[0] = textureSample(surface_texture0, surface_sampler0, surface_uv(0u, uv));
|
||||
samples.values[1] = textureSample(surface_texture1, surface_sampler1, surface_uv(1u, uv));
|
||||
samples.values[2] = textureSample(surface_texture2, surface_sampler2, surface_uv(2u, uv));
|
||||
samples.values[3] = textureSample(surface_texture3, surface_sampler3, surface_uv(3u, uv));
|
||||
samples.values[4] = textureSample(surface_texture4, surface_sampler4, surface_uv(4u, uv));
|
||||
samples.values[5] = textureSample(surface_texture5, surface_sampler5, surface_uv(5u, uv));
|
||||
samples.values[6] = textureSample(surface_texture6, surface_sampler6, surface_uv(6u, uv));
|
||||
samples.values[7] = textureSample(surface_texture7, surface_sampler7, surface_uv(7u, uv));
|
||||
return samples;
|
||||
}
|
||||
@ -33,7 +33,6 @@ egui_phosphor_icons.workspace = true
|
||||
bevy_ufbx.workspace = true
|
||||
ufbx = "0.9"
|
||||
game.workspace = true
|
||||
polyanya.workspace = true
|
||||
game_hot.workspace = true
|
||||
hot-lib-reloader = { version = "0.8.2", optional = true }
|
||||
notify = { version = "6.1", optional = true }
|
||||
|
||||
@ -210,13 +210,6 @@ fn build_gltf_manifest(
|
||||
});
|
||||
}
|
||||
|
||||
let default_animation_clip_id = record.import_settings.default_animation_clip_id.clone();
|
||||
validate_default_animation_clip(
|
||||
default_animation_clip_id.as_deref(),
|
||||
&clips,
|
||||
&mut diagnostics,
|
||||
);
|
||||
|
||||
let runtime_supported = animation_roots.len() <= 1;
|
||||
if !runtime_supported {
|
||||
let roots = animation_roots
|
||||
@ -239,7 +232,6 @@ fn build_gltf_manifest(
|
||||
schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: record.id.as_string(),
|
||||
label: record.label.clone(),
|
||||
default_animation_clip_id,
|
||||
source: AnimationManifestSource {
|
||||
path: record.path.clone(),
|
||||
format,
|
||||
@ -321,22 +313,15 @@ fn build_fbx_manifest(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (runtime_supported, mut diagnostics) = fbx_runtime_support(
|
||||
let (runtime_supported, diagnostics) = fbx_runtime_support(
|
||||
scene.anim_stacks.as_ref().len(),
|
||||
scene.skin_deformers.as_ref().len(),
|
||||
);
|
||||
let default_animation_clip_id = record.import_settings.default_animation_clip_id.clone();
|
||||
validate_default_animation_clip(
|
||||
default_animation_clip_id.as_deref(),
|
||||
&clips,
|
||||
&mut diagnostics,
|
||||
);
|
||||
|
||||
Ok(AnimationManifest {
|
||||
schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: record.id.as_string(),
|
||||
label: record.label.clone(),
|
||||
default_animation_clip_id,
|
||||
source: AnimationManifestSource {
|
||||
path: record.path.clone(),
|
||||
format,
|
||||
@ -350,27 +335,6 @@ fn build_fbx_manifest(
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_default_animation_clip(
|
||||
default_clip_id: Option<&str>,
|
||||
clips: &[AnimationClipRecord],
|
||||
diagnostics: &mut Vec<AnimationImportDiagnostic>,
|
||||
) {
|
||||
let Some(default_clip_id) = default_clip_id.filter(|id| !id.trim().is_empty()) else {
|
||||
return;
|
||||
};
|
||||
if clips.iter().any(|clip| clip.id == default_clip_id) {
|
||||
return;
|
||||
}
|
||||
diagnostics.push(AnimationImportDiagnostic {
|
||||
severity: AnimationDiagnosticSeverity::Error,
|
||||
code: "animation.default_clip_missing".into(),
|
||||
message: format!(
|
||||
"configured default animation clip `{default_clip_id}` no longer exists in the imported source"
|
||||
),
|
||||
repair: "Choose an existing Default animation in Model Import Settings, or select Imported rest pose, then reimport.".into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn fbx_runtime_support(
|
||||
animation_count: usize,
|
||||
skin_count: usize,
|
||||
@ -732,8 +696,7 @@ mod tests {
|
||||
let second = build_animation_manifest(&record).unwrap();
|
||||
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(first.schema_version, ANIMATION_MANIFEST_SCHEMA_VERSION);
|
||||
assert!(first.default_animation_clip_id.is_none());
|
||||
assert_eq!(first.schema_version, 2);
|
||||
assert!(first.runtime_supported);
|
||||
assert_eq!(first.source.dependencies.len(), 1);
|
||||
assert_eq!(first.skeletons.len(), 1);
|
||||
@ -754,32 +717,6 @@ mod tests {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_default_clip_is_stable_and_stale_ids_are_diagnostic() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("blacksite-animation-default-{}", Uuid::new_v4()));
|
||||
let path = write_animated_gltf(&root);
|
||||
let mut record = fixture_record(&path);
|
||||
record.import_settings.default_animation_clip_id =
|
||||
Some("animation:clip:0:idle_loop".into());
|
||||
|
||||
let manifest = build_animation_manifest(&record).unwrap();
|
||||
assert_eq!(
|
||||
manifest.default_animation_clip_id.as_deref(),
|
||||
Some("animation:clip:0:idle_loop")
|
||||
);
|
||||
assert!(manifest.diagnostics.is_empty());
|
||||
|
||||
record.import_settings.default_animation_clip_id = Some("animation:clip:99:removed".into());
|
||||
let stale = build_animation_manifest(&record).unwrap();
|
||||
assert!(stale.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.code == "animation.default_clip_missing"
|
||||
&& diagnostic.severity == AnimationDiagnosticSeverity::Error
|
||||
}));
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_signature_changes_with_joint_path_or_bind_pose() {
|
||||
let identity = identity_matrix_bytes();
|
||||
|
||||
@ -27,12 +27,8 @@ impl Default for AssetId {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ModelPlacementMode {
|
||||
/// Place renderable content through the appropriate normalized renderer. Imports containing
|
||||
/// skins or animation route to `SkinnedMeshRenderer`; unrigged, non-animated content routes to
|
||||
/// `StaticMeshRenderer`.
|
||||
#[default]
|
||||
StaticAsset,
|
||||
/// Instantiate the complete source scene as a generic imported model.
|
||||
SceneInstance,
|
||||
}
|
||||
|
||||
@ -65,10 +61,6 @@ pub struct ImportSettings {
|
||||
pub static_mesh_manifest_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub animation_manifest_path: Option<String>,
|
||||
/// Stable animation clip sub-asset ID used as this model's edit-mode rest presentation.
|
||||
/// `None` preserves the imported node pose and never guesses a clip.
|
||||
#[serde(default)]
|
||||
pub default_animation_clip_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ImportSettings {
|
||||
@ -82,7 +74,6 @@ impl Default for ImportSettings {
|
||||
material_policy: MaterialImportPolicy::default(),
|
||||
static_mesh_manifest_path: None,
|
||||
animation_manifest_path: None,
|
||||
default_animation_clip_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -345,7 +336,6 @@ mod tests {
|
||||
schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: id.as_string(),
|
||||
label: "Moved Model".into(),
|
||||
default_animation_clip_id: None,
|
||||
source: AnimationManifestSource {
|
||||
path: old_path.clone(),
|
||||
format: "glb".into(),
|
||||
|
||||
@ -524,7 +524,6 @@ fn empty_snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> EditorEnt
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -543,18 +542,8 @@ fn empty_snapshot_for_asset(asset: &EditorAsset, translation: Vec3) -> EditorEnt
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
@ -683,33 +672,17 @@ pub fn spawn_asset_at(world: &mut World, asset: &EditorAsset, translation: Vec3)
|
||||
record.import_settings.placement_mode,
|
||||
ModelPlacementMode::StaticAsset
|
||||
) {
|
||||
if let Some((renderer, requires_skinned_renderer)) =
|
||||
static_mesh_renderer_for_asset(world, path)
|
||||
{
|
||||
if requires_skinned_renderer {
|
||||
apply_skinned_mesh_placement_mode_with_materials(
|
||||
&mut snapshot,
|
||||
renderer.materials.clone(),
|
||||
);
|
||||
match default_animation_controller_for_record(&record) {
|
||||
Ok(controller) => snapshot.animation_controller = controller,
|
||||
Err(error) => warn!(
|
||||
"Default animation was not applied while placing {}: {error}",
|
||||
record.path
|
||||
),
|
||||
}
|
||||
} else {
|
||||
let collider = record
|
||||
.import_settings
|
||||
.generate_collider
|
||||
.then(|| static_mesh_collider_for_renderer(&renderer));
|
||||
apply_static_mesh_placement_mode(
|
||||
&mut snapshot,
|
||||
renderer,
|
||||
collider,
|
||||
record.import_settings.hierarchy_mode,
|
||||
);
|
||||
}
|
||||
if let Some(renderer) = static_mesh_renderer_for_asset(world, path) {
|
||||
let collider = record
|
||||
.import_settings
|
||||
.generate_collider
|
||||
.then(|| static_mesh_collider_for_renderer(&renderer));
|
||||
apply_static_mesh_placement_mode(
|
||||
&mut snapshot,
|
||||
renderer,
|
||||
collider,
|
||||
record.import_settings.hierarchy_mode,
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
"Static mesh placement fell back to SceneRoot for {} because no renderer manifest was available",
|
||||
@ -787,11 +760,6 @@ pub fn spawn_subasset_at(
|
||||
.parts
|
||||
.iter()
|
||||
.find(|part| part_effective_id_for_selection(part) == *sub_asset_id)?;
|
||||
let part_requires_skinned_renderer = manifest.metadata.animation_count > 0
|
||||
|| part.skinned
|
||||
|| (manifest.schema_version < 2
|
||||
&& crate::assets::gltf_skinned_primitive_labels(parent_path)
|
||||
.contains(&part.mesh_label));
|
||||
|
||||
let mesh_ref = shared::EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
@ -816,10 +784,6 @@ pub fn spawn_subasset_at(
|
||||
id: shared::ComponentInstanceId::new("slot:0"),
|
||||
name: label.clone(),
|
||||
mesh: mesh_ref.clone(),
|
||||
material_slot_id: shared::ComponentInstanceId::new(format!(
|
||||
"slot:{}",
|
||||
part_effective_id_for_selection(part)
|
||||
)),
|
||||
material,
|
||||
local_transform: part.local_transform,
|
||||
visible: true,
|
||||
@ -836,7 +800,6 @@ pub fn spawn_subasset_at(
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(slot)),
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -855,41 +818,11 @@ pub fn spawn_subasset_at(
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
snapshot.transform.scale *= record.import_settings.scale;
|
||||
if part_requires_skinned_renderer {
|
||||
snapshot.actor_kind = ActorKind::SkinnedMesh;
|
||||
snapshot.static_mesh_renderer = None;
|
||||
snapshot.skinned_mesh_renderer = Some(shared::SkinnedMeshRenderer {
|
||||
materials: crate::assets::static_mesh::renderer_materials_from_manifest(
|
||||
&manifest,
|
||||
&record.import_settings,
|
||||
),
|
||||
..shared::SkinnedMeshRenderer::new(parent_path.clone())
|
||||
.with_asset_id(record.id.as_string())
|
||||
});
|
||||
match default_animation_controller_for_record(&record) {
|
||||
Ok(controller) => snapshot.animation_controller = controller,
|
||||
Err(error) => warn!(
|
||||
"Default animation was not applied while placing {}: {error}",
|
||||
record.path
|
||||
),
|
||||
}
|
||||
return Some(spawn_with_history(world, snapshot));
|
||||
}
|
||||
if record.import_settings.generate_collider {
|
||||
snapshot.rigid_body = Some(RigidBodyDesc::default());
|
||||
snapshot.collider = Some(ColliderDesc::static_mesh(vec![mesh_ref]));
|
||||
@ -907,7 +840,6 @@ pub struct AnimationClipAuthoringData {
|
||||
pub skeleton: shared::EditorAssetRef,
|
||||
pub skeleton_signature: shared::AnimationSkeletonSignature,
|
||||
pub state: shared::AnimationStateDesc,
|
||||
pub renderer_materials: shared::RendererMaterialSet,
|
||||
}
|
||||
|
||||
pub fn animation_clip_authoring_data(
|
||||
@ -973,18 +905,6 @@ pub fn animation_clip_authoring_data(
|
||||
let clip_ref = shared::EditorAssetRef::new(asset_id, clip.id.clone(), clip.label.clone())
|
||||
.with_source_path(parent_path);
|
||||
let state_id = animation_state_id(&clip.label, clip.source_index);
|
||||
let renderer_materials = record
|
||||
.import_settings
|
||||
.static_mesh_manifest_path
|
||||
.as_deref()
|
||||
.and_then(|path| load_static_mesh_manifest(path).ok())
|
||||
.map(|render_manifest| {
|
||||
crate::assets::static_mesh::renderer_materials_from_manifest(
|
||||
&render_manifest,
|
||||
&record.import_settings,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(AnimationClipAuthoringData {
|
||||
model_asset_id: record.id.as_string(),
|
||||
model_path: parent_path.clone(),
|
||||
@ -1000,7 +920,6 @@ pub fn animation_clip_authoring_data(
|
||||
speed: 1.0,
|
||||
range: shared::AnimationPlaybackRange::default(),
|
||||
},
|
||||
renderer_materials,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1038,7 +957,7 @@ pub fn animation_skeleton_signature_for_ref(
|
||||
|
||||
pub fn compatible_animation_skeleton_for_model(
|
||||
world: &World,
|
||||
model: &shared::SkinnedMeshRenderer,
|
||||
model: &ModelRef,
|
||||
signature: &shared::AnimationSkeletonSignature,
|
||||
) -> Result<shared::EditorAssetRef, String> {
|
||||
let record = world
|
||||
@ -1098,82 +1017,6 @@ fn animation_state_id(label: &str, source_index: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_animation_controller_for_record(
|
||||
record: &crate::asset_db::AssetRecord,
|
||||
) -> Result<Option<shared::AnimationControllerDesc>, String> {
|
||||
let Some(manifest_path) = record.import_settings.animation_manifest_path.as_deref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let manifest = load_animation_manifest(manifest_path)?;
|
||||
if manifest.default_animation_clip_id != record.import_settings.default_animation_clip_id {
|
||||
return Err(
|
||||
"model import settings and generated animation manifest disagree; reimport the model"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let Some(default_clip_id) = manifest
|
||||
.default_animation_clip_id
|
||||
.as_deref()
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !manifest.runtime_supported {
|
||||
return Err(format!(
|
||||
"configured clip `{default_clip_id}` belongs to a model unsupported by the animation runtime"
|
||||
));
|
||||
}
|
||||
let clip = manifest
|
||||
.clips
|
||||
.iter()
|
||||
.find(|clip| clip.id == default_clip_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"configured clip `{default_clip_id}` is missing from {manifest_path}; choose an existing clip or Imported rest pose"
|
||||
)
|
||||
})?;
|
||||
let signature = clip.target_skeleton_signature.as_ref().ok_or_else(|| {
|
||||
format!(
|
||||
"configured clip `{}` has no uniquely resolved target skeleton",
|
||||
clip.label
|
||||
)
|
||||
})?;
|
||||
let skeleton = manifest
|
||||
.skeletons
|
||||
.iter()
|
||||
.find(|skeleton| &skeleton.signature == signature)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"configured clip `{}` targets skeleton `{}` but no exact match exists",
|
||||
clip.label, signature.0
|
||||
)
|
||||
})?;
|
||||
let state_id = animation_state_id(&clip.label, clip.source_index);
|
||||
let source_path = manifest.source.path.clone();
|
||||
let asset_id = manifest.asset_id.clone();
|
||||
Ok(Some(shared::AnimationControllerDesc {
|
||||
skeleton: Some(
|
||||
shared::EditorAssetRef::new(
|
||||
asset_id.clone(),
|
||||
skeleton.id.clone(),
|
||||
skeleton.label.clone(),
|
||||
)
|
||||
.with_source_path(&source_path),
|
||||
),
|
||||
states: vec![shared::AnimationStateDesc {
|
||||
id: state_id.clone(),
|
||||
label: clip.label.clone(),
|
||||
clip: shared::EditorAssetRef::new(asset_id, clip.id.clone(), clip.label.clone())
|
||||
.with_source_path(source_path),
|
||||
looping: true,
|
||||
speed: 1.0,
|
||||
range: shared::AnimationPlaybackRange::default(),
|
||||
}],
|
||||
default_state: state_id,
|
||||
..default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn spawn_animation_clip_at(
|
||||
world: &mut World,
|
||||
selection: &AssetSelection,
|
||||
@ -1192,7 +1035,7 @@ fn spawn_animation_clip_at(
|
||||
let default_state = authored.state.id.clone();
|
||||
let snapshot = EditorEntitySnapshot {
|
||||
actor_id: None,
|
||||
actor_kind: ActorKind::SkinnedMesh,
|
||||
actor_kind: ActorKind::ImportedModel,
|
||||
actor_name: None,
|
||||
name: Some(authored.model_label),
|
||||
transform: Transform {
|
||||
@ -1203,11 +1046,6 @@ fn spawn_animation_clip_at(
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: Some(shared::SkinnedMeshRenderer {
|
||||
materials: authored.renderer_materials,
|
||||
..shared::SkinnedMeshRenderer::new(authored.model_path)
|
||||
.with_asset_id(authored.model_asset_id)
|
||||
}),
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -1223,7 +1061,7 @@ fn spawn_animation_clip_at(
|
||||
audio_source: None,
|
||||
audio_listener: None,
|
||||
player_spawn: false,
|
||||
model: None,
|
||||
model: Some(ModelRef::new(authored.model_path).with_asset_id(authored.model_asset_id)),
|
||||
prefab: None,
|
||||
prefab_instance: None,
|
||||
weapon_spawn: None,
|
||||
@ -1231,18 +1069,8 @@ fn spawn_animation_clip_at(
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
Some(spawn_with_history(world, snapshot))
|
||||
@ -1290,7 +1118,6 @@ fn apply_static_mesh_placement_mode(
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(entry)),
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: collider.as_ref().map(|_| RigidBodyDesc::default()),
|
||||
@ -1309,18 +1136,8 @@ fn apply_static_mesh_placement_mode(
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: index as i32,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
})
|
||||
@ -1329,35 +1146,6 @@ fn apply_static_mesh_placement_mode(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_skinned_mesh_placement_mode(snapshot: &mut EditorEntitySnapshot) {
|
||||
apply_skinned_mesh_placement_mode_with_materials(
|
||||
snapshot,
|
||||
shared::RendererMaterialSet::default(),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_skinned_mesh_placement_mode_with_materials(
|
||||
snapshot: &mut EditorEntitySnapshot,
|
||||
materials: shared::RendererMaterialSet,
|
||||
) {
|
||||
let Some(model) = snapshot.model.take() else {
|
||||
return;
|
||||
};
|
||||
snapshot.actor_kind = ActorKind::SkinnedMesh;
|
||||
snapshot.static_mesh_renderer = None;
|
||||
snapshot.skinned_mesh_renderer = Some(shared::SkinnedMeshRenderer {
|
||||
asset_id: model.asset_id,
|
||||
path: model.path,
|
||||
scene_index: model.scene_index,
|
||||
materials,
|
||||
});
|
||||
// Triangle-mesh colliders are a static-geometry concern. Animated collision must be authored
|
||||
// explicitly (capsules, hitboxes, or another gameplay-specific representation).
|
||||
snapshot.rigid_body = None;
|
||||
snapshot.collider = None;
|
||||
}
|
||||
|
||||
fn static_mesh_collider_for_renderer(renderer: &shared::StaticMeshRenderer) -> ColliderDesc {
|
||||
ColliderDesc::static_mesh(
|
||||
renderer
|
||||
@ -1389,26 +1177,14 @@ fn part_effective_material_id_for_selection(
|
||||
fn static_mesh_renderer_for_asset(
|
||||
world: &mut World,
|
||||
path: &str,
|
||||
) -> Option<(shared::StaticMeshRenderer, bool)> {
|
||||
) -> Option<shared::StaticMeshRenderer> {
|
||||
let mut manifest_path = None;
|
||||
if let Some(mut registry) = world.get_resource_mut::<AssetRegistry>() {
|
||||
let mut renderer = None;
|
||||
if let Some(record) = find_asset_mut_by_path(&mut registry, path) {
|
||||
match super::refresh_model_artifacts(record) {
|
||||
Ok(manifest) => {
|
||||
let requires_skinned_renderer = manifest.metadata.animation_count > 0
|
||||
|| manifest.metadata.skin_count > 0
|
||||
|| manifest.parts.iter().any(|part| part.skinned);
|
||||
let mut authored_renderer =
|
||||
renderer_from_manifest(&manifest, &record.import_settings);
|
||||
if requires_skinned_renderer {
|
||||
authored_renderer.materials =
|
||||
crate::assets::static_mesh::renderer_materials_from_manifest(
|
||||
&manifest,
|
||||
&record.import_settings,
|
||||
);
|
||||
}
|
||||
renderer = Some((authored_renderer, requires_skinned_renderer));
|
||||
renderer = Some(renderer_from_manifest(&manifest, &record.import_settings));
|
||||
registry.index_dirty = true;
|
||||
}
|
||||
Err(error) => {
|
||||
@ -1431,17 +1207,7 @@ fn static_mesh_renderer_for_asset(
|
||||
let registry = world.get_resource::<AssetRegistry>()?;
|
||||
let record = find_asset_by_path(registry, path)?;
|
||||
let manifest = load_static_mesh_manifest(&manifest_path).ok()?;
|
||||
let requires_skinned_renderer = manifest.metadata.animation_count > 0
|
||||
|| manifest.metadata.skin_count > 0
|
||||
|| manifest.parts.iter().any(|part| part.skinned);
|
||||
let mut renderer = renderer_from_manifest(&manifest, &record.import_settings);
|
||||
if requires_skinned_renderer {
|
||||
renderer.materials = crate::assets::static_mesh::renderer_materials_from_manifest(
|
||||
&manifest,
|
||||
&record.import_settings,
|
||||
);
|
||||
}
|
||||
Some((renderer, requires_skinned_renderer))
|
||||
Some(renderer_from_manifest(&manifest, &record.import_settings))
|
||||
}
|
||||
|
||||
pub fn import_external_assets(paths: &[PathBuf]) -> Result<usize, String> {
|
||||
@ -1582,106 +1348,4 @@ mod tests {
|
||||
);
|
||||
assert!(world.resource::<crate::history::EditorHistory>().can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skinned_model_placement_replaces_static_and_generic_model_renderers() {
|
||||
let asset = EditorAsset {
|
||||
label: "Hero".into(),
|
||||
path: Some("assets/models/hero.glb".into()),
|
||||
folder_path: "assets/models".into(),
|
||||
kind: EditorAssetKind::Model,
|
||||
};
|
||||
let mut snapshot = snapshot_for_asset(&asset, Vec3::ZERO).unwrap();
|
||||
snapshot.model.as_mut().unwrap().asset_id = "hero-id".into();
|
||||
snapshot.static_mesh_renderer = Some(shared::StaticMeshRenderer::default());
|
||||
snapshot.rigid_body = Some(RigidBodyDesc::default());
|
||||
snapshot.collider = Some(ColliderDesc::static_cuboid(Vec3::ONE));
|
||||
|
||||
apply_skinned_mesh_placement_mode(&mut snapshot);
|
||||
|
||||
assert_eq!(snapshot.actor_kind, ActorKind::SkinnedMesh);
|
||||
assert!(snapshot.model.is_none());
|
||||
assert!(snapshot.static_mesh_renderer.is_none());
|
||||
assert!(snapshot.rigid_body.is_none());
|
||||
assert!(snapshot.collider.is_none());
|
||||
assert_eq!(
|
||||
snapshot.skinned_mesh_renderer,
|
||||
Some(
|
||||
shared::SkinnedMeshRenderer::new("assets/models/hero.glb").with_asset_id("hero-id")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_default_clip_builds_one_explicit_controller_state() {
|
||||
let key = Uuid::new_v4();
|
||||
let manifest_path =
|
||||
std::env::temp_dir().join(format!("blacksite-default-animation-{key}.animation.ron"));
|
||||
let asset_id = key.to_string();
|
||||
let signature = shared::AnimationSkeletonSignature::new("rig-signature");
|
||||
let clip_id = shared::animation_clip_sub_asset_id(8, "Standing");
|
||||
let manifest = shared::AnimationManifest {
|
||||
schema_version: shared::ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: asset_id.clone(),
|
||||
label: "Robot".into(),
|
||||
default_animation_clip_id: Some(clip_id.clone()),
|
||||
source: shared::AnimationManifestSource {
|
||||
path: "assets/models/robot.glb".into(),
|
||||
format: "glb".into(),
|
||||
fingerprint: shared::AnimationSourceFingerprint {
|
||||
byte_len: 1,
|
||||
modified_unix_secs: 0,
|
||||
content_hash: "fixture".into(),
|
||||
},
|
||||
dependencies: Vec::new(),
|
||||
},
|
||||
runtime_supported: true,
|
||||
skeletons: vec![shared::AnimationSkeletonRecord {
|
||||
id: shared::animation_skeleton_sub_asset_id(0, "Rig"),
|
||||
label: "Rig".into(),
|
||||
source_index: 0,
|
||||
signature: signature.clone(),
|
||||
joint_paths: vec!["Root".into()],
|
||||
}],
|
||||
clips: vec![shared::AnimationClipRecord {
|
||||
id: clip_id.clone(),
|
||||
label: "Standing".into(),
|
||||
source_index: 8,
|
||||
duration_seconds: 0.4,
|
||||
target_skeleton_signature: Some(signature),
|
||||
events: Vec::new(),
|
||||
}],
|
||||
diagnostics: Vec::new(),
|
||||
};
|
||||
std::fs::write(
|
||||
&manifest_path,
|
||||
ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let record = AssetRecord {
|
||||
id: AssetId(key),
|
||||
path: "assets/models/robot.glb".into(),
|
||||
label: "Robot".into(),
|
||||
kind_tag: "Model".into(),
|
||||
import_settings: crate::asset_db::ImportSettings {
|
||||
animation_manifest_path: Some(manifest_path.to_string_lossy().into_owned()),
|
||||
default_animation_clip_id: Some(clip_id.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
dependencies: Vec::new(),
|
||||
};
|
||||
|
||||
let controller = default_animation_controller_for_record(&record)
|
||||
.unwrap()
|
||||
.expect("explicit default should produce a controller");
|
||||
assert_eq!(controller.default_state, "standing");
|
||||
assert_eq!(controller.states.len(), 1);
|
||||
assert_eq!(controller.states[0].clip.sub_asset_id, clip_id);
|
||||
assert_eq!(
|
||||
controller.skeleton.unwrap().sub_asset_id,
|
||||
shared::animation_skeleton_sub_asset_id(0, "Rig")
|
||||
);
|
||||
|
||||
std::fs::remove_file(manifest_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,10 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::egui;
|
||||
use shared::{
|
||||
ComponentInstanceId, LevelObject, MaterialAsset, MaterialDesc, MaterialRef,
|
||||
RendererMaterialSlot,
|
||||
};
|
||||
use shared::{LevelObject, MaterialAsset, MaterialDesc};
|
||||
|
||||
use crate::assets::{EditorAsset, EditorAssetKind, EditorAssets};
|
||||
use crate::history::{reflected_component_transaction, set_material_group_with_history};
|
||||
use crate::history::set_material_group_with_history;
|
||||
use crate::ui::inspector::property_row;
|
||||
|
||||
pub fn load_material_asset(catalog_path: &str) -> Result<MaterialAsset, String> {
|
||||
@ -31,105 +28,18 @@ pub fn apply_material_asset_to_selection(
|
||||
.path
|
||||
.clone()
|
||||
.ok_or_else(|| format!("Material has no source path: {}", asset.label))?;
|
||||
let record = world
|
||||
.get_resource::<crate::asset_db::AssetRegistry>()
|
||||
.and_then(|registry| crate::asset_db::find_asset_by_path(registry, &path))
|
||||
.ok_or_else(|| format!("Material is missing from the asset registry: {path}"))?;
|
||||
let sub_asset_id = if shared::MaterialInstanceAsset::load_from_path(&path).is_ok() {
|
||||
"material:instance"
|
||||
} else {
|
||||
"material:source"
|
||||
};
|
||||
let material_ref = MaterialRef::new(
|
||||
shared::EditorAssetRef::new(record.id.as_string(), sub_asset_id, asset.label.clone())
|
||||
.with_source_path(path.clone()),
|
||||
);
|
||||
let desc = material_desc_from_asset(&path).ok();
|
||||
let entities = crate::ui::helpers::selected_level_entities(world, selected);
|
||||
let mut renderer_changes = 0usize;
|
||||
let mut legacy_changes = Vec::new();
|
||||
for entity in entities {
|
||||
if world.get::<LevelObject>(entity).is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(mut renderer) = world.get::<shared::StaticMeshRenderer>(entity).cloned() {
|
||||
ensure_static_material_slots(&mut renderer);
|
||||
for slot in &mut renderer.materials.slots {
|
||||
slot.material = Some(material_ref.clone());
|
||||
}
|
||||
reflected_component_transaction(
|
||||
world,
|
||||
entity,
|
||||
"Assign Material Slots",
|
||||
shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
|
||||
shared::COMPONENT_STATIC_MESH_RENDERER,
|
||||
move |world, entity| {
|
||||
world.entity_mut(entity).insert(renderer);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
renderer_changes += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(mut renderer) = world.get::<shared::SkinnedMeshRenderer>(entity).cloned() {
|
||||
if renderer.materials.slots.is_empty() {
|
||||
return Err(format!(
|
||||
"Skinned renderer `{}` has no imported material slots; reimport its model",
|
||||
renderer.path
|
||||
));
|
||||
}
|
||||
for slot in &mut renderer.materials.slots {
|
||||
slot.material = Some(material_ref.clone());
|
||||
}
|
||||
reflected_component_transaction(
|
||||
world,
|
||||
entity,
|
||||
"Assign Material Slots",
|
||||
shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
|
||||
shared::COMPONENT_SKINNED_MESH_RENDERER,
|
||||
move |world, entity| {
|
||||
world.entity_mut(entity).insert(renderer);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
renderer_changes += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(desc) = desc.as_ref() {
|
||||
legacy_changes.push((entity, desc.clone()));
|
||||
}
|
||||
}
|
||||
if !legacy_changes.is_empty() {
|
||||
set_material_group_with_history(world, legacy_changes);
|
||||
}
|
||||
if renderer_changes == 0 && desc.is_none() {
|
||||
return Err("No selected renderable actor accepts this material".into());
|
||||
}
|
||||
let desc = material_desc_from_asset(&path)?;
|
||||
let changes = crate::ui::helpers::selected_level_entities(world, selected)
|
||||
.into_iter()
|
||||
.filter(|entity| world.get::<LevelObject>(*entity).is_some())
|
||||
.map(|entity| (entity, desc.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
set_material_group_with_history(world, changes);
|
||||
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
||||
format!("Applied shared material {}", asset.label);
|
||||
format!("Applied material {}", asset.label);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_static_material_slots(renderer: &mut shared::StaticMeshRenderer) {
|
||||
for part in &mut renderer.slots {
|
||||
if part.material_slot_id.0.trim().is_empty() {
|
||||
part.material_slot_id = if part.id.0.trim().is_empty() {
|
||||
ComponentInstanceId::new("slot:legacy")
|
||||
} else {
|
||||
ComponentInstanceId::new(format!("slot:{}", part.id.0))
|
||||
};
|
||||
}
|
||||
if renderer.materials.slot(&part.material_slot_id).is_none() {
|
||||
renderer.materials.slots.push(RendererMaterialSlot {
|
||||
id: part.material_slot_id.clone(),
|
||||
name: part.name.clone(),
|
||||
source_material: part.material.clone().map(MaterialRef::new),
|
||||
material: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn material_asset_picker_ui(
|
||||
world: &mut World,
|
||||
ui: &mut egui::Ui,
|
||||
|
||||
@ -119,10 +119,10 @@ pub fn assign_animation_clip_operator(
|
||||
selection: AssetSelection,
|
||||
entity: Entity,
|
||||
) -> bool {
|
||||
let availability = if world.get::<shared::SkinnedMeshRenderer>(entity).is_some() {
|
||||
let availability = if world.get::<shared::ModelRef>(entity).is_some() {
|
||||
OperatorAvailability::Ready
|
||||
} else {
|
||||
OperatorAvailability::Disabled("Select an authored skinned-mesh actor".to_string())
|
||||
OperatorAvailability::Disabled("Select an authored imported-model actor".to_string())
|
||||
};
|
||||
let label = format!("Assign Animation Clip {}", selection.display_label());
|
||||
run_immediate_operator(
|
||||
@ -133,9 +133,9 @@ pub fn assign_animation_clip_operator(
|
||||
move |world| {
|
||||
let authored = animation_clip_authoring_data(world, &selection)?;
|
||||
let model = world
|
||||
.get::<shared::SkinnedMeshRenderer>(entity)
|
||||
.get::<shared::ModelRef>(entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| "Selected actor has no SkinnedMeshRenderer".to_string())?;
|
||||
.ok_or_else(|| "Selected actor has no ModelRef".to_string())?;
|
||||
let mut controller = world
|
||||
.get::<shared::AnimationControllerDesc>(entity)
|
||||
.cloned()
|
||||
@ -197,7 +197,7 @@ mod tests {
|
||||
animation_clip_sub_asset_id, animation_skeleton_sub_asset_id, ActorKind,
|
||||
AnimationClipRecord, AnimationControllerDesc, AnimationManifest, AnimationManifestSource,
|
||||
AnimationSkeletonRecord, AnimationSkeletonSignature, AnimationSourceFingerprint,
|
||||
AudioSourceDesc, LevelObject, MaterialDesc, PrimitiveShape, SkinnedMeshRenderer,
|
||||
AudioSourceDesc, LevelObject, MaterialDesc, ModelRef, PrimitiveShape,
|
||||
ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
};
|
||||
|
||||
@ -222,7 +222,6 @@ mod tests {
|
||||
schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: asset_id.as_string(),
|
||||
label: "Operator Fixture".into(),
|
||||
default_animation_clip_id: None,
|
||||
source: AnimationManifestSource {
|
||||
path: model_path.clone(),
|
||||
format: "glb".into(),
|
||||
@ -302,43 +301,6 @@ mod tests {
|
||||
assert_undo_redo_round_trip(&mut world, 0, 1, authored_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_clip_placement_creates_only_the_skinned_renderer_path() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<ActiveOperator>();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
let (manifest_path, record, selection) = animation_fixture();
|
||||
world.insert_resource(AssetRegistry {
|
||||
records: vec![record],
|
||||
index_dirty: false,
|
||||
});
|
||||
let harness = OperatorInvariantHarness::capture(&mut world);
|
||||
|
||||
assert!(place_subasset_operator(
|
||||
&mut world,
|
||||
selection,
|
||||
Vec3::new(1.0, 2.0, 3.0),
|
||||
));
|
||||
|
||||
let entity = world
|
||||
.query_filtered::<Entity, With<LevelObject>>()
|
||||
.single(&world)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
world.get::<ActorKind>(entity),
|
||||
Some(&ActorKind::SkinnedMesh)
|
||||
);
|
||||
assert!(world.get::<SkinnedMeshRenderer>(entity).is_some());
|
||||
assert!(world.get::<AnimationControllerDesc>(entity).is_some());
|
||||
assert!(world.get::<shared::ModelRef>(entity).is_none());
|
||||
assert!(world.get::<shared::StaticMeshRenderer>(entity).is_none());
|
||||
harness.assert_committed(&mut world, 1, 1);
|
||||
assert_undo_redo_round_trip(&mut world, 0, 1, authored_count);
|
||||
let _ = std::fs::remove_file(manifest_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_clip_assignment_is_one_undoable_operator() {
|
||||
let mut world = World::new();
|
||||
@ -401,8 +363,8 @@ mod tests {
|
||||
let entity = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::SkinnedMesh,
|
||||
SkinnedMeshRenderer::new(model_path),
|
||||
ActorKind::ImportedModel,
|
||||
ModelRef::new(model_path),
|
||||
))
|
||||
.id();
|
||||
let harness = OperatorInvariantHarness::capture(&mut world);
|
||||
|
||||
@ -1248,10 +1248,6 @@ fn apply_prefab_scope_to_source(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let before_snapshot = crate::collaboration::FileSnapshot::from_loaded_bytes(
|
||||
&source_path,
|
||||
before_source.as_bytes(),
|
||||
);
|
||||
let mut document = match SceneDocument::from_ron_text(&before_source) {
|
||||
Ok(document) => document,
|
||||
Err(error) => {
|
||||
@ -1365,13 +1361,7 @@ fn apply_prefab_scope_to_source(
|
||||
}
|
||||
let after_source = document.to_ron_text()?;
|
||||
scene::validate_prefab_graph_text(&after_source, &source_path, &project_root)?;
|
||||
crate::collaboration::publish_authored_file(
|
||||
world,
|
||||
&source_path,
|
||||
after_source.as_bytes(),
|
||||
&before_snapshot,
|
||||
crate::collaboration::FileWriteIntent::PrefabSource { instance_root },
|
||||
)?;
|
||||
crate::scene::recovery::atomic_write(&source_path, after_source.as_bytes())?;
|
||||
Ok::<_, String>(after_source)
|
||||
})();
|
||||
let after_source = match apply_result {
|
||||
@ -2617,22 +2607,6 @@ fn retry_prefab_source(world: &mut World, entity: Entity) {
|
||||
"Prefab source queued for reload".into();
|
||||
}
|
||||
|
||||
pub(crate) fn reload_prefab_source_after_file_conflict(
|
||||
world: &mut World,
|
||||
instance_root: Entity,
|
||||
) -> Result<String, String> {
|
||||
if world.get::<PrefabInstance>(instance_root).is_none() {
|
||||
return Err("the prefab instance no longer exists".into());
|
||||
}
|
||||
retry_prefab_source(world, instance_root);
|
||||
let status = world.resource::<crate::scene_io::SceneIo>().status.clone();
|
||||
if status.starts_with("Prefab retry failed") {
|
||||
Err(status)
|
||||
} else {
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefab_member_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||
let Some((instance_root, target)) = prefab_member_override_target(world, entity) else {
|
||||
ui.label(egui::RichText::new("Prefab target unavailable").color(ERROR));
|
||||
@ -3284,38 +3258,6 @@ mod tests {
|
||||
assert_eq!(read_overrides(&instance), PrefabOverrides::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collaboration_reload_restarts_prefab_hydration_without_dropping_overrides() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-prefab-collaboration-reload-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let source_path = root.join("assets/prefabs/base.scn.ron");
|
||||
std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&source_path, prefab_source("mesh", None)).unwrap();
|
||||
let mut world = prefab_test_world(&root);
|
||||
let mut instance = PrefabInstance::new("base", "assets/prefabs/base.scn.ron");
|
||||
instance.overrides_ron = encode_prefab_overrides(&PrefabOverrides {
|
||||
source_revision: Some("previous".into()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let instance_root = world.spawn((instance.clone(), HydratedPrefabReady)).id();
|
||||
|
||||
let status = reload_prefab_source_after_file_conflict(&mut world, instance_root).unwrap();
|
||||
|
||||
assert_eq!(status, "Prefab source queued for reload");
|
||||
assert_eq!(
|
||||
world
|
||||
.get::<PrefabInstance>(instance_root)
|
||||
.map(|current| ¤t.overrides_ron),
|
||||
Some(&instance.overrides_ron)
|
||||
);
|
||||
assert!(world.get::<HydratedPrefabReady>(instance_root).is_none());
|
||||
assert!(world.get::<PrefabRef>(instance_root).is_some());
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_unpack_fold_is_transactional_when_a_later_link_is_invalid() {
|
||||
let mut world = World::new();
|
||||
|
||||
@ -14,12 +14,9 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::asset_db::{
|
||||
AssetRecord, ImportSettings, MaterialImportPolicy, ModelHierarchyMode, ModelPlacementMode,
|
||||
};
|
||||
use shared::{
|
||||
ComponentInstanceId, EditorAssetRef, MaterialRef, RendererMaterialSet, RendererMaterialSlot,
|
||||
StaticMeshRenderer, StaticMeshRendererEntry,
|
||||
};
|
||||
use shared::{ComponentInstanceId, EditorAssetRef, StaticMeshRenderer, StaticMeshRendererEntry};
|
||||
|
||||
pub const STATIC_MESH_MANIFEST_SCHEMA: u32 = 3;
|
||||
pub const STATIC_MESH_MANIFEST_SCHEMA: u32 = 1;
|
||||
pub const STATIC_MESH_ARTIFACT_DIR: &str = "assets/meshes/generated";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@ -83,10 +80,6 @@ pub struct StaticMeshPart {
|
||||
pub source_node: Option<String>,
|
||||
pub source_mesh: Option<String>,
|
||||
pub source_material: Option<String>,
|
||||
/// Whether the source primitive is bound to a skin and therefore must never become a static
|
||||
/// renderer slot.
|
||||
#[serde(default)]
|
||||
pub skinned: bool,
|
||||
}
|
||||
|
||||
pub fn static_mesh_manifest_path(asset_id: &str) -> String {
|
||||
@ -101,17 +94,6 @@ pub fn material_id_from_label(label: &str) -> String {
|
||||
format!("material:{}", stable_sub_asset_slug(label))
|
||||
}
|
||||
|
||||
fn gltf_draw_id(node_index: Option<usize>, mesh_index: usize, primitive_index: usize) -> String {
|
||||
let node = node_index
|
||||
.map(|index| index.to_string())
|
||||
.unwrap_or_else(|| "unbound".into());
|
||||
format!("draw:scene0:node{node}:mesh{mesh_index}:primitive{primitive_index}")
|
||||
}
|
||||
|
||||
fn fbx_draw_id(node_index: usize, material_index: usize) -> String {
|
||||
format!("draw:scene0:node{node_index}:material{material_index}")
|
||||
}
|
||||
|
||||
fn stable_sub_asset_slug(label: &str) -> String {
|
||||
let mut slug = String::new();
|
||||
for ch in label.chars() {
|
||||
@ -169,106 +151,38 @@ pub fn renderer_from_manifest(
|
||||
MaterialImportPolicy::SourceMaterials
|
||||
);
|
||||
|
||||
let parts: Vec<_> = manifest
|
||||
.parts
|
||||
.iter()
|
||||
.filter(|part| !part.skinned && manifest.metadata.animation_count == 0)
|
||||
.map(|part| StaticMeshRendererEntry {
|
||||
id: ComponentInstanceId::new(part_effective_id(part)),
|
||||
name: part.name.clone(),
|
||||
mesh: EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
part_effective_id(part),
|
||||
part.name.clone(),
|
||||
),
|
||||
material_slot_id: ComponentInstanceId::new(material_slot_id(part)),
|
||||
material: use_source_materials
|
||||
.then(|| {
|
||||
part_effective_material_id(part).map(|id| {
|
||||
EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
id,
|
||||
part.material_slot_name.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.flatten(),
|
||||
local_transform: part.local_transform,
|
||||
visible: true,
|
||||
cast_shadows: true,
|
||||
receive_shadows: true,
|
||||
})
|
||||
.collect();
|
||||
let materials = RendererMaterialSet {
|
||||
slots: manifest
|
||||
.parts
|
||||
.iter()
|
||||
.filter(|part| !part.skinned && manifest.metadata.animation_count == 0)
|
||||
.map(|part| RendererMaterialSlot {
|
||||
id: ComponentInstanceId::new(material_slot_id(part)),
|
||||
name: part.material_slot_name.clone(),
|
||||
source_material: use_source_materials
|
||||
.then(|| {
|
||||
part_effective_material_id(part).map(|id| {
|
||||
MaterialRef::new(EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
id,
|
||||
part.material_slot_name.clone(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.flatten(),
|
||||
material: None,
|
||||
})
|
||||
.collect(),
|
||||
orphaned_assignments: Vec::new(),
|
||||
};
|
||||
|
||||
StaticMeshRenderer {
|
||||
slots: parts,
|
||||
materials,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the shared material slots for a full imported hierarchy. Unlike static renderer
|
||||
/// construction this intentionally includes skin-bound and rigid animated draw bindings.
|
||||
pub fn renderer_materials_from_manifest(
|
||||
manifest: &StaticMeshManifest,
|
||||
settings: &ImportSettings,
|
||||
) -> RendererMaterialSet {
|
||||
let use_source_materials = matches!(
|
||||
settings.material_policy,
|
||||
MaterialImportPolicy::SourceMaterials
|
||||
);
|
||||
RendererMaterialSet {
|
||||
slots: manifest
|
||||
.parts
|
||||
.iter()
|
||||
.map(|part| RendererMaterialSlot {
|
||||
id: ComponentInstanceId::new(material_slot_id(part)),
|
||||
name: part.material_slot_name.clone(),
|
||||
source_material: use_source_materials
|
||||
.map(|part| StaticMeshRendererEntry {
|
||||
id: ComponentInstanceId::new(part_effective_id(part)),
|
||||
name: part.name.clone(),
|
||||
mesh: EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
part_effective_id(part),
|
||||
part.name.clone(),
|
||||
),
|
||||
material: use_source_materials
|
||||
.then(|| {
|
||||
part_effective_material_id(part).map(|id| {
|
||||
MaterialRef::new(EditorAssetRef::new(
|
||||
EditorAssetRef::new(
|
||||
manifest.asset_id.clone(),
|
||||
id,
|
||||
part.material_slot_name.clone(),
|
||||
))
|
||||
)
|
||||
})
|
||||
})
|
||||
.flatten(),
|
||||
material: None,
|
||||
local_transform: part.local_transform,
|
||||
visible: true,
|
||||
cast_shadows: true,
|
||||
receive_shadows: true,
|
||||
})
|
||||
.collect(),
|
||||
orphaned_assignments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn material_slot_id(part: &StaticMeshPart) -> String {
|
||||
format!("slot:{}", part_effective_id(part))
|
||||
}
|
||||
|
||||
fn part_effective_id(part: &StaticMeshPart) -> String {
|
||||
if part.id.trim().is_empty() {
|
||||
part_id_from_label(&part.mesh_label)
|
||||
@ -341,23 +255,23 @@ fn build_gltf_manifest(
|
||||
.or_else(|| gltf.document.scenes().next())
|
||||
{
|
||||
for node in scene.nodes() {
|
||||
collect_gltf_node_parts(node, Mat4::IDENTITY, "", &mut parts);
|
||||
collect_gltf_node_parts(node, Mat4::IDENTITY, &mut parts);
|
||||
}
|
||||
} else {
|
||||
for mesh in gltf.document.meshes() {
|
||||
collect_gltf_mesh_parts(None, None, None, mesh, Mat4::IDENTITY, false, &mut parts);
|
||||
collect_gltf_mesh_parts(None, None, mesh, Mat4::IDENTITY, &mut parts);
|
||||
}
|
||||
}
|
||||
|
||||
if gltf.document.animations().count() > 0 {
|
||||
warnings.push(
|
||||
"Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer."
|
||||
"Animations are recorded as metadata; use Scene Instance placement for playback."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if gltf.document.skins().count() > 0 {
|
||||
warnings.push(
|
||||
"Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer."
|
||||
"Skins are recorded as metadata; skinned playback stays on the Scene Instance path."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
@ -392,44 +306,29 @@ fn build_gltf_manifest(
|
||||
fn collect_gltf_node_parts(
|
||||
node: gltf::Node<'_>,
|
||||
parent_transform: Mat4,
|
||||
parent_path: &str,
|
||||
parts: &mut Vec<StaticMeshPart>,
|
||||
) {
|
||||
let local = Mat4::from_cols_array_2d(&node.transform().matrix());
|
||||
let world_transform = parent_transform * local;
|
||||
let segment = node
|
||||
.name()
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| format!("Node{}", node.index()));
|
||||
let node_path = if parent_path.is_empty() {
|
||||
segment
|
||||
} else {
|
||||
format!("{parent_path}/{segment}")
|
||||
};
|
||||
let skinned = node.skin().is_some();
|
||||
if let Some(mesh) = node.mesh() {
|
||||
collect_gltf_mesh_parts(
|
||||
node.name().map(str::to_string),
|
||||
Some(node.index()),
|
||||
Some(node_path.clone()),
|
||||
mesh,
|
||||
world_transform,
|
||||
skinned,
|
||||
parts,
|
||||
);
|
||||
}
|
||||
for child in node.children() {
|
||||
collect_gltf_node_parts(child, world_transform, &node_path, parts);
|
||||
collect_gltf_node_parts(child, world_transform, parts);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_gltf_mesh_parts(
|
||||
node_name: Option<String>,
|
||||
node_index: Option<usize>,
|
||||
node_path: Option<String>,
|
||||
mesh: gltf::Mesh<'_>,
|
||||
transform: Mat4,
|
||||
skinned: bool,
|
||||
parts: &mut Vec<StaticMeshPart>,
|
||||
) {
|
||||
let mesh_index = mesh.index();
|
||||
@ -460,11 +359,9 @@ fn collect_gltf_mesh_parts(
|
||||
.clone()
|
||||
.or_else(|| mesh_name.clone())
|
||||
.unwrap_or_else(|| format!("Mesh {mesh_index}"));
|
||||
let source_node = node_path
|
||||
.clone()
|
||||
.or_else(|| node_index.map(|index| format!("Node{index}")));
|
||||
let source_node = node_index.map(|index| format!("Node{index}"));
|
||||
parts.push(StaticMeshPart {
|
||||
id: gltf_draw_id(node_index, mesh_index, primitive_index),
|
||||
id: part_id_from_label(&mesh_label),
|
||||
name: format!("{name} / Primitive {primitive_index}"),
|
||||
material_id: material_label
|
||||
.as_ref()
|
||||
@ -476,7 +373,6 @@ fn collect_gltf_mesh_parts(
|
||||
source_node,
|
||||
source_mesh: Some(format!("Mesh{mesh_index}")),
|
||||
source_material: Some(material_name),
|
||||
skinned,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -509,7 +405,6 @@ fn build_fbx_manifest(
|
||||
if mesh.num_vertices == 0 || mesh.faces.as_ref().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let skinned = !mesh.skin_deformers.as_ref().is_empty();
|
||||
|
||||
let mut groups: Vec<(usize, Vec<u32>)> =
|
||||
group_faces_by_material(mesh).into_iter().collect();
|
||||
@ -533,7 +428,7 @@ fn build_fbx_manifest(
|
||||
};
|
||||
let mesh_label = FbxAssetLabel::Mesh(node_index * 1000 + material_index).to_string();
|
||||
parts.push(StaticMeshPart {
|
||||
id: fbx_draw_id(node_index, material_index),
|
||||
id: part_id_from_label(&mesh_label),
|
||||
name: format!("{node_name} / Material {material_index}"),
|
||||
material_id: material_label
|
||||
.as_ref()
|
||||
@ -545,20 +440,19 @@ fn build_fbx_manifest(
|
||||
source_node: Some(format!("Node{node_index}")),
|
||||
source_mesh: Some(format!("Mesh{node_index}")),
|
||||
source_material: Some(material_name),
|
||||
skinned,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !scene.anim_stacks.as_ref().is_empty() {
|
||||
warnings.push(
|
||||
"Animations are recorded as metadata; animated placement uses SkinnedMeshRenderer."
|
||||
"Animations are recorded as metadata; use Scene Instance placement for playback."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if !scene.skin_deformers.as_ref().is_empty() {
|
||||
warnings.push(
|
||||
"Skinned primitives are excluded from StaticMeshRenderer and use SkinnedMeshRenderer."
|
||||
"Skins are recorded as metadata; skinned playback stays on the Scene Instance path."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
@ -660,7 +554,6 @@ fn resolve_dependency(source_path: &str, uri: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::asset_db::AssetId;
|
||||
|
||||
fn test_manifest() -> StaticMeshManifest {
|
||||
StaticMeshManifest {
|
||||
@ -704,7 +597,6 @@ mod tests {
|
||||
source_node: Some("Node0".into()),
|
||||
source_mesh: Some("Mesh0".into()),
|
||||
source_material: Some("Wood".into()),
|
||||
skinned: false,
|
||||
}],
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
@ -753,48 +645,4 @@ mod tests {
|
||||
|
||||
assert!(renderer.slots[0].material.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_from_manifest_excludes_skinned_primitives() {
|
||||
let mut manifest = test_manifest();
|
||||
manifest.parts[0].skinned = true;
|
||||
|
||||
let renderer = renderer_from_manifest(&manifest, &ImportSettings::default());
|
||||
|
||||
assert!(renderer.slots.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_from_manifest_excludes_node_animated_geometry() {
|
||||
let mut manifest = test_manifest();
|
||||
manifest.metadata.animation_count = 1;
|
||||
|
||||
let renderer = renderer_from_manifest(&manifest, &ImportSettings::default());
|
||||
|
||||
assert!(renderer.slots.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_rigged_fixture_never_builds_static_renderer_slots() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../assets/models/robot_expressive.glb")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let record = AssetRecord {
|
||||
id: AssetId::new(),
|
||||
path,
|
||||
label: "Robot Expressive".into(),
|
||||
kind_tag: "Model".into(),
|
||||
import_settings: ImportSettings::default(),
|
||||
dependencies: Vec::new(),
|
||||
};
|
||||
|
||||
let manifest = build_static_mesh_manifest(&record).unwrap();
|
||||
let renderer = renderer_from_manifest(&manifest, &record.import_settings);
|
||||
|
||||
assert!(manifest.metadata.skin_count > 0);
|
||||
assert!(manifest.metadata.animation_count > 0);
|
||||
assert!(manifest.parts.iter().any(|part| part.skinned));
|
||||
assert!(renderer.slots.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,6 @@ use crate::infra::EditorOnly;
|
||||
|
||||
pub(crate) const THUMB_SIZE: u32 = 128;
|
||||
pub(crate) const THUMBNAIL_LAYER: usize = 31;
|
||||
const THUMBNAIL_VERTICAL_FOV: f32 = 40.0_f32.to_radians();
|
||||
const THUMBNAIL_HALF_VERTICAL_FOV: f32 = THUMBNAIL_VERTICAL_FOV * 0.5;
|
||||
const SCENE_FRAME_PADDING: f32 = 1.35;
|
||||
const MATERIAL_SPHERE_FRAME_PADDING: f32 = 1.28;
|
||||
const MESH_WARMUP_FRAMES: u8 = 12;
|
||||
const POST_ATTACH_FRAMES: u8 = 8;
|
||||
const RENDER_FRAMES: u8 = 3;
|
||||
@ -65,25 +61,6 @@ struct ActiveModelThumbnail {
|
||||
framed: bool,
|
||||
frames_remaining: u8,
|
||||
wait_frames: u32,
|
||||
framing: ThumbnailFraming,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ThumbnailFraming {
|
||||
SceneBounds,
|
||||
MaterialSphere,
|
||||
}
|
||||
|
||||
impl ThumbnailFraming {
|
||||
fn for_source(source: &ThumbnailJobSource) -> Self {
|
||||
match source {
|
||||
ThumbnailJobSource::SourceMaterial { .. }
|
||||
| ThumbnailJobSource::MaterialAsset { .. } => Self::MaterialSphere,
|
||||
ThumbnailJobSource::Model { .. } | ThumbnailJobSource::MeshSubAsset { .. } => {
|
||||
Self::SceneBounds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ThumbnailStudioPlugin;
|
||||
@ -166,7 +143,7 @@ fn setup_thumbnail_studio(mut commands: Commands, mut images: ResMut<Assets<Imag
|
||||
RenderTarget::Image(render_image.clone().into()),
|
||||
Transform::from_xyz(2.0, 1.4, 2.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y),
|
||||
Projection::Perspective(PerspectiveProjection {
|
||||
fov: THUMBNAIL_VERTICAL_FOV,
|
||||
fov: 40.0_f32.to_radians(),
|
||||
..default()
|
||||
}),
|
||||
))
|
||||
@ -252,7 +229,6 @@ fn process_thumbnail_studio(
|
||||
&mesh3d,
|
||||
&mesh_storage,
|
||||
&mut camera_transforms,
|
||||
active.framing,
|
||||
) {
|
||||
active.framed = true;
|
||||
active.post_attach_frames = 0;
|
||||
@ -346,7 +322,6 @@ fn process_thumbnail_studio(
|
||||
.id();
|
||||
|
||||
let label = job.source.label().to_string();
|
||||
let framing = ThumbnailFraming::for_source(&job.source);
|
||||
let content_spawned = spawn_thumbnail_job_content(
|
||||
&mut commands,
|
||||
&mut mesh_storage,
|
||||
@ -375,7 +350,6 @@ fn process_thumbnail_studio(
|
||||
framed: false,
|
||||
frames_remaining: 0,
|
||||
wait_frames: 0,
|
||||
framing,
|
||||
},
|
||||
"no renderable meshes",
|
||||
true,
|
||||
@ -395,7 +369,6 @@ fn process_thumbnail_studio(
|
||||
framed: false,
|
||||
frames_remaining: 0,
|
||||
wait_frames: 0,
|
||||
framing,
|
||||
});
|
||||
}
|
||||
|
||||
@ -615,7 +588,6 @@ fn frame_thumbnail_scene(
|
||||
meshes: &Query<&Mesh3d>,
|
||||
mesh_assets: &Assets<Mesh>,
|
||||
camera_transforms: &mut Query<&mut Transform, With<ThumbnailStudioCamera>>,
|
||||
framing: ThumbnailFraming,
|
||||
) -> bool {
|
||||
let mut entities = Vec::new();
|
||||
collect_descendants(root, children, &mut entities);
|
||||
@ -661,7 +633,8 @@ fn frame_thumbnail_scene(
|
||||
}
|
||||
|
||||
let center = (min + max) * 0.5;
|
||||
let distance = thumbnail_camera_distance(min, max, framing);
|
||||
let radius = (max - min).length().max(0.25) * 0.5;
|
||||
let distance = radius / (20.0_f32.to_radians()).tan() * 1.35;
|
||||
let eye = center + Vec3::new(1.0, 0.75, 1.0).normalize() * distance;
|
||||
|
||||
if let Ok(mut transform) = camera_transforms.single_mut() {
|
||||
@ -671,20 +644,6 @@ fn frame_thumbnail_scene(
|
||||
true
|
||||
}
|
||||
|
||||
fn thumbnail_camera_distance(min: Vec3, max: Vec3, framing: ThumbnailFraming) -> f32 {
|
||||
let bounds_size = max - min;
|
||||
let (radius, padding) = match framing {
|
||||
ThumbnailFraming::SceneBounds => {
|
||||
(bounds_size.length().max(0.25) * 0.5, SCENE_FRAME_PADDING)
|
||||
}
|
||||
ThumbnailFraming::MaterialSphere => (
|
||||
bounds_size.max_element().max(0.5) * 0.5,
|
||||
MATERIAL_SPHERE_FRAME_PADDING,
|
||||
),
|
||||
};
|
||||
radius / THUMBNAIL_HALF_VERTICAL_FOV.tan() * padding
|
||||
}
|
||||
|
||||
fn collect_descendants(entity: Entity, children: &Query<&Children>, out: &mut Vec<Entity>) {
|
||||
out.push(entity);
|
||||
let Ok(child_list) = children.get(entity) else {
|
||||
@ -714,24 +673,3 @@ fn aabb_corners(aabb: &Aabb3d) -> [Vec3; 8] {
|
||||
pub fn model_file_exists(catalog_path: &str) -> bool {
|
||||
Path::new(catalog_path).is_file()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn material_sphere_framing_fills_more_of_the_thumbnail_with_padding() {
|
||||
let sphere_radius = 0.65;
|
||||
let min = Vec3::splat(-sphere_radius);
|
||||
let max = Vec3::splat(sphere_radius);
|
||||
|
||||
let scene_distance = thumbnail_camera_distance(min, max, ThumbnailFraming::SceneBounds);
|
||||
let material_distance =
|
||||
thumbnail_camera_distance(min, max, ThumbnailFraming::MaterialSphere);
|
||||
let projected_radius_fraction =
|
||||
sphere_radius / (material_distance * THUMBNAIL_HALF_VERTICAL_FOV.tan());
|
||||
|
||||
assert!(material_distance < scene_distance * 0.6);
|
||||
assert!((0.75..0.82).contains(&projected_radius_fraction));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,9 @@ use std::path::PathBuf;
|
||||
use bevy::prelude::*;
|
||||
use shared::{
|
||||
ActorId, ActorKind, ActorName, AnimationControllerDesc, AudioListenerDesc, AudioSourceDesc,
|
||||
AuthoringComponentStates, BrushDesc, ColliderDesc, EditorVisibility, InspectorOrder, LightDesc,
|
||||
MaterialDesc, MaterialOverride, ModelRef, NavigationArea, NavigationBounds, NavigationLink,
|
||||
NavigationObstacle, ObjectiveMarker, PhysicsBody, PostProcessVolumeDesc, PrefabInstance,
|
||||
PrefabRef, Primitive, RigidBodyDesc, SceneComposition, SkinnedMeshRenderer, StaticMeshRenderer,
|
||||
BrushDesc, ColliderDesc, EditorVisibility, InspectorOrder, LightDesc, MaterialDesc,
|
||||
MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody, PostProcessVolumeDesc,
|
||||
PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, SceneComposition, StaticMeshRenderer,
|
||||
TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
};
|
||||
#[derive(Debug, Clone)]
|
||||
@ -19,7 +18,6 @@ pub struct EditorEntitySnapshot {
|
||||
pub primitive: Option<Primitive>,
|
||||
pub brush: Option<BrushDesc>,
|
||||
pub static_mesh_renderer: Option<StaticMeshRenderer>,
|
||||
pub skinned_mesh_renderer: Option<SkinnedMeshRenderer>,
|
||||
pub material: Option<MaterialDesc>,
|
||||
pub material_override: Option<MaterialOverride>,
|
||||
pub rigid_body: Option<RigidBodyDesc>,
|
||||
@ -38,52 +36,11 @@ pub struct EditorEntitySnapshot {
|
||||
pub post_process_volume: Option<PostProcessVolumeDesc>,
|
||||
pub team_spawn: Option<TeamSpawn>,
|
||||
pub objective: Option<ObjectiveMarker>,
|
||||
pub navigation_bounds: Option<NavigationBounds>,
|
||||
pub navigation_obstacle: Option<NavigationObstacle>,
|
||||
pub navigation_area: Option<NavigationArea>,
|
||||
pub navigation_link: Option<NavigationLink>,
|
||||
pub hierarchy_sibling_index: i32,
|
||||
pub editor_visibility: EditorVisibility,
|
||||
pub inspector_order: Option<InspectorOrder>,
|
||||
pub component_states: Option<AuthoringComponentStates>,
|
||||
pub children: Vec<EditorEntitySnapshot>,
|
||||
}
|
||||
|
||||
/// One reflected component value used by registry-driven editor transactions.
|
||||
/// `component_id` is stable; `type_path` is retained only to resolve Bevy's
|
||||
/// reflection adapter for this build.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReflectedComponentValue {
|
||||
pub component_id: String,
|
||||
pub type_path: String,
|
||||
pub ron: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ComponentDelta {
|
||||
pub component_id: String,
|
||||
pub type_path: String,
|
||||
pub before: Option<ReflectedComponentValue>,
|
||||
pub after: Option<ReflectedComponentValue>,
|
||||
}
|
||||
|
||||
/// An atomic add/remove/reset/paste operation. Requirements, dependent removals,
|
||||
/// and the derived ActorKind hint can be represented as additional deltas.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ComponentTransaction {
|
||||
pub entity: Entity,
|
||||
pub label: &'static str,
|
||||
pub deltas: Vec<ComponentDelta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct NavigationComponentState {
|
||||
pub bounds: Option<NavigationBounds>,
|
||||
pub obstacle: Option<NavigationObstacle>,
|
||||
pub area: Option<NavigationArea>,
|
||||
pub link: Option<NavigationLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SiblingChange {
|
||||
pub entity: Entity,
|
||||
@ -217,11 +174,6 @@ pub enum EditorCommand {
|
||||
old: Option<PostProcessVolumeDesc>,
|
||||
new: PostProcessVolumeDesc,
|
||||
},
|
||||
SetNavigation {
|
||||
entity: Entity,
|
||||
old: NavigationComponentState,
|
||||
new: NavigationComponentState,
|
||||
},
|
||||
SetTransformGroup {
|
||||
entities: Vec<Entity>,
|
||||
olds: Vec<Transform>,
|
||||
@ -287,7 +239,6 @@ pub enum EditorCommand {
|
||||
entity: Entity,
|
||||
snapshot: EditorEntitySnapshot,
|
||||
},
|
||||
ComponentTransaction(ComponentTransaction),
|
||||
}
|
||||
|
||||
impl EditorCommand {
|
||||
@ -315,7 +266,6 @@ impl EditorCommand {
|
||||
EditorCommand::ApplyBrushCsg { .. } => "Brush CSG",
|
||||
EditorCommand::SetStaticMeshRenderer { .. } => "Set Static Mesh Renderer",
|
||||
EditorCommand::SetPostProcessVolume { .. } => "Set Post Process Volume",
|
||||
EditorCommand::SetNavigation { .. } => "Set Navigation",
|
||||
EditorCommand::SetTransformGroup { .. } => "Move Selection",
|
||||
EditorCommand::Reparent { .. } => "Reparent",
|
||||
EditorCommand::SetActorKind { .. } => "Set Actor Kind",
|
||||
@ -333,7 +283,6 @@ impl EditorCommand {
|
||||
} => "Unpack Prefab Layer",
|
||||
EditorCommand::AddComponent { .. } => "Add Component",
|
||||
EditorCommand::RemoveComponent { .. } => "Remove Component",
|
||||
EditorCommand::ComponentTransaction(transaction) => transaction.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bevy::ecs::reflect::ReflectComponent;
|
||||
use bevy::prelude::*;
|
||||
use bevy::reflect::serde::{TypedReflectDeserializer, TypedReflectSerializer};
|
||||
use bevy::reflect::std_traits::ReflectDefault;
|
||||
use bevy::world_serialization::WorldInstance;
|
||||
use serde::de::DeserializeSeed;
|
||||
use shared::{
|
||||
infer_actor_kind, ActorId, ActorKind, ActorName, AnimationControllerDesc, AudioListenerDesc,
|
||||
AudioSourceDesc, AuthoringComponentStates, BrushDesc, ColliderDesc, EditorVisibility,
|
||||
HierarchySiblingIndex, HydratedPrefabMember, HydratedPrefabReady, InspectorOrder, LevelObject,
|
||||
LightDesc, MaterialDesc, MaterialOverride, ModelRef, NavigationArea, NavigationBounds,
|
||||
NavigationLink, NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn,
|
||||
AudioSourceDesc, BrushDesc, ColliderDesc, EditorVisibility, HierarchySiblingIndex,
|
||||
HydratedPrefabMember, HydratedPrefabReady, InspectorOrder, LevelObject, LightDesc,
|
||||
MaterialDesc, MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn,
|
||||
PostProcessVolumeDesc, PrefabHydrationBlocked, PrefabInstance, PrefabRef, Primitive,
|
||||
RigidBodyDesc, SceneComposition, SkinnedMeshRenderer, StaticMeshRenderer, TeamSpawn,
|
||||
TriggerVolume, WeaponSpawn,
|
||||
RigidBodyDesc, SceneComposition, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
};
|
||||
|
||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||
@ -28,10 +22,7 @@ use crate::ui::selection_ops::is_mutable_level_object;
|
||||
use crate::ui::{egui_captures_keyboard_from_world, UiState};
|
||||
|
||||
mod commands;
|
||||
pub use commands::{
|
||||
ComponentDelta, ComponentTransaction, EditorCommand, EditorEntitySnapshot,
|
||||
NavigationComponentState, PrefabEditState, ReflectedComponentValue, SiblingChange,
|
||||
};
|
||||
pub use commands::{EditorCommand, EditorEntitySnapshot, PrefabEditState, SiblingChange};
|
||||
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct EditorHistory {
|
||||
@ -147,7 +138,6 @@ pub fn snapshot_entity(world: &World, entity: Entity) -> Option<EditorEntitySnap
|
||||
primitive: entity_ref.get::<Primitive>().cloned(),
|
||||
brush: entity_ref.get::<BrushDesc>().cloned(),
|
||||
static_mesh_renderer: entity_ref.get::<StaticMeshRenderer>().cloned(),
|
||||
skinned_mesh_renderer: entity_ref.get::<SkinnedMeshRenderer>().cloned(),
|
||||
material: entity_ref.get::<MaterialDesc>().cloned(),
|
||||
material_override: entity_ref.get::<MaterialOverride>().cloned(),
|
||||
rigid_body: entity_ref.get::<RigidBodyDesc>().cloned(),
|
||||
@ -166,10 +156,6 @@ pub fn snapshot_entity(world: &World, entity: Entity) -> Option<EditorEntitySnap
|
||||
post_process_volume: entity_ref.get::<PostProcessVolumeDesc>().cloned(),
|
||||
team_spawn: entity_ref.get::<TeamSpawn>().cloned(),
|
||||
objective: entity_ref.get::<ObjectiveMarker>().cloned(),
|
||||
navigation_bounds: entity_ref.get::<NavigationBounds>().cloned(),
|
||||
navigation_obstacle: entity_ref.get::<NavigationObstacle>().cloned(),
|
||||
navigation_area: entity_ref.get::<NavigationArea>().cloned(),
|
||||
navigation_link: entity_ref.get::<NavigationLink>().cloned(),
|
||||
hierarchy_sibling_index: entity_ref
|
||||
.get::<HierarchySiblingIndex>()
|
||||
.map(|index| index.0)
|
||||
@ -178,8 +164,6 @@ pub fn snapshot_entity(world: &World, entity: Entity) -> Option<EditorEntitySnap
|
||||
.get::<EditorVisibility>()
|
||||
.copied()
|
||||
.unwrap_or_default(),
|
||||
inspector_order: entity_ref.get::<InspectorOrder>().cloned(),
|
||||
component_states: entity_ref.get::<AuthoringComponentStates>().cloned(),
|
||||
children,
|
||||
})
|
||||
}
|
||||
@ -220,9 +204,6 @@ fn spawn_snapshot_with_parent(
|
||||
if let Some(renderer) = &snapshot.static_mesh_renderer {
|
||||
entity_mut.insert(renderer.clone());
|
||||
}
|
||||
if let Some(renderer) = &snapshot.skinned_mesh_renderer {
|
||||
entity_mut.insert(renderer.clone());
|
||||
}
|
||||
if let Some(material) = &snapshot.material {
|
||||
entity_mut.insert(material.clone());
|
||||
}
|
||||
@ -277,26 +258,8 @@ fn spawn_snapshot_with_parent(
|
||||
if let Some(objective) = &snapshot.objective {
|
||||
entity_mut.insert(objective.clone());
|
||||
}
|
||||
if let Some(bounds) = &snapshot.navigation_bounds {
|
||||
entity_mut.insert(bounds.clone());
|
||||
}
|
||||
if let Some(obstacle) = &snapshot.navigation_obstacle {
|
||||
entity_mut.insert(obstacle.clone());
|
||||
}
|
||||
if let Some(area) = &snapshot.navigation_area {
|
||||
entity_mut.insert(area.clone());
|
||||
}
|
||||
if let Some(link) = &snapshot.navigation_link {
|
||||
entity_mut.insert(link.clone());
|
||||
}
|
||||
entity_mut.insert(HierarchySiblingIndex(snapshot.hierarchy_sibling_index));
|
||||
entity_mut.insert(snapshot.editor_visibility);
|
||||
if let Some(order) = &snapshot.inspector_order {
|
||||
entity_mut.insert(order.clone());
|
||||
}
|
||||
if let Some(states) = &snapshot.component_states {
|
||||
entity_mut.insert(states.clone());
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
entity_mut.insert(ChildOf(parent));
|
||||
}
|
||||
@ -319,7 +282,6 @@ pub fn clear_level_objects(world: &mut World) {
|
||||
}
|
||||
|
||||
pub fn spawn_with_history(world: &mut World, mut snapshot: EditorEntitySnapshot) -> Entity {
|
||||
assign_missing_actor_ids(&mut snapshot);
|
||||
snapshot.hierarchy_sibling_index = next_sibling_index(world, None);
|
||||
let entity = spawn_snapshot(world, &snapshot);
|
||||
push_history(
|
||||
@ -341,9 +303,6 @@ pub fn spawn_many_with_history(
|
||||
if snapshots.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
for snapshot in &mut snapshots {
|
||||
assign_missing_actor_ids(snapshot);
|
||||
}
|
||||
for (sibling_index, snapshot) in (next_sibling_index(world, None)..).zip(snapshots.iter_mut()) {
|
||||
snapshot.hierarchy_sibling_index = sibling_index;
|
||||
}
|
||||
@ -425,37 +384,12 @@ pub fn duplicate_entities_with_history(world: &mut World, entities: &[Entity]) {
|
||||
}
|
||||
|
||||
fn assign_fresh_actor_ids(snapshot: &mut EditorEntitySnapshot) {
|
||||
let actor_id = new_actor_id();
|
||||
if let Some(bounds) = &mut snapshot.navigation_bounds {
|
||||
bounds.artifact_path = shared::navigation_artifact_path_for_actor(&actor_id.0);
|
||||
}
|
||||
snapshot.actor_id = Some(actor_id);
|
||||
snapshot.actor_id = Some(new_actor_id());
|
||||
for child in &mut snapshot.children {
|
||||
assign_fresh_actor_ids(child);
|
||||
}
|
||||
}
|
||||
|
||||
fn assign_missing_actor_ids(snapshot: &mut EditorEntitySnapshot) {
|
||||
let existing_actor_id = snapshot
|
||||
.actor_id
|
||||
.clone()
|
||||
.filter(|id| !id.0.trim().is_empty());
|
||||
let assigned_new_id = existing_actor_id.is_none();
|
||||
let actor_id = existing_actor_id.unwrap_or_else(new_actor_id);
|
||||
if let Some(bounds) = &mut snapshot.navigation_bounds {
|
||||
if assigned_new_id
|
||||
|| bounds.artifact_path == NavigationBounds::default().artifact_path
|
||||
|| shared::navigation_generated_artifact_path(&bounds.artifact_path).is_err()
|
||||
{
|
||||
bounds.artifact_path = shared::navigation_artifact_path_for_actor(&actor_id.0);
|
||||
}
|
||||
}
|
||||
snapshot.actor_id = Some(actor_id);
|
||||
for child in &mut snapshot.children {
|
||||
assign_missing_actor_ids(child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rename_entity_with_history(world: &mut World, entity: Entity, new_name: String) {
|
||||
if !is_mutable_level_object(world, entity) {
|
||||
return;
|
||||
@ -627,31 +561,6 @@ pub fn set_post_process_volume_with_history(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn navigation_component_state(world: &World, entity: Entity) -> NavigationComponentState {
|
||||
NavigationComponentState {
|
||||
bounds: world.get::<NavigationBounds>(entity).cloned(),
|
||||
obstacle: world.get::<NavigationObstacle>(entity).cloned(),
|
||||
area: world.get::<NavigationArea>(entity).cloned(),
|
||||
link: world.get::<NavigationLink>(entity).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_navigation_with_history(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
new: NavigationComponentState,
|
||||
) {
|
||||
if !is_mutable_level_object(world, entity) {
|
||||
return;
|
||||
}
|
||||
let old = navigation_component_state(world, entity);
|
||||
if old == new {
|
||||
return;
|
||||
}
|
||||
apply_navigation_state(world, entity, &new);
|
||||
push_history(world, EditorCommand::SetNavigation { entity, old, new });
|
||||
}
|
||||
|
||||
pub fn set_physics_with_history(world: &mut World, entity: Entity, new: PhysicsBody) {
|
||||
if !is_mutable_level_object(world, entity) {
|
||||
return;
|
||||
@ -909,7 +818,6 @@ pub fn group_selection_with_history(world: &mut World, entities: &[Entity]) {
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -928,18 +836,8 @@ pub fn group_selection_with_history(world: &mut World, entities: &[Entity]) {
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: next_sibling_index(world, None),
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
let group = spawn_snapshot(world, &snapshot);
|
||||
@ -970,7 +868,6 @@ pub fn create_empty_child_with_history(world: &mut World, parent: Entity) {
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -989,18 +886,8 @@ pub fn create_empty_child_with_history(world: &mut World, parent: Entity) {
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: next_sibling_index(world, Some(parent)),
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
let entity = spawn_snapshot_with_parent(world, &snapshot, Some(parent));
|
||||
@ -1348,215 +1235,6 @@ pub fn apply_actor_kind(world: &mut World, entity: Entity, kind: ActorKind) {
|
||||
}
|
||||
}
|
||||
|
||||
pub const ACTOR_KIND_COMPONENT_ID: &str = "core.actor_kind";
|
||||
pub const ACTOR_KIND_TYPE_PATH: &str = "shared::components::ActorKind";
|
||||
|
||||
/// Captures any reflected component without adding a typed history branch.
|
||||
pub fn capture_reflected_component(
|
||||
world: &World,
|
||||
entity: Entity,
|
||||
component_id: &str,
|
||||
type_path: &str,
|
||||
) -> Result<Option<ReflectedComponentValue>, String> {
|
||||
let entity_ref = world
|
||||
.get_entity(entity)
|
||||
.map_err(|_| format!("entity {entity:?} no longer exists"))?;
|
||||
let app_registry = world
|
||||
.get_resource::<AppTypeRegistry>()
|
||||
.ok_or_else(|| "AppTypeRegistry is unavailable".to_string())?;
|
||||
let type_registry = app_registry.read();
|
||||
let registration = type_registry
|
||||
.get_with_type_path(type_path)
|
||||
.ok_or_else(|| format!("component type `{type_path}` is not registered"))?;
|
||||
let reflect_component = registration
|
||||
.data::<ReflectComponent>()
|
||||
.ok_or_else(|| format!("component `{type_path}` has no ReflectComponent adapter"))?;
|
||||
let Some(value) = reflect_component.reflect(entity_ref) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let ron = ron::ser::to_string(&TypedReflectSerializer::new(value, &type_registry))
|
||||
.map_err(|error| format!("could not serialize `{type_path}`: {error}"))?;
|
||||
Ok(Some(ReflectedComponentValue {
|
||||
component_id: component_id.to_string(),
|
||||
type_path: type_path.to_string(),
|
||||
ron,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Applies or removes one reflected component value.
|
||||
pub fn apply_reflected_component(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
type_path: &str,
|
||||
value: Option<&ReflectedComponentValue>,
|
||||
) -> Result<(), String> {
|
||||
if world.get_entity(entity).is_err() {
|
||||
return Err(format!("entity {entity:?} no longer exists"));
|
||||
}
|
||||
world.resource_scope(|world, app_registry: Mut<AppTypeRegistry>| {
|
||||
let type_registry = app_registry.read();
|
||||
let registration = type_registry
|
||||
.get_with_type_path(type_path)
|
||||
.ok_or_else(|| format!("component type `{type_path}` is not registered"))?;
|
||||
let reflect_component = registration
|
||||
.data::<ReflectComponent>()
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("component `{type_path}` has no ReflectComponent adapter"))?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
if value.type_path != type_path {
|
||||
return Err(format!(
|
||||
"clipboard type `{}` does not match `{type_path}`",
|
||||
value.type_path
|
||||
));
|
||||
}
|
||||
let mut deserializer = ron::de::Deserializer::from_str(&value.ron)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let reflected = TypedReflectDeserializer::new(registration, &type_registry)
|
||||
.deserialize(&mut deserializer)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let exists = reflect_component.reflect(world.entity(entity)).is_some();
|
||||
if exists {
|
||||
reflect_component.apply(world.entity_mut(entity), reflected.as_ref());
|
||||
} else {
|
||||
reflect_component.insert(
|
||||
&mut world.entity_mut(entity),
|
||||
reflected.as_ref(),
|
||||
&type_registry,
|
||||
);
|
||||
}
|
||||
}
|
||||
None => reflect_component.remove(&mut world.entity_mut(entity)),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts the reflected `Default` for a registered authoring component.
|
||||
pub fn apply_reflected_default(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
type_path: &str,
|
||||
) -> Result<(), String> {
|
||||
world.resource_scope(|world, app_registry: Mut<AppTypeRegistry>| {
|
||||
let type_registry = app_registry.read();
|
||||
let registration = type_registry
|
||||
.get_with_type_path(type_path)
|
||||
.ok_or_else(|| format!("component type `{type_path}` is not registered"))?;
|
||||
let reflect_component = registration
|
||||
.data::<ReflectComponent>()
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("component `{type_path}` has no ReflectComponent adapter"))?;
|
||||
let default = registration
|
||||
.data::<ReflectDefault>()
|
||||
.ok_or_else(|| format!("component `{type_path}` has no reflected Default"))?
|
||||
.default();
|
||||
let exists = reflect_component.reflect(world.entity(entity)).is_some();
|
||||
if exists {
|
||||
reflect_component.apply(world.entity_mut(entity), default.as_ref());
|
||||
} else {
|
||||
reflect_component.insert(
|
||||
&mut world.entity_mut(entity),
|
||||
default.as_ref(),
|
||||
&type_registry,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sync_actor_kind_hint(world: &mut World, entity: Entity) {
|
||||
let kind = world
|
||||
.get_entity(entity)
|
||||
.ok()
|
||||
.and_then(infer_actor_kind)
|
||||
.unwrap_or(ActorKind::Empty);
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut.insert(kind);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes one atomic reflected component edit and records all changed values,
|
||||
/// including the derived ActorKind presentation hint, as a single history item.
|
||||
pub fn reflected_component_transaction(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
label: &'static str,
|
||||
component_id: &str,
|
||||
type_path: &str,
|
||||
edit: impl FnOnce(&mut World, Entity) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
if !is_mutable_level_object(world, entity) {
|
||||
return Err("actor is not editable".to_string());
|
||||
}
|
||||
let tracked = [
|
||||
(component_id, type_path),
|
||||
(ACTOR_KIND_COMPONENT_ID, ACTOR_KIND_TYPE_PATH),
|
||||
];
|
||||
let before = tracked
|
||||
.iter()
|
||||
.map(|(id, path)| capture_reflected_component(world, entity, id, path))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if let Err(error) = edit(world, entity) {
|
||||
for ((_, path), value) in tracked.iter().zip(before.iter()) {
|
||||
let _ = apply_reflected_component(world, entity, path, value.as_ref());
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
sync_actor_kind_hint(world, entity);
|
||||
let after = tracked
|
||||
.iter()
|
||||
.map(|(id, path)| capture_reflected_component(world, entity, id, path))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let deltas = tracked
|
||||
.iter()
|
||||
.zip(before)
|
||||
.zip(after)
|
||||
.filter_map(|(((id, path), before), after)| {
|
||||
(before != after).then(|| ComponentDelta {
|
||||
component_id: (*id).to_string(),
|
||||
type_path: (*path).to_string(),
|
||||
before,
|
||||
after,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !deltas.is_empty() {
|
||||
push_history(
|
||||
world,
|
||||
EditorCommand::ComponentTransaction(ComponentTransaction {
|
||||
entity,
|
||||
label,
|
||||
deltas,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_component_transaction(
|
||||
world: &mut World,
|
||||
transaction: &ComponentTransaction,
|
||||
use_after: bool,
|
||||
) {
|
||||
for delta in &transaction.deltas {
|
||||
let value = if use_after {
|
||||
delta.after.as_ref()
|
||||
} else {
|
||||
delta.before.as_ref()
|
||||
};
|
||||
if let Err(error) =
|
||||
apply_reflected_component(world, transaction.entity, &delta.type_path, value)
|
||||
{
|
||||
world.resource_mut::<EditorHistory>().status = format!(
|
||||
"{} partially failed for {}: {error}",
|
||||
transaction.label, delta.component_id
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_scene_composition_with_history(world: &mut World, new: SceneComposition) {
|
||||
let old = world.resource::<SceneComposition>().clone();
|
||||
if old == new {
|
||||
@ -1570,7 +1248,7 @@ pub fn set_scene_composition_with_history(world: &mut World, new: SceneCompositi
|
||||
pub fn apply_command_undo(world: &mut World) {
|
||||
let candidate = world.resource::<EditorHistory>().undo_stack.last().cloned();
|
||||
if let Some(candidate) = candidate {
|
||||
if let Err(error) = prepare_prefab_source_history(world, &candidate, true) {
|
||||
if let Err(error) = prepare_prefab_source_history(&candidate, true) {
|
||||
world.resource_mut::<EditorHistory>().status = format!("Undo blocked: {error}");
|
||||
world
|
||||
.resource_mut::<SceneIo>()
|
||||
@ -1591,7 +1269,7 @@ pub fn apply_command_undo(world: &mut World) {
|
||||
pub fn apply_command_redo(world: &mut World) {
|
||||
let candidate = world.resource::<EditorHistory>().redo_stack.last().cloned();
|
||||
if let Some(candidate) = candidate {
|
||||
if let Err(error) = prepare_prefab_source_history(world, &candidate, false) {
|
||||
if let Err(error) = prepare_prefab_source_history(&candidate, false) {
|
||||
world.resource_mut::<EditorHistory>().status = format!("Redo blocked: {error}");
|
||||
world
|
||||
.resource_mut::<SceneIo>()
|
||||
@ -1609,13 +1287,8 @@ pub fn apply_command_redo(world: &mut World) {
|
||||
mark_dirty(world);
|
||||
}
|
||||
|
||||
fn prepare_prefab_source_history(
|
||||
world: &mut World,
|
||||
command: &EditorCommand,
|
||||
undo: bool,
|
||||
) -> Result<(), String> {
|
||||
fn prepare_prefab_source_history(command: &EditorCommand, undo: bool) -> Result<(), String> {
|
||||
let EditorCommand::ApplyPrefabToSource {
|
||||
entity,
|
||||
source_path,
|
||||
before_source,
|
||||
after_source,
|
||||
@ -1629,18 +1302,15 @@ fn prepare_prefab_source_history(
|
||||
} else {
|
||||
(before_source, after_source)
|
||||
};
|
||||
let expected =
|
||||
crate::collaboration::FileSnapshot::from_loaded_bytes(source_path, expected.as_bytes());
|
||||
crate::collaboration::publish_authored_file(
|
||||
world,
|
||||
source_path,
|
||||
replacement.as_bytes(),
|
||||
&expected,
|
||||
crate::collaboration::FileWriteIntent::PrefabSourceHistory {
|
||||
instance_root: *entity,
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
let current = std::fs::read_to_string(source_path)
|
||||
.map_err(|error| format!("could not read {}: {error}", source_path.display()))?;
|
||||
if ¤t != expected {
|
||||
return Err(format!(
|
||||
"{} changed outside this history command",
|
||||
source_path.display()
|
||||
));
|
||||
}
|
||||
crate::scene::recovery::atomic_write(source_path, replacement.as_bytes())
|
||||
}
|
||||
|
||||
fn undo_command(world: &mut World, command: &mut EditorCommand) {
|
||||
@ -1702,9 +1372,6 @@ fn undo_command(world: &mut World, command: &mut EditorCommand) {
|
||||
EditorCommand::SetPostProcessVolume { entity, old, .. } => {
|
||||
apply_post_process_volume(world, *entity, old);
|
||||
}
|
||||
EditorCommand::SetNavigation { entity, old, .. } => {
|
||||
apply_navigation_state(world, *entity, old);
|
||||
}
|
||||
EditorCommand::SetPhysics { entity, old, .. } => {
|
||||
apply_physics(world, *entity, old);
|
||||
}
|
||||
@ -1818,9 +1485,6 @@ fn undo_command(world: &mut World, command: &mut EditorCommand) {
|
||||
EditorCommand::RemoveComponent { entity, snapshot } => {
|
||||
apply_authored_components(world, *entity, snapshot);
|
||||
}
|
||||
EditorCommand::ComponentTransaction(transaction) => {
|
||||
apply_component_transaction(world, transaction, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1909,9 +1573,6 @@ fn redo_command(world: &mut World, command: &mut EditorCommand) {
|
||||
entity_mut.insert(new.clone());
|
||||
}
|
||||
}
|
||||
EditorCommand::SetNavigation { entity, new, .. } => {
|
||||
apply_navigation_state(world, *entity, new);
|
||||
}
|
||||
EditorCommand::SetPhysics { entity, new, .. } => {
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(*entity) {
|
||||
entity_mut.insert(new.clone());
|
||||
@ -2045,9 +1706,6 @@ fn redo_command(world: &mut World, command: &mut EditorCommand) {
|
||||
EditorCommand::RemoveComponent { entity, snapshot } => {
|
||||
remove_authored_components(world, *entity, snapshot);
|
||||
}
|
||||
EditorCommand::ComponentTransaction(transaction) => {
|
||||
apply_component_transaction(world, transaction, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2204,45 +1862,6 @@ fn apply_post_process_volume(
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_navigation_state(world: &mut World, entity: Entity, state: &NavigationComponentState) {
|
||||
let has_navigation = state.bounds.is_some()
|
||||
|| state.obstacle.is_some()
|
||||
|| state.area.is_some()
|
||||
|| state.link.is_some();
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut
|
||||
.remove::<NavigationBounds>()
|
||||
.remove::<NavigationObstacle>()
|
||||
.remove::<NavigationArea>()
|
||||
.remove::<NavigationLink>();
|
||||
if let Some(value) = &state.bounds {
|
||||
entity_mut.insert(value.clone());
|
||||
}
|
||||
if let Some(value) = &state.obstacle {
|
||||
entity_mut.insert(value.clone());
|
||||
}
|
||||
if let Some(value) = &state.area {
|
||||
entity_mut.insert(value.clone());
|
||||
}
|
||||
if let Some(value) = &state.link {
|
||||
entity_mut.insert(value.clone());
|
||||
}
|
||||
if has_navigation {
|
||||
entity_mut.insert(ActorKind::Navigation);
|
||||
}
|
||||
}
|
||||
if !has_navigation {
|
||||
let fallback = world
|
||||
.get_entity(entity)
|
||||
.ok()
|
||||
.and_then(infer_actor_kind)
|
||||
.unwrap_or(ActorKind::Empty);
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut.insert(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_physics(world: &mut World, entity: Entity, physics: &Option<PhysicsBody>) {
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
match physics {
|
||||
@ -2367,9 +1986,6 @@ fn apply_authored_components(world: &mut World, entity: Entity, snapshot: &Edito
|
||||
if let Some(v) = &snapshot.static_mesh_renderer {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.skinned_mesh_renderer {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.material {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
@ -2424,24 +2040,6 @@ fn apply_authored_components(world: &mut World, entity: Entity, snapshot: &Edito
|
||||
if let Some(v) = &snapshot.objective {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.navigation_bounds {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.navigation_obstacle {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.navigation_area {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.navigation_link {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.inspector_order {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
if let Some(v) = &snapshot.component_states {
|
||||
entity_mut.insert(v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2463,9 +2061,6 @@ fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &Edit
|
||||
if snapshot.static_mesh_renderer.is_some() {
|
||||
entity_mut.remove::<StaticMeshRenderer>();
|
||||
}
|
||||
if snapshot.skinned_mesh_renderer.is_some() {
|
||||
entity_mut.remove::<SkinnedMeshRenderer>();
|
||||
}
|
||||
if snapshot.material.is_some() {
|
||||
entity_mut.remove::<MaterialDesc>();
|
||||
}
|
||||
@ -2520,24 +2115,6 @@ fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &Edit
|
||||
if snapshot.objective.is_some() {
|
||||
entity_mut.remove::<ObjectiveMarker>();
|
||||
}
|
||||
if snapshot.navigation_bounds.is_some() {
|
||||
entity_mut.remove::<NavigationBounds>();
|
||||
}
|
||||
if snapshot.navigation_obstacle.is_some() {
|
||||
entity_mut.remove::<NavigationObstacle>();
|
||||
}
|
||||
if snapshot.navigation_area.is_some() {
|
||||
entity_mut.remove::<NavigationArea>();
|
||||
}
|
||||
if snapshot.navigation_link.is_some() {
|
||||
entity_mut.remove::<NavigationLink>();
|
||||
}
|
||||
if snapshot.inspector_order.is_some() {
|
||||
entity_mut.remove::<InspectorOrder>();
|
||||
}
|
||||
if snapshot.component_states.is_some() {
|
||||
entity_mut.remove::<AuthoringComponentStates>();
|
||||
}
|
||||
}
|
||||
if removed_dedicated_audio_kind {
|
||||
let fallback = world
|
||||
@ -2806,90 +2383,6 @@ mod tests {
|
||||
use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
|
||||
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq)]
|
||||
#[reflect(Component, Default)]
|
||||
struct StaticExtensionFixture {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflected_extension_component_transaction_round_trips_atomically() {
|
||||
let mut app = App::new();
|
||||
app.register_type::<ActorKind>()
|
||||
.register_type::<StaticExtensionFixture>();
|
||||
let world = app.world_mut();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
let entity = world
|
||||
.spawn((LevelObject, ActorKind::Light, Transform::default()))
|
||||
.id();
|
||||
let type_path = std::any::type_name::<StaticExtensionFixture>();
|
||||
|
||||
reflected_component_transaction(
|
||||
world,
|
||||
entity,
|
||||
"Add Fixture",
|
||||
"game.static_extension_fixture",
|
||||
type_path,
|
||||
|world, entity| {
|
||||
world
|
||||
.entity_mut(entity)
|
||||
.insert(StaticExtensionFixture { value: 42 });
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
world.get::<StaticExtensionFixture>(entity).unwrap().value,
|
||||
42
|
||||
);
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Empty));
|
||||
assert_eq!(world.resource::<EditorHistory>().undo_depth(), 1);
|
||||
|
||||
apply_command_undo(world);
|
||||
assert!(world.get::<StaticExtensionFixture>(entity).is_none());
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Light));
|
||||
|
||||
apply_command_redo(world);
|
||||
assert_eq!(
|
||||
world.get::<StaticExtensionFixture>(entity).unwrap().value,
|
||||
42
|
||||
);
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_preserve_component_order_and_independent_active_state() {
|
||||
let mut world = World::new();
|
||||
let order = InspectorOrder {
|
||||
component_ids: vec![shared::AUTHORING_COMPONENT_LIGHT.to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let mut states = AuthoringComponentStates::default();
|
||||
states.set_component_active(shared::COMPONENT_LIGHT_DESC, false);
|
||||
let entity = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::Light,
|
||||
Transform::default(),
|
||||
LightDesc::default(),
|
||||
order.clone(),
|
||||
states.clone(),
|
||||
))
|
||||
.id();
|
||||
|
||||
let snapshot = snapshot_entity(&world, entity).unwrap();
|
||||
world.entity_mut(entity).despawn();
|
||||
let restored = spawn_snapshot(&mut world, &snapshot);
|
||||
|
||||
assert_eq!(world.get::<InspectorOrder>(restored), Some(&order));
|
||||
assert_eq!(
|
||||
world.get::<AuthoringComponentStates>(restored),
|
||||
Some(&states)
|
||||
);
|
||||
}
|
||||
|
||||
fn animation_controller(state_id: &str, crossfade_seconds: f32) -> AnimationControllerDesc {
|
||||
AnimationControllerDesc {
|
||||
skeleton: Some(
|
||||
@ -2927,7 +2420,7 @@ mod tests {
|
||||
let entity = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::SkinnedMesh,
|
||||
ActorKind::ImportedModel,
|
||||
Transform::IDENTITY,
|
||||
old.clone(),
|
||||
))
|
||||
@ -2946,36 +2439,6 @@ mod tests {
|
||||
assert_eq!(world.get::<AnimationControllerDesc>(entity), Some(&new));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_component_history_round_trips_and_preserves_kind() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
let entity = world
|
||||
.spawn((LevelObject, ActorKind::Empty, Transform::IDENTITY))
|
||||
.id();
|
||||
let bounds = NavigationBounds::default();
|
||||
|
||||
set_navigation_with_history(
|
||||
&mut world,
|
||||
entity,
|
||||
NavigationComponentState {
|
||||
bounds: Some(bounds.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(world.get::<NavigationBounds>(entity), Some(&bounds));
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Navigation));
|
||||
|
||||
apply_command_undo(&mut world);
|
||||
assert!(world.get::<NavigationBounds>(entity).is_none());
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Empty));
|
||||
|
||||
apply_command_redo(&mut world);
|
||||
assert_eq!(world.get::<NavigationBounds>(entity), Some(&bounds));
|
||||
assert_eq!(world.get::<ActorKind>(entity), Some(&ActorKind::Navigation));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undoing_a_new_animation_controller_removes_the_component() {
|
||||
let mut world = World::new();
|
||||
@ -2983,7 +2446,7 @@ mod tests {
|
||||
world.init_resource::<SceneIo>();
|
||||
let controller = animation_controller("idle", 0.2);
|
||||
let entity = world
|
||||
.spawn((LevelObject, ActorKind::SkinnedMesh, Transform::IDENTITY))
|
||||
.spawn((LevelObject, ActorKind::ImportedModel, Transform::IDENTITY))
|
||||
.id();
|
||||
|
||||
set_animation_controller_with_history(&mut world, entity, controller.clone());
|
||||
@ -3004,10 +2467,9 @@ mod tests {
|
||||
let source = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::SkinnedMesh,
|
||||
ActorKind::ImportedModel,
|
||||
Name::new("Animated Robot"),
|
||||
Transform::from_xyz(1.0, 2.0, 3.0),
|
||||
SkinnedMeshRenderer::new("assets/models/robot.glb").with_asset_id("robot-asset"),
|
||||
controller.clone(),
|
||||
))
|
||||
.id();
|
||||
@ -3024,10 +2486,6 @@ mod tests {
|
||||
world.get::<Name>(respawned).map(Name::as_str),
|
||||
Some("Animated Robot")
|
||||
);
|
||||
assert_eq!(
|
||||
world.get::<SkinnedMeshRenderer>(respawned),
|
||||
Some(&SkinnedMeshRenderer::new("assets/models/robot.glb").with_asset_id("robot-asset"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -3555,73 +3013,6 @@ mod tests {
|
||||
assert_eq!(unique.len(), ids.len(), "duplicated actors need fresh IDs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_navigation_bounds_gets_actor_owned_artifact_path() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<EditorHistory>();
|
||||
world.init_resource::<SceneIo>();
|
||||
world.init_resource::<SelectedEntity>();
|
||||
let original_id = ActorId::new("navigation-bounds-original");
|
||||
let original_bounds = NavigationBounds::for_actor(&original_id.0);
|
||||
let original = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::Navigation,
|
||||
original_id,
|
||||
Transform::IDENTITY,
|
||||
original_bounds.clone(),
|
||||
))
|
||||
.id();
|
||||
|
||||
duplicate_entities_with_history(&mut world, &[original]);
|
||||
|
||||
let (duplicate_id, duplicate_bounds) = world
|
||||
.query::<(Entity, &ActorId, &NavigationBounds)>()
|
||||
.iter(&world)
|
||||
.find_map(|(entity, actor_id, bounds)| {
|
||||
(entity != original).then_some((actor_id.clone(), bounds.clone()))
|
||||
})
|
||||
.expect("duplicated navigation bounds should exist");
|
||||
assert_ne!(
|
||||
duplicate_bounds.artifact_path,
|
||||
original_bounds.artifact_path
|
||||
);
|
||||
assert_eq!(
|
||||
duplicate_bounds.artifact_path,
|
||||
shared::navigation_artifact_path_for_actor(&duplicate_id.0)
|
||||
);
|
||||
|
||||
apply_command_undo(&mut world);
|
||||
apply_command_redo(&mut world);
|
||||
let redone = world
|
||||
.query::<(&ActorId, &NavigationBounds)>()
|
||||
.iter(&world)
|
||||
.find(|(actor_id, _)| **actor_id == duplicate_id)
|
||||
.expect("redo should preserve the duplicate actor identity");
|
||||
assert_eq!(redone.1, &duplicate_bounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_navigation_snapshot_keeps_deliberate_generated_path() {
|
||||
let mut world = World::new();
|
||||
let mut bounds = NavigationBounds::for_actor("bounds-existing");
|
||||
bounds.artifact_path = "assets/navigation/generated/custom-zone.nav.ron".into();
|
||||
let entity = world
|
||||
.spawn((
|
||||
LevelObject,
|
||||
ActorKind::Navigation,
|
||||
ActorId::new("bounds-existing"),
|
||||
Transform::IDENTITY,
|
||||
bounds.clone(),
|
||||
))
|
||||
.id();
|
||||
let mut snapshot = snapshot_entity(&world, entity).unwrap();
|
||||
|
||||
assign_missing_actor_ids(&mut snapshot);
|
||||
|
||||
assert_eq!(snapshot.navigation_bounds.as_ref(), Some(&bounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_members_reject_direct_history_mutations() {
|
||||
let mut world = World::new();
|
||||
|
||||
@ -22,7 +22,6 @@ pub use ext::extensibility;
|
||||
pub use ext::hot_reload;
|
||||
pub use play::net_editor;
|
||||
pub use play::state;
|
||||
pub use project::collaboration;
|
||||
pub use project::diagnostics_bundle;
|
||||
pub use project::launcher;
|
||||
pub use project::project_io;
|
||||
@ -65,7 +64,6 @@ use infra::EditorInfraPlugin;
|
||||
use operators::OperatorPlugin;
|
||||
use play::audio_preview::AudioPreviewPlugin;
|
||||
use play::PlaySessionPlugin;
|
||||
use project::collaboration::CollaborationPlugin;
|
||||
use project_io::ProjectIoPlugin;
|
||||
use render_view::RenderViewPlugin;
|
||||
use scene_io::SceneIoPlugin;
|
||||
@ -113,7 +111,6 @@ impl PluginGroup for EditorPluginGroup {
|
||||
.add(EditorVisualizersPlugin)
|
||||
.add(EditorHistoryPlugin)
|
||||
.add(SceneIoPlugin)
|
||||
.add(CollaborationPlugin)
|
||||
.add(EditorUiPlugin)
|
||||
.add(EditorSessionPlugin)
|
||||
.add(brp::BrpPlugin);
|
||||
|
||||
@ -6,10 +6,7 @@ use bevy::window::WindowRef;
|
||||
use game::player::PlayerCamera;
|
||||
use protocol::PlayerInputIntent;
|
||||
use settings::SimTuning;
|
||||
use shared::{
|
||||
authoring_component_active, AuthoringComponentStates, InspectorOrder, PlayerSpawn,
|
||||
COMPONENT_PLAYER_SPAWN,
|
||||
};
|
||||
use shared::{inspector_component_active, InspectorOrder, PlayerSpawn, COMPONENT_PLAYER_SPAWN};
|
||||
use sim::{Crouching, Grounded, JumpState, Player, PlayerVelocity};
|
||||
|
||||
use crate::render_target::ViewportRenderTarget;
|
||||
@ -69,16 +66,10 @@ fn bootstrap_player_on_play(world: &mut World) {
|
||||
.unwrap_or_else(|| SimTuning::from_physics(&settings::PhysicsSettings::default()));
|
||||
|
||||
let spawn = world
|
||||
.query_filtered::<(
|
||||
&Transform,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
), With<PlayerSpawn>>()
|
||||
.query_filtered::<(&Transform, Option<&InspectorOrder>), With<PlayerSpawn>>()
|
||||
.iter(world)
|
||||
.filter(|(_, states, legacy_order)| {
|
||||
authoring_component_active(*states, *legacy_order, COMPONENT_PLAYER_SPAWN)
|
||||
})
|
||||
.map(|(transform, _, _)| transform)
|
||||
.filter(|(_, order)| inspector_component_active(*order, COMPONENT_PLAYER_SPAWN))
|
||||
.map(|(transform, _)| transform)
|
||||
.next()
|
||||
.copied()
|
||||
.unwrap_or_else(default_player_spawn);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
||||
//! Project workspace I/O, settings UI, and recent-level sync.
|
||||
|
||||
pub mod collaboration;
|
||||
pub mod diagnostics_bundle;
|
||||
pub mod launcher;
|
||||
pub mod project_io;
|
||||
|
||||
@ -1,28 +1,18 @@
|
||||
//! Editor-only Project Settings panel (egui). Data lives in the `settings` crate.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::egui;
|
||||
use game_hot::sync_project_sun_from_settings;
|
||||
use settings::{
|
||||
save_project_settings_to_string, sync_sim_tuning, ExposureMode, ProjectSettings,
|
||||
ProjectSettingsChanged, ProjectSettingsIo,
|
||||
load_project_settings_from_path, save_project_settings, sync_sim_tuning, ExposureMode,
|
||||
ProjectSettings, ProjectSettingsChanged, ProjectSettingsIo,
|
||||
};
|
||||
|
||||
use crate::project::collaboration::{
|
||||
file_status_indicator_ui, publish_authored_file, CollaborationState, FileSnapshot,
|
||||
FileWriteIntent,
|
||||
};
|
||||
use crate::scene_io::SceneIo;
|
||||
|
||||
/// Whether the Project Settings window is visible (editor UI state only).
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct ProjectSettingsPanel {
|
||||
pub open: bool,
|
||||
pub draft: Option<ProjectSettings>,
|
||||
disk_snapshot: Option<FileSnapshot>,
|
||||
save_error: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SettingsUiPlugin;
|
||||
@ -49,23 +39,11 @@ pub fn project_settings_window(
|
||||
) {
|
||||
if !panel.open {
|
||||
panel.draft = None;
|
||||
panel.disk_snapshot = None;
|
||||
panel.save_error = None;
|
||||
return;
|
||||
}
|
||||
|
||||
if panel.draft.is_none() {
|
||||
panel.draft = Some(world.resource::<ProjectSettings>().clone());
|
||||
let io = world.resource::<ProjectSettingsIo>();
|
||||
panel.disk_snapshot = Some(
|
||||
io.loaded_source
|
||||
.as_deref()
|
||||
.map(|source| {
|
||||
FileSnapshot::from_loaded_bytes(Path::new(&io.path), source.as_bytes())
|
||||
})
|
||||
.unwrap_or_else(FileSnapshot::missing),
|
||||
);
|
||||
panel.save_error = None;
|
||||
}
|
||||
|
||||
let mut open = panel.open;
|
||||
@ -73,33 +51,17 @@ pub fn project_settings_window(
|
||||
.open(&mut open)
|
||||
.default_width(420.0)
|
||||
.show(ctx, |ui| {
|
||||
let ProjectSettingsPanel {
|
||||
draft: Some(draft),
|
||||
disk_snapshot,
|
||||
save_error,
|
||||
..
|
||||
} = panel
|
||||
else {
|
||||
let Some(draft) = panel.draft.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let io_path = world.resource::<ProjectSettingsIo>().path.clone();
|
||||
let mut io_dirty = world.resource::<ProjectSettingsIo>().dirty;
|
||||
draw_project_settings_form(
|
||||
world,
|
||||
ui,
|
||||
draft,
|
||||
&io_path,
|
||||
&mut io_dirty,
|
||||
disk_snapshot,
|
||||
save_error,
|
||||
);
|
||||
draw_project_settings_form(world, ui, draft, &io_path, &mut io_dirty);
|
||||
world.resource_mut::<ProjectSettingsIo>().dirty = io_dirty;
|
||||
});
|
||||
panel.open = open;
|
||||
if !open {
|
||||
panel.draft = None;
|
||||
panel.disk_snapshot = None;
|
||||
panel.save_error = None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,26 +71,12 @@ fn draw_project_settings_form(
|
||||
draft: &mut ProjectSettings,
|
||||
io_path: &str,
|
||||
io_dirty: &mut bool,
|
||||
disk_snapshot: &mut Option<FileSnapshot>,
|
||||
save_error: &mut Option<String>,
|
||||
) {
|
||||
let caps = world
|
||||
.get_resource::<settings::RenderingCapabilities>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.small(egui::RichText::new(io_path).monospace());
|
||||
if let Some(state) = world.get_resource::<CollaborationState>() {
|
||||
let status = state.file_status(Path::new(io_path));
|
||||
let _ = file_status_indicator_ui(ui, &status, Path::new(io_path));
|
||||
}
|
||||
});
|
||||
if let Some(error) = save_error.as_deref() {
|
||||
ui.colored_label(crate::ui::theme::ERROR, error);
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
ui.collapsing("Global Illumination", |ui| {
|
||||
egui::ComboBox::from_label("GiMode")
|
||||
.selected_text(format!("{:?}", draft.rendering.gi_mode))
|
||||
@ -387,116 +335,25 @@ fn draw_project_settings_form(
|
||||
.clicked()
|
||||
{
|
||||
commit_project_settings(world, draft);
|
||||
*io_dirty = true;
|
||||
let settings = world.resource::<ProjectSettings>().clone();
|
||||
let Some(expected) = disk_snapshot.clone() else {
|
||||
let error = "Project settings disk baseline is unavailable; reload before saving";
|
||||
*save_error = Some(error.into());
|
||||
world.resource_mut::<SceneIo>().set_status(error);
|
||||
return;
|
||||
};
|
||||
match persist_project_settings(world, &settings, &expected) {
|
||||
Ok((snapshot, source)) => {
|
||||
let mut io = world.resource_mut::<ProjectSettingsIo>();
|
||||
io.dirty = false;
|
||||
io.loaded_source = Some(source);
|
||||
*disk_snapshot = Some(snapshot);
|
||||
*save_error = None;
|
||||
*io_dirty = false;
|
||||
world
|
||||
.resource_mut::<SceneIo>()
|
||||
.set_status(format!("Saved project settings to {io_path}"));
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Save project settings failed: {error}");
|
||||
*save_error = Some(error.clone());
|
||||
world
|
||||
.resource_mut::<SceneIo>()
|
||||
.set_status(format!("Save project settings failed: {error}"));
|
||||
}
|
||||
let mut io = world.resource_mut::<ProjectSettingsIo>();
|
||||
if let Err(error) = save_project_settings(&settings, &mut io) {
|
||||
warn!("Save project settings failed: {error}");
|
||||
} else {
|
||||
*io_dirty = false;
|
||||
}
|
||||
}
|
||||
if ui.button("Revert from disk").clicked() {
|
||||
match read_project_settings_document(Path::new(io_path)) {
|
||||
Ok((settings, source, snapshot)) => {
|
||||
*draft = settings;
|
||||
commit_project_settings(world, draft);
|
||||
let mut io = world.resource_mut::<ProjectSettingsIo>();
|
||||
io.dirty = false;
|
||||
io.loaded_source = Some(source);
|
||||
*disk_snapshot = Some(snapshot);
|
||||
*save_error = None;
|
||||
*io_dirty = false;
|
||||
}
|
||||
Err(error) => {
|
||||
*save_error = Some(error.clone());
|
||||
world
|
||||
.resource_mut::<SceneIo>()
|
||||
.set_status(format!("Revert project settings failed: {error}"));
|
||||
}
|
||||
}
|
||||
*draft = load_project_settings_from_path(io_path);
|
||||
commit_project_settings(world, draft);
|
||||
world.resource_mut::<ProjectSettingsIo>().dirty = false;
|
||||
*io_dirty = false;
|
||||
}
|
||||
if *io_dirty {
|
||||
ui.label(egui::RichText::new("Unsaved changes").color(egui::Color32::YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_project_settings(
|
||||
world: &mut World,
|
||||
settings: &ProjectSettings,
|
||||
expected: &FileSnapshot,
|
||||
) -> Result<(FileSnapshot, String), String> {
|
||||
let path = world.resource::<ProjectSettingsIo>().path.clone();
|
||||
let source = save_project_settings_to_string(settings).map_err(|error| error.to_string())?;
|
||||
let snapshot = publish_authored_file(
|
||||
world,
|
||||
Path::new(&path),
|
||||
source.as_bytes(),
|
||||
expected,
|
||||
FileWriteIntent::ProjectSettings,
|
||||
)?;
|
||||
Ok((snapshot, source))
|
||||
}
|
||||
|
||||
fn read_project_settings_document(
|
||||
path: &Path,
|
||||
) -> Result<(ProjectSettings, String, FileSnapshot), String> {
|
||||
let source = std::fs::read_to_string(path)
|
||||
.map_err(|error| format!("could not read {}: {error}", path.display()))?;
|
||||
let settings: ProjectSettings = ron::from_str(&source)
|
||||
.map_err(|error| format!("invalid project settings in {}: {error}", path.display()))?;
|
||||
let snapshot = FileSnapshot::from_loaded_bytes(path, source.as_bytes());
|
||||
Ok((settings, source, snapshot))
|
||||
}
|
||||
|
||||
pub(crate) fn reload_project_settings_after_file_conflict(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
) -> Result<String, String> {
|
||||
let active_path = world.resource::<ProjectSettingsIo>().path.clone();
|
||||
if Path::new(&active_path) != path {
|
||||
return Err("the project settings conflict no longer matches the active manifest".into());
|
||||
}
|
||||
let (settings, source, snapshot) = read_project_settings_document(path)?;
|
||||
commit_project_settings(world, &settings);
|
||||
{
|
||||
let mut io = world.resource_mut::<ProjectSettingsIo>();
|
||||
io.dirty = false;
|
||||
io.loaded_source = Some(source);
|
||||
}
|
||||
{
|
||||
let mut panel = world.resource_mut::<ProjectSettingsPanel>();
|
||||
panel.draft = Some(settings);
|
||||
panel.disk_snapshot = Some(snapshot);
|
||||
panel.save_error = None;
|
||||
}
|
||||
Ok(format!("Reloaded project settings from {}", path.display()))
|
||||
}
|
||||
|
||||
pub(crate) fn project_settings_conflict_copy_saved(world: &mut World) {
|
||||
world.resource_mut::<ProjectSettingsPanel>().save_error = None;
|
||||
}
|
||||
|
||||
fn commit_project_settings(world: &mut World, draft: &ProjectSettings) {
|
||||
let previous = world.resource::<ProjectSettings>().rendering.gi_mode;
|
||||
*world.resource_mut::<ProjectSettings>() = draft.clone();
|
||||
@ -520,11 +377,7 @@ fn apply_project_settings_changes(
|
||||
settings: Res<ProjectSettings>,
|
||||
tuning: ResMut<settings::SimTuning>,
|
||||
scene_suns: Query<
|
||||
(
|
||||
&shared::LightDesc,
|
||||
Option<&shared::AuthoringComponentStates>,
|
||||
Option<&shared::InspectorOrder>,
|
||||
),
|
||||
(&shared::LightDesc, Option<&shared::InspectorOrder>),
|
||||
With<shared::LevelObject>,
|
||||
>,
|
||||
project_suns: Query<(&mut DirectionalLight, &mut Visibility), With<shared::ProjectSun>>,
|
||||
@ -549,35 +402,3 @@ fn apply_project_settings_changes(
|
||||
// Full stack refresh runs next frame via `render_view::sync_project_render_view`
|
||||
// when `ProjectSettings` changes.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn project_settings_save_refuses_an_external_revision() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-project-settings-external-write-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let path = root.join("assets/project.ron");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let loaded = save_project_settings_to_string(&ProjectSettings::default()).unwrap();
|
||||
std::fs::write(&path, &loaded).unwrap();
|
||||
let expected = FileSnapshot::from_loaded_bytes(&path, loaded.as_bytes());
|
||||
let mut world = World::new();
|
||||
world.insert_resource(ProjectSettingsIo {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
loaded_source: Some(loaded),
|
||||
..Default::default()
|
||||
});
|
||||
world.insert_resource(SceneIo::default());
|
||||
std::fs::write(&path, b"external").unwrap();
|
||||
|
||||
let result = persist_project_settings(&mut world, &ProjectSettings::default(), &expected);
|
||||
|
||||
assert!(result.unwrap_err().contains("changed outside Blacksite"));
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"external");
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,20 +13,17 @@ pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
atomic_write_with_pre_rename(path, bytes, || Ok(()))
|
||||
}
|
||||
|
||||
pub(crate) fn atomic_write_with_pre_rename<E>(
|
||||
fn atomic_write_with_pre_rename(
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
before_rename: impl FnOnce() -> Result<(), E>,
|
||||
) -> Result<(), E>
|
||||
where
|
||||
E: From<String>,
|
||||
{
|
||||
before_rename: impl FnOnce() -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| E::from(format!("could not create {}: {error}", parent.display())))?;
|
||||
.map_err(|error| format!("could not create {}: {error}", parent.display()))?;
|
||||
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@ -43,21 +40,18 @@ where
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&temporary)
|
||||
.map_err(|error| {
|
||||
E::from(format!("could not create {}: {error}", temporary.display()))
|
||||
})?;
|
||||
file.write_all(bytes).map_err(|error| {
|
||||
E::from(format!("could not write {}: {error}", temporary.display()))
|
||||
})?;
|
||||
.map_err(|error| format!("could not create {}: {error}", temporary.display()))?;
|
||||
file.write_all(bytes)
|
||||
.map_err(|error| format!("could not write {}: {error}", temporary.display()))?;
|
||||
file.sync_all()
|
||||
.map_err(|error| E::from(format!("could not sync {}: {error}", temporary.display())))?;
|
||||
.map_err(|error| format!("could not sync {}: {error}", temporary.display()))?;
|
||||
before_rename()?;
|
||||
fs::rename(&temporary, path).map_err(|error| {
|
||||
E::from(format!(
|
||||
format!(
|
||||
"could not replace {} with {}: {error}",
|
||||
path.display(),
|
||||
temporary.display()
|
||||
))
|
||||
)
|
||||
})?;
|
||||
sync_parent_directory(parent);
|
||||
Ok(())
|
||||
|
||||
@ -6,26 +6,23 @@ use bevy::ecs::system::SystemState;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
use bevy::world_serialization::serde::WorldDeserializer;
|
||||
use bevy::world_serialization::{DynamicWorld, DynamicWorldBuilder, WorldFilter, WorldInstance};
|
||||
use bevy::world_serialization::{DynamicWorld, DynamicWorldBuilder, WorldInstance};
|
||||
use scene::{document::SceneDocument, strip_schema_version, validate_level_text};
|
||||
use serde::de::DeserializeSeed;
|
||||
use shared::{
|
||||
infer_actor_kind, validate_actor, ActorId, ActorKind, ActorName, ActorValidationError,
|
||||
AuthoringComponentStates, EditorVisibility, HierarchySiblingIndex, HydratedPrefabMember,
|
||||
HydratedPrefabReady, InspectorOrder, LevelObject, LightDesc, PrefabHydrationBlocked,
|
||||
PrefabInstance, PrefabRef, SceneComposition,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
use shared::{
|
||||
AnimationControllerDesc, AudioSourceDesc, BrushDesc, ColliderDesc, SkinnedMeshRenderer,
|
||||
AnimationControllerDesc, AudioListenerDesc, AudioSourceDesc, BrushDesc, ColliderDesc,
|
||||
EditorVisibility, HierarchySiblingIndex, HydratedPrefabMember, HydratedPrefabReady,
|
||||
InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride, ModelRef,
|
||||
ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabHydrationBlocked,
|
||||
PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, SceneComposition, StaticMeshRenderer,
|
||||
TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
};
|
||||
|
||||
use crate::assets::{import_external_assets, EditorAssets, IMPORTABLE_ASSET_EXTENSIONS};
|
||||
use crate::history::{clear_level_objects, snapshot_entity, EditorHistory};
|
||||
use crate::project::collaboration::{publish_authored_file, FileSnapshot, FileWriteIntent};
|
||||
use crate::scene::recovery::{
|
||||
default_state_root, discard_recovery_snapshots, latest_recovery_snapshot,
|
||||
atomic_write, default_state_root, discard_recovery_snapshots, latest_recovery_snapshot,
|
||||
write_recovery_snapshot,
|
||||
};
|
||||
use crate::selection::SelectedEntity;
|
||||
@ -61,7 +58,6 @@ pub struct SceneTab {
|
||||
pub dirty: bool,
|
||||
snapshot: String,
|
||||
recovery_snapshot: Option<PathBuf>,
|
||||
disk_snapshot: Option<FileSnapshot>,
|
||||
}
|
||||
|
||||
impl SceneTab {
|
||||
@ -90,7 +86,6 @@ pub struct SceneIo {
|
||||
pub events: VecDeque<SceneIoEvent>,
|
||||
pub tabs: Vec<SceneTab>,
|
||||
pub active_tab: usize,
|
||||
change_revision: u64,
|
||||
next_event_id: u64,
|
||||
next_tab_id: u64,
|
||||
}
|
||||
@ -128,10 +123,8 @@ impl Default for SceneIo {
|
||||
dirty: false,
|
||||
snapshot: String::new(),
|
||||
recovery_snapshot: None,
|
||||
disk_snapshot: None,
|
||||
}],
|
||||
active_tab: 0,
|
||||
change_revision: 0,
|
||||
next_event_id: 1,
|
||||
next_tab_id: 2,
|
||||
}
|
||||
@ -141,20 +134,14 @@ impl Default for SceneIo {
|
||||
impl SceneIo {
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.dirty = true;
|
||||
self.change_revision = self.change_revision.wrapping_add(1);
|
||||
self.sync_active_tab_metadata();
|
||||
}
|
||||
|
||||
pub fn mark_clean(&mut self) {
|
||||
self.dirty = false;
|
||||
self.change_revision = self.change_revision.wrapping_add(1);
|
||||
self.sync_active_tab_metadata();
|
||||
}
|
||||
|
||||
pub fn change_revision(&self) -> u64 {
|
||||
self.change_revision
|
||||
}
|
||||
|
||||
pub fn active_path_label(&self) -> String {
|
||||
self.active_path
|
||||
.as_ref()
|
||||
@ -255,14 +242,9 @@ fn load_startup_scene(world: &mut World) {
|
||||
let path = active_path.unwrap_or(default_path);
|
||||
|
||||
match load_level(world, &path) {
|
||||
Ok(disk_snapshot) => {
|
||||
{
|
||||
let mut io = world.resource_mut::<SceneIo>();
|
||||
io.active_path = Some(path.clone());
|
||||
let active_tab = io.active_tab;
|
||||
io.tabs[active_tab].disk_snapshot = Some(disk_snapshot);
|
||||
io.mark_clean();
|
||||
}
|
||||
Ok(()) => {
|
||||
world.resource_mut::<SceneIo>().active_path = Some(path.clone());
|
||||
world.resource_mut::<SceneIo>().mark_clean();
|
||||
remember_path(world, path.clone());
|
||||
world.resource_mut::<EditorHistory>().clear();
|
||||
refresh_recovery_notice(world, &path);
|
||||
@ -404,7 +386,7 @@ fn open_path(world: &mut World, path: PathBuf) -> String {
|
||||
return format!("Open failed: could not preserve active scene: {error}");
|
||||
}
|
||||
match load_level(world, &path) {
|
||||
Ok(disk_snapshot) => {
|
||||
Ok(()) => {
|
||||
let id = world.resource_mut::<SceneIo>().allocate_tab_id();
|
||||
{
|
||||
let mut io = world.resource_mut::<SceneIo>();
|
||||
@ -414,7 +396,6 @@ fn open_path(world: &mut World, path: PathBuf) -> String {
|
||||
dirty: false,
|
||||
snapshot: String::new(),
|
||||
recovery_snapshot: None,
|
||||
disk_snapshot: Some(disk_snapshot),
|
||||
});
|
||||
io.active_tab = io.tabs.len() - 1;
|
||||
io.active_path = Some(path.clone());
|
||||
@ -451,7 +432,6 @@ fn new_scene_tab(world: &mut World) -> String {
|
||||
dirty: false,
|
||||
snapshot: String::new(),
|
||||
recovery_snapshot: None,
|
||||
disk_snapshot: None,
|
||||
});
|
||||
io.active_tab = io.tabs.len() - 1;
|
||||
io.active_path = None;
|
||||
@ -560,7 +540,6 @@ fn close_scene_tab(world: &mut World, index: usize) -> String {
|
||||
dirty: false,
|
||||
snapshot: String::new(),
|
||||
recovery_snapshot: None,
|
||||
disk_snapshot: None,
|
||||
}];
|
||||
io.active_tab = 0;
|
||||
io.active_path = None;
|
||||
@ -702,24 +681,12 @@ fn save_selection_as_prefab(world: &mut World) -> String {
|
||||
else {
|
||||
return "Save prefab cancelled".to_string();
|
||||
};
|
||||
let expected = match FileSnapshot::capture(&path) {
|
||||
Ok(expected) => expected,
|
||||
Err(error) => return format!("Save prefab failed: {error}"),
|
||||
};
|
||||
|
||||
if ensure_unique_actor_ids(world, &selection) > 0 {
|
||||
world.resource_mut::<SceneIo>().mark_dirty();
|
||||
}
|
||||
|
||||
match save_prefab_entities(
|
||||
world,
|
||||
&path,
|
||||
selection,
|
||||
SceneWriteContext::Standalone {
|
||||
expected,
|
||||
description: "prefab copy",
|
||||
},
|
||||
) {
|
||||
match save_prefab_entities(world, &path, selection) {
|
||||
Ok(count) => {
|
||||
world.resource_mut::<EditorAssets>().refresh();
|
||||
format!(
|
||||
@ -798,7 +765,6 @@ fn save_prefab_entities(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
entities: Vec<Entity>,
|
||||
write_context: SceneWriteContext,
|
||||
) -> Result<usize, String> {
|
||||
let count = entities.len();
|
||||
let original_actor_ids: Vec<_> = entities
|
||||
@ -815,7 +781,7 @@ fn save_prefab_entities(
|
||||
.clone(),
|
||||
);
|
||||
scene::validate_prefab_graph_text(&text, path, &project_root)?;
|
||||
publish_scene_text(world, path, text.as_bytes(), count, write_context)?;
|
||||
atomic_write(path, text.as_bytes())?;
|
||||
Ok(count)
|
||||
})();
|
||||
if result.is_err() {
|
||||
@ -845,7 +811,7 @@ pub fn save_active_or_prompt_world(world: &mut World) -> String {
|
||||
fn save_active_or_prompt(world: &mut World) -> String {
|
||||
let path = world.resource::<SceneIo>().active_path.clone();
|
||||
match path {
|
||||
Some(path) => match save_level(world, &path, SceneWriteContext::Active) {
|
||||
Some(path) => match save_level(world, &path) {
|
||||
Ok(count) => {
|
||||
world.resource_mut::<SceneIo>().mark_clean();
|
||||
remember_path(world, path.clone());
|
||||
@ -867,12 +833,8 @@ fn save_with_dialog(world: &mut World) -> String {
|
||||
else {
|
||||
return "Save cancelled".to_string();
|
||||
};
|
||||
let expected = match FileSnapshot::capture(&path) {
|
||||
Ok(expected) => expected,
|
||||
Err(error) => return format!("Save failed: {error}"),
|
||||
};
|
||||
|
||||
match save_level(world, &path, SceneWriteContext::ActiveSaveAs { expected }) {
|
||||
match save_level(world, &path) {
|
||||
Ok(count) => {
|
||||
world.resource_mut::<SceneIo>().active_path = Some(path.clone());
|
||||
world.resource_mut::<SceneIo>().mark_clean();
|
||||
@ -929,20 +891,8 @@ fn export_selection_with_dialog(world: &mut World) -> String {
|
||||
else {
|
||||
return "Export cancelled".to_string();
|
||||
};
|
||||
let expected = match FileSnapshot::capture(&path) {
|
||||
Ok(expected) => expected,
|
||||
Err(error) => return format!("Export failed: {error}"),
|
||||
};
|
||||
|
||||
match save_standalone_entities(
|
||||
world,
|
||||
&path,
|
||||
vec![entity],
|
||||
SceneWriteContext::Standalone {
|
||||
expected,
|
||||
description: "selection export",
|
||||
},
|
||||
) {
|
||||
match save_standalone_entities(world, &path, vec![entity]) {
|
||||
Ok(count) => format!("Exported {count} selected entity to {}", path.display()),
|
||||
Err(err) => format!("Export failed: {err}"),
|
||||
}
|
||||
@ -974,20 +924,16 @@ fn clear_scene_world(world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
fn save_level(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
write_context: SceneWriteContext,
|
||||
) -> Result<usize, String> {
|
||||
fn save_level(world: &mut World, path: &Path) -> Result<usize, String> {
|
||||
let entities = authored_scene_entities(world);
|
||||
if is_prefab_document(path) {
|
||||
save_prefab_entities(world, path, entities, write_context)
|
||||
save_prefab_entities(world, path, entities)
|
||||
} else {
|
||||
save_entities(world, path, entities, write_context)
|
||||
save_entities(world, path, entities)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_active_scene(world: &mut World) -> Result<String, String> {
|
||||
fn serialize_active_scene(world: &mut World) -> Result<String, String> {
|
||||
let entities = authored_scene_entities(world);
|
||||
if world
|
||||
.resource::<SceneIo>()
|
||||
@ -1060,27 +1006,6 @@ fn format_actor_validation(err: ActorValidationError) -> String {
|
||||
ActorValidationError::ImportedModelHasStaticMeshRenderer => {
|
||||
"Save failed: ImportedModel actor cannot have StaticMeshRenderer".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshMissingRenderer => {
|
||||
"Save failed: SkinnedMesh actor requires SkinnedMeshRenderer".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshInvalidRenderer => {
|
||||
"Save failed: SkinnedMeshRenderer requires a model source path".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshHasPrimitive => {
|
||||
"Save failed: SkinnedMesh actor cannot have Primitive".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshHasStaticMeshRenderer => {
|
||||
"Save failed: SkinnedMesh actor cannot have StaticMeshRenderer".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshHasModelRef => {
|
||||
"Save failed: SkinnedMesh actor cannot have ModelRef".into()
|
||||
}
|
||||
ActorValidationError::SkinnedMeshRendererActorKindMismatch => {
|
||||
"Save failed: SkinnedMeshRenderer requires ActorKind::SkinnedMesh".into()
|
||||
}
|
||||
ActorValidationError::ConflictingGeometrySources => {
|
||||
"Save failed: actor has more than one primary geometry source component".into()
|
||||
}
|
||||
ActorValidationError::LightMissingLightDesc => {
|
||||
"Save failed: Light actor requires LightDesc".into()
|
||||
}
|
||||
@ -1123,9 +1048,8 @@ fn format_actor_validation(err: ActorValidationError) -> String {
|
||||
ActorValidationError::AudioListenerInvalidEarGap => {
|
||||
"Save failed: audio listener ear gap must be positive and finite".into()
|
||||
}
|
||||
ActorValidationError::AnimationControllerMissingSkinnedMeshRenderer => {
|
||||
"Save failed: animation controller requires SkinnedMeshRenderer on the same actor"
|
||||
.into()
|
||||
ActorValidationError::AnimationControllerMissingModelRef => {
|
||||
"Save failed: animation controller requires ModelRef on the same actor".into()
|
||||
}
|
||||
ActorValidationError::AnimationControllerMissingSkeleton => {
|
||||
"Save failed: animation controller requires an assigned skeleton".into()
|
||||
@ -1169,21 +1093,13 @@ fn format_actor_validation(err: ActorValidationError) -> String {
|
||||
ActorValidationError::PostProcessVolumeInvalidOverrideScalar => {
|
||||
"Save failed: post-process volume override contains invalid scalar".into()
|
||||
}
|
||||
ActorValidationError::InvalidNavigation(message) => {
|
||||
format!("Save failed: invalid navigation authoring: {message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_entities(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
entities: Vec<Entity>,
|
||||
write_context: SceneWriteContext,
|
||||
) -> Result<usize, String> {
|
||||
fn save_entities(world: &mut World, path: &Path, entities: Vec<Entity>) -> Result<usize, String> {
|
||||
let count = entities.len();
|
||||
let text = serialize_entities(world, entities)?;
|
||||
publish_scene_text(world, path, text.as_bytes(), count, write_context)?;
|
||||
atomic_write(path, text.as_bytes())?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
@ -1191,108 +1107,13 @@ fn save_standalone_entities(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
entities: Vec<Entity>,
|
||||
write_context: SceneWriteContext,
|
||||
) -> Result<usize, String> {
|
||||
let count = entities.len();
|
||||
let text = serialize_standalone_entities(world, entities)?;
|
||||
publish_scene_text(world, path, text.as_bytes(), count, write_context)?;
|
||||
atomic_write(path, text.as_bytes())?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SceneWriteContext {
|
||||
Active,
|
||||
ActiveSaveAs {
|
||||
expected: FileSnapshot,
|
||||
},
|
||||
Standalone {
|
||||
expected: FileSnapshot,
|
||||
description: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
fn publish_scene_text(
|
||||
world: &mut World,
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
entity_count: usize,
|
||||
context: SceneWriteContext,
|
||||
) -> Result<(), String> {
|
||||
match context {
|
||||
SceneWriteContext::Active | SceneWriteContext::ActiveSaveAs { .. } => {
|
||||
let (tab_id, expected) = match context {
|
||||
SceneWriteContext::Active => active_scene_write_baseline(world, path)?,
|
||||
SceneWriteContext::ActiveSaveAs { expected } => {
|
||||
(active_scene_tab_id(world)?, expected)
|
||||
}
|
||||
SceneWriteContext::Standalone { .. } => unreachable!(),
|
||||
};
|
||||
let snapshot = publish_authored_file(
|
||||
world,
|
||||
path,
|
||||
bytes,
|
||||
&expected,
|
||||
FileWriteIntent::Scene {
|
||||
tab_id,
|
||||
entity_count,
|
||||
},
|
||||
)?;
|
||||
let mut io = world.resource_mut::<SceneIo>();
|
||||
let Some(tab) = io.tabs.iter_mut().find(|tab| tab.id == tab_id) else {
|
||||
return Err("saved scene tab no longer exists".into());
|
||||
};
|
||||
tab.disk_snapshot = Some(snapshot);
|
||||
}
|
||||
SceneWriteContext::Standalone {
|
||||
expected,
|
||||
description,
|
||||
} => {
|
||||
publish_authored_file(
|
||||
world,
|
||||
path,
|
||||
bytes,
|
||||
&expected,
|
||||
FileWriteIntent::Standalone {
|
||||
description: description.into(),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn active_scene_write_baseline(
|
||||
world: &World,
|
||||
destination: &Path,
|
||||
) -> Result<(u64, FileSnapshot), String> {
|
||||
let io = world.resource::<SceneIo>();
|
||||
let tab = io
|
||||
.tabs
|
||||
.get(io.active_tab)
|
||||
.ok_or_else(|| "active scene tab is missing".to_string())?;
|
||||
if io.active_path.as_deref() != Some(destination) || tab.path.as_deref() != Some(destination) {
|
||||
return Err(format!(
|
||||
"{} is not the active scene destination; use Save As",
|
||||
destination.display()
|
||||
));
|
||||
}
|
||||
let expected = tab.disk_snapshot.clone().ok_or_else(|| {
|
||||
format!(
|
||||
"scene disk baseline is unavailable for {}; reload it or use Save As",
|
||||
destination.display()
|
||||
)
|
||||
})?;
|
||||
Ok((tab.id, expected))
|
||||
}
|
||||
|
||||
fn active_scene_tab_id(world: &World) -> Result<u64, String> {
|
||||
let io = world.resource::<SceneIo>();
|
||||
io.tabs
|
||||
.get(io.active_tab)
|
||||
.map(|tab| tab.id)
|
||||
.ok_or_else(|| "active scene tab is missing".to_string())
|
||||
}
|
||||
|
||||
fn serialize_entities(world: &mut World, entities: Vec<Entity>) -> Result<String, String> {
|
||||
serialize_entities_inner_with_resources(world, entities, true)
|
||||
}
|
||||
@ -1358,26 +1179,39 @@ fn serialize_entities_inner_with_resources(
|
||||
let result = (|| {
|
||||
let ron = {
|
||||
let registry = world.resource::<AppTypeRegistry>().read();
|
||||
let mut component_filter = WorldFilter::deny_all()
|
||||
.allow::<Name>()
|
||||
.allow::<Transform>()
|
||||
.allow::<ChildOf>()
|
||||
.allow::<LevelObject>()
|
||||
.allow::<ActorId>()
|
||||
.allow::<ActorName>()
|
||||
.allow::<HierarchySiblingIndex>()
|
||||
.allow::<EditorVisibility>();
|
||||
let component_registry = world
|
||||
.get_resource::<crate::ui::component_registry::EditorComponentRegistry>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
for descriptor in &component_registry.descriptors {
|
||||
if let Some(registration) = registry.get_with_type_path(descriptor.type_name) {
|
||||
component_filter = component_filter.allow_by_id(registration.type_id());
|
||||
}
|
||||
}
|
||||
let builder = DynamicWorldBuilder::from_world(world, ®istry)
|
||||
.with_component_filter(component_filter)
|
||||
.deny_all()
|
||||
.allow_component::<Name>()
|
||||
.allow_component::<Transform>()
|
||||
.allow_component::<ChildOf>()
|
||||
.allow_component::<LevelObject>()
|
||||
.allow_component::<ActorId>()
|
||||
.allow_component::<ActorName>()
|
||||
.allow_component::<ActorKind>()
|
||||
.allow_component::<InspectorOrder>()
|
||||
.allow_component::<Primitive>()
|
||||
.allow_component::<BrushDesc>()
|
||||
.allow_component::<StaticMeshRenderer>()
|
||||
.allow_component::<MaterialDesc>()
|
||||
.allow_component::<MaterialOverride>()
|
||||
.allow_component::<RigidBodyDesc>()
|
||||
.allow_component::<ColliderDesc>()
|
||||
.allow_component::<PhysicsBody>()
|
||||
.allow_component::<LightDesc>()
|
||||
.allow_component::<AnimationControllerDesc>()
|
||||
.allow_component::<AudioSourceDesc>()
|
||||
.allow_component::<AudioListenerDesc>()
|
||||
.allow_component::<PlayerSpawn>()
|
||||
.allow_component::<ModelRef>()
|
||||
.allow_component::<PrefabRef>()
|
||||
.allow_component::<PrefabInstance>()
|
||||
.allow_component::<WeaponSpawn>()
|
||||
.allow_component::<TriggerVolume>()
|
||||
.allow_component::<PostProcessVolumeDesc>()
|
||||
.allow_component::<TeamSpawn>()
|
||||
.allow_component::<ObjectiveMarker>()
|
||||
.allow_component::<HierarchySiblingIndex>()
|
||||
.allow_component::<EditorVisibility>()
|
||||
.extract_entities(entities.into_iter());
|
||||
let scene = if include_scene_resources {
|
||||
builder
|
||||
@ -1574,7 +1408,7 @@ fn restore_recovery(world: &mut World) -> String {
|
||||
return "No scene recovery snapshot is available".to_string();
|
||||
};
|
||||
match load_level(world, &snapshot) {
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
mark_recovery_restored(&mut world.resource_mut::<SceneIo>(), active_path);
|
||||
world.resource_mut::<EditorHistory>().clear();
|
||||
format!(
|
||||
@ -1615,12 +1449,8 @@ fn save_recovery_copy_with_dialog(world: &mut World) -> String {
|
||||
else {
|
||||
return "Save recovery copy cancelled".to_string();
|
||||
};
|
||||
let expected = match FileSnapshot::capture(&destination) {
|
||||
Ok(expected) => expected,
|
||||
Err(error) => return format!("Save recovery copy failed: {error}"),
|
||||
};
|
||||
|
||||
match write_recovery_copy(world, &snapshot, &destination, &expected) {
|
||||
match write_recovery_copy(&snapshot, &destination) {
|
||||
Ok(()) => format!(
|
||||
"Saved recovery copy {} from {}",
|
||||
destination.display(),
|
||||
@ -1630,24 +1460,10 @@ fn save_recovery_copy_with_dialog(world: &mut World) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_recovery_copy(
|
||||
world: &mut World,
|
||||
snapshot: &Path,
|
||||
destination: &Path,
|
||||
expected: &FileSnapshot,
|
||||
) -> Result<(), String> {
|
||||
fn write_recovery_copy(snapshot: &Path, destination: &Path) -> Result<(), String> {
|
||||
let bytes = std::fs::read(snapshot)
|
||||
.map_err(|error| format!("could not read {}: {error}", snapshot.display()))?;
|
||||
publish_authored_file(
|
||||
world,
|
||||
destination,
|
||||
&bytes,
|
||||
expected,
|
||||
FileWriteIntent::Standalone {
|
||||
description: "recovery scene copy".into(),
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
atomic_write(destination, &bytes)
|
||||
}
|
||||
|
||||
fn mark_recovery_restored(io: &mut SceneIo, active_path: Option<PathBuf>) {
|
||||
@ -1684,79 +1500,14 @@ fn discard_recovery(world: &mut World) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_level(world: &mut World, path: &Path) -> Result<FileSnapshot, String> {
|
||||
fn load_level(world: &mut World, path: &Path) -> Result<(), String> {
|
||||
if !path.exists() {
|
||||
return Err(format!("{} does not exist yet", path.display()));
|
||||
}
|
||||
|
||||
let text = std::fs::read_to_string(path)
|
||||
.map_err(|err| format!("could not read {}: {err}", path.display()))?;
|
||||
let disk_snapshot = FileSnapshot::from_loaded_bytes(path, text.as_bytes());
|
||||
load_level_text(world, Some(path), &text)?;
|
||||
Ok(disk_snapshot)
|
||||
}
|
||||
|
||||
pub(crate) fn reload_scene_after_file_conflict(
|
||||
world: &mut World,
|
||||
tab_id: u64,
|
||||
path: &Path,
|
||||
) -> Result<String, String> {
|
||||
let active_matches = {
|
||||
let io = world.resource::<SceneIo>();
|
||||
io.tabs
|
||||
.get(io.active_tab)
|
||||
.is_some_and(|tab| tab.id == tab_id)
|
||||
};
|
||||
if !active_matches {
|
||||
return Err("the scene conflict no longer belongs to the active tab".into());
|
||||
}
|
||||
|
||||
let disk_snapshot = load_level(world, path)?;
|
||||
{
|
||||
let mut io = world.resource_mut::<SceneIo>();
|
||||
io.active_path = Some(path.to_path_buf());
|
||||
io.recovery_snapshot = None;
|
||||
let active_tab = io.active_tab;
|
||||
io.tabs[active_tab].disk_snapshot = Some(disk_snapshot);
|
||||
io.mark_clean();
|
||||
}
|
||||
world.resource_mut::<EditorHistory>().clear();
|
||||
remember_path(world, path.to_path_buf());
|
||||
refresh_recovery_notice(world, path);
|
||||
capture_active_tab(world)?;
|
||||
Ok(format!("Reloaded {} from disk", path.display()))
|
||||
}
|
||||
|
||||
pub(crate) fn adopt_scene_conflict_save_as(
|
||||
world: &mut World,
|
||||
tab_id: u64,
|
||||
path: PathBuf,
|
||||
disk_snapshot: FileSnapshot,
|
||||
entity_count: usize,
|
||||
) -> Result<String, String> {
|
||||
let active_matches = {
|
||||
let io = world.resource::<SceneIo>();
|
||||
io.tabs
|
||||
.get(io.active_tab)
|
||||
.is_some_and(|tab| tab.id == tab_id)
|
||||
};
|
||||
if !active_matches {
|
||||
return Err("the scene conflict no longer belongs to the active tab".into());
|
||||
}
|
||||
|
||||
{
|
||||
let mut io = world.resource_mut::<SceneIo>();
|
||||
io.active_path = Some(path.clone());
|
||||
let active_tab = io.active_tab;
|
||||
io.tabs[active_tab].disk_snapshot = Some(disk_snapshot);
|
||||
io.mark_clean();
|
||||
}
|
||||
remember_path(world, path.clone());
|
||||
retire_scene_recovery(world, &path);
|
||||
Ok(format!(
|
||||
"Saved {entity_count} level entities to {}",
|
||||
path.display()
|
||||
))
|
||||
load_level_text(world, Some(path), &text)
|
||||
}
|
||||
|
||||
fn load_level_text(world: &mut World, path: Option<&Path>, text: &str) -> Result<(), String> {
|
||||
@ -2030,14 +1781,7 @@ fn finalize_scene_load(world: &mut World) {
|
||||
|
||||
let mut state: SystemState<(
|
||||
Res<settings::ProjectSettings>,
|
||||
Query<
|
||||
(
|
||||
&LightDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
>,
|
||||
Query<(&LightDesc, Option<&InspectorOrder>), With<LevelObject>>,
|
||||
Query<(&mut DirectionalLight, &mut Visibility), With<shared::ProjectSun>>,
|
||||
)> = SystemState::new(world);
|
||||
|
||||
@ -2057,8 +1801,10 @@ fn backfill_missing_actor_kinds(world: &mut World) {
|
||||
let Ok(entity_ref) = world.get_entity(entity) else {
|
||||
continue;
|
||||
};
|
||||
let current = entity_ref.get::<ActorKind>().copied();
|
||||
if let Some(kind) = infer_actor_kind(entity_ref).filter(|kind| current != Some(*kind)) {
|
||||
if entity_ref.get::<ActorKind>().is_some() {
|
||||
continue;
|
||||
}
|
||||
if let Some(kind) = infer_actor_kind(entity_ref) {
|
||||
world.entity_mut(entity).insert(kind);
|
||||
}
|
||||
}
|
||||
@ -2110,12 +1856,6 @@ mod tests {
|
||||
|
||||
struct NoAssetLoads;
|
||||
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component, Default)]
|
||||
struct RegisteredSceneExtension {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl LoadFromPath for NoAssetLoads {
|
||||
fn load_from_path_erased(
|
||||
&mut self,
|
||||
@ -2126,43 +1866,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_registered_extension_is_included_in_scene_persistence() {
|
||||
let mut app = App::new();
|
||||
app.register_type::<Name>()
|
||||
.register_type::<Transform>()
|
||||
.register_type::<LevelObject>()
|
||||
.register_type::<ActorKind>()
|
||||
.register_type::<SceneComposition>()
|
||||
.register_type::<RegisteredSceneExtension>();
|
||||
let mut component_registry =
|
||||
crate::ui::component_registry::EditorComponentRegistry::default();
|
||||
let mut descriptor = component_registry.descriptors[0].clone();
|
||||
descriptor.id = "game.registered_scene_extension";
|
||||
descriptor.type_name = std::any::type_name::<RegisteredSceneExtension>();
|
||||
descriptor.display_name = "Registered Scene Extension";
|
||||
descriptor.recommended = &[];
|
||||
descriptor.conflicts_with = &[];
|
||||
component_registry.register(descriptor).unwrap();
|
||||
app.insert_resource(component_registry);
|
||||
app.insert_resource(SceneComposition::default());
|
||||
let entity = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Name::new("Extension"),
|
||||
Transform::default(),
|
||||
LevelObject,
|
||||
ActorKind::Empty,
|
||||
RegisteredSceneExtension { value: 73 },
|
||||
))
|
||||
.id();
|
||||
|
||||
let text = serialize_entities_inner(app.world_mut(), vec![entity]).unwrap();
|
||||
|
||||
assert!(text.contains(std::any::type_name::<RegisteredSceneExtension>()));
|
||||
assert!(text.contains("value: 73") || text.contains("value:73"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_recovery_remains_available_until_save_or_discard() {
|
||||
let authored = PathBuf::from("assets/levels/authored.scn.ron");
|
||||
@ -2185,13 +1888,11 @@ mod tests {
|
||||
let mut io = SceneIo::default();
|
||||
assert_eq!(io.tabs.len(), 1);
|
||||
assert!(!io.tabs[0].dirty);
|
||||
let initial_revision = io.change_revision();
|
||||
|
||||
io.active_path = Some(PathBuf::from("assets/levels/arena.scn.ron"));
|
||||
io.mark_dirty();
|
||||
|
||||
assert!(io.has_unsaved_tabs());
|
||||
assert!(io.change_revision() > initial_revision);
|
||||
assert!(io.tabs[0].dirty);
|
||||
assert_eq!(
|
||||
io.tabs[0].path.as_deref(),
|
||||
@ -2206,7 +1907,6 @@ mod tests {
|
||||
dirty: true,
|
||||
snapshot: "dirty inactive tab".to_string(),
|
||||
recovery_snapshot: None,
|
||||
disk_snapshot: None,
|
||||
});
|
||||
assert!(
|
||||
io.has_unsaved_tabs(),
|
||||
@ -2221,61 +1921,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_scene_write_refuses_an_external_revision() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-scene-external-write-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let path = root.join("assets/levels/main.scn.ron");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, b"loaded").unwrap();
|
||||
let disk_snapshot = FileSnapshot::capture(&path).unwrap();
|
||||
let mut io = SceneIo {
|
||||
active_path: Some(path.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
io.tabs[0].path = Some(path.clone());
|
||||
io.tabs[0].disk_snapshot = Some(disk_snapshot);
|
||||
let mut world = World::new();
|
||||
world.insert_resource(io);
|
||||
std::fs::write(&path, b"external").unwrap();
|
||||
|
||||
let result = publish_scene_text(&mut world, &path, b"editor", 1, SceneWriteContext::Active);
|
||||
|
||||
assert!(result.unwrap_err().contains("changed outside Blacksite"));
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"external");
|
||||
assert!(world.resource::<SceneIo>().dirty);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_save_as_refuses_a_file_created_after_destination_selection() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-scene-save-as-race-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let path = root.join("assets/levels/new.scn.ron");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let expected = FileSnapshot::missing();
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SceneIo>();
|
||||
std::fs::write(&path, b"external").unwrap();
|
||||
|
||||
let result = publish_scene_text(
|
||||
&mut world,
|
||||
&path,
|
||||
b"editor",
|
||||
1,
|
||||
SceneWriteContext::ActiveSaveAs { expected },
|
||||
);
|
||||
|
||||
assert!(result.unwrap_err().contains("changed outside Blacksite"));
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"external");
|
||||
assert!(world.resource::<SceneIo>().dirty);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_edits_participate_in_undo_and_redo() {
|
||||
let mut app = App::new();
|
||||
@ -2507,8 +2152,8 @@ mod tests {
|
||||
Transform::IDENTITY,
|
||||
LevelObject,
|
||||
ActorId::new("animated-model"),
|
||||
ActorKind::SkinnedMesh,
|
||||
SkinnedMeshRenderer::new(source_path),
|
||||
ActorKind::ImportedModel,
|
||||
ModelRef::new(source_path),
|
||||
controller.clone(),
|
||||
AnimationPlayer::default(),
|
||||
AnimationGraphHandle::default(),
|
||||
@ -2526,8 +2171,6 @@ mod tests {
|
||||
let decoded: AnimationControllerDesc = ron::from_str(&component.ron).unwrap();
|
||||
|
||||
assert_eq!(decoded, controller);
|
||||
assert!(text.contains("shared::animation::SkinnedMeshRenderer"));
|
||||
assert!(!text.contains("shared::components::ModelRef"));
|
||||
assert!(!text.contains("AnimationPlayer"));
|
||||
assert!(!text.contains("AnimationGraphHandle"));
|
||||
assert!(!text.contains("AnimationTransitions"));
|
||||
@ -3053,13 +2696,8 @@ mod tests {
|
||||
.insert_resource(crate::ui::hierarchy_state::HierarchyPanelState::default());
|
||||
app.world_mut().init_resource::<SceneIo>();
|
||||
|
||||
let expected = load_level(app.world_mut(), &prefab_path).unwrap();
|
||||
save_level(
|
||||
app.world_mut(),
|
||||
&prefab_path,
|
||||
SceneWriteContext::ActiveSaveAs { expected },
|
||||
)
|
||||
.unwrap();
|
||||
load_level(app.world_mut(), &prefab_path).unwrap();
|
||||
save_level(app.world_mut(), &prefab_path).unwrap();
|
||||
|
||||
scene::validate_prefab_graph(&prefab_path, &root).unwrap();
|
||||
let document =
|
||||
@ -3101,11 +2739,8 @@ mod tests {
|
||||
let destination = root.join("kept.scn.ron");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::fs::write(&snapshot, b"recovered scene bytes").unwrap();
|
||||
let expected = FileSnapshot::missing();
|
||||
let mut world = World::new();
|
||||
world.init_resource::<SceneIo>();
|
||||
|
||||
write_recovery_copy(&mut world, &snapshot, &destination, &expected).unwrap();
|
||||
write_recovery_copy(&snapshot, &destination).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read(&destination).unwrap(),
|
||||
|
||||
@ -5,7 +5,7 @@ mod transform;
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::egui;
|
||||
use egui_phosphor_icons::icons;
|
||||
use shared::{infer_actor_kind, ActorId, ActorKind, ActorName, HydratedPrefabMember, LevelObject};
|
||||
use shared::{ActorId, ActorKind, ActorName, HydratedPrefabMember, LevelObject};
|
||||
|
||||
use crate::ext::extensibility::ActorInspectorSectionRegistry;
|
||||
use crate::ui::inspector;
|
||||
@ -29,8 +29,9 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
return;
|
||||
}
|
||||
|
||||
let kind = infer_actor_kind(entity_ref)
|
||||
.or_else(|| entity_ref.get::<ActorKind>().copied())
|
||||
let kind = entity_ref
|
||||
.get::<ActorKind>()
|
||||
.copied()
|
||||
.unwrap_or(ActorKind::Empty);
|
||||
|
||||
egui::Frame::new()
|
||||
@ -110,7 +111,6 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
|
||||
match kind {
|
||||
ActorKind::StaticMesh
|
||||
| ActorKind::SkinnedMesh
|
||||
| ActorKind::Brush
|
||||
| ActorKind::ImportedModel
|
||||
| ActorKind::Light
|
||||
@ -123,8 +123,7 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
| ActorKind::TeamSpawn
|
||||
| ActorKind::Objective
|
||||
| ActorKind::AudioSource
|
||||
| ActorKind::AudioListener
|
||||
| ActorKind::Navigation => {
|
||||
| ActorKind::AudioListener => {
|
||||
inspector::authoring_inspector_ui(world, ui, entity);
|
||||
}
|
||||
}
|
||||
@ -140,10 +139,7 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
|
||||
fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
||||
match kind {
|
||||
ActorKind::StaticMesh
|
||||
| ActorKind::SkinnedMesh
|
||||
| ActorKind::Brush
|
||||
| ActorKind::ImportedModel => icons::CUBE,
|
||||
ActorKind::StaticMesh | ActorKind::Brush | ActorKind::ImportedModel => icons::CUBE,
|
||||
ActorKind::Light => icons::LIGHTBULB,
|
||||
ActorKind::PrefabAnchor => icons::PACKAGE,
|
||||
ActorKind::PlayerSpawn => icons::PERSON_SIMPLE_RUN,
|
||||
@ -153,7 +149,6 @@ fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
||||
ActorKind::TeamSpawn | ActorKind::Objective => icons::FLAG,
|
||||
ActorKind::AudioSource => icons::SPEAKER_HIGH,
|
||||
ActorKind::AudioListener => icons::HEADPHONES,
|
||||
ActorKind::Navigation => icons::PATH,
|
||||
ActorKind::Empty => icons::CIRCLE,
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ use bevy_egui::egui;
|
||||
use egui_phosphor_icons::icons;
|
||||
use shared::{
|
||||
AnimationClipRecord, AnimationControllerDesc, AnimationManifest, AnimationSkeletonRecord,
|
||||
AnimationStateDesc, EditorAssetRef, SkinnedMeshRenderer, COMPONENT_ANIMATION_CONTROLLER_DESC,
|
||||
AnimationStateDesc, EditorAssetRef, ModelRef, COMPONENT_ANIMATION_CONTROLLER_DESC,
|
||||
};
|
||||
|
||||
use crate::assets::animation::load_animation_manifest;
|
||||
@ -656,8 +656,8 @@ fn manifest_summary(ui: &mut egui::Ui, manifest: &Result<AnimationManifest, Stri
|
||||
|
||||
fn manifest_for_actor(world: &World, entity: Entity) -> Result<AnimationManifest, String> {
|
||||
let model = world
|
||||
.get::<SkinnedMeshRenderer>(entity)
|
||||
.ok_or_else(|| "actor has no SkinnedMeshRenderer".to_string())?;
|
||||
.get::<ModelRef>(entity)
|
||||
.ok_or_else(|| "actor has no ModelRef".to_string())?;
|
||||
let registry = world
|
||||
.get_resource::<AssetRegistry>()
|
||||
.ok_or_else(|| "asset registry is unavailable".to_string())?;
|
||||
@ -878,7 +878,6 @@ mod tests {
|
||||
schema_version: 1,
|
||||
asset_id: "model-id".into(),
|
||||
label: "Robot".into(),
|
||||
default_animation_clip_id: None,
|
||||
source: AnimationManifestSource {
|
||||
path: "assets/models/robot.glb".into(),
|
||||
format: "glb".into(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use shared::{MaterialAsset, MaterialInstanceAsset};
|
||||
use shared::MaterialAsset;
|
||||
|
||||
use crate::asset_db::ImportSettings;
|
||||
use crate::assets::{AssetSelection, ASSETS_ROOT, BUILTINS_FOLDER};
|
||||
use crate::project::collaboration::FileSnapshot;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AssetBrowserView {
|
||||
@ -49,7 +48,6 @@ pub struct AssetBrowserUiState {
|
||||
pub(crate) pending_delete: Option<AssetDeleteRequest>,
|
||||
pub(crate) import_draft: Option<ImportSettingsDraft>,
|
||||
pub(crate) material_draft: Option<MaterialAssetDraft>,
|
||||
pub(crate) material_instance_draft: Option<MaterialInstanceAssetDraft>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -70,15 +68,6 @@ pub(crate) struct ImportSettingsDraft {
|
||||
pub(crate) struct MaterialAssetDraft {
|
||||
pub(crate) path: String,
|
||||
pub(crate) asset: MaterialAsset,
|
||||
pub(crate) disk_snapshot: FileSnapshot,
|
||||
pub(crate) error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MaterialInstanceAssetDraft {
|
||||
pub(crate) path: String,
|
||||
pub(crate) asset: MaterialInstanceAsset,
|
||||
pub(crate) disk_snapshot: FileSnapshot,
|
||||
pub(crate) error: Option<String>,
|
||||
}
|
||||
|
||||
@ -100,7 +89,6 @@ impl Default for AssetBrowserUiState {
|
||||
pending_delete: None,
|
||||
import_draft: None,
|
||||
material_draft: None,
|
||||
material_instance_draft: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
//! Editor-visible component registry for actor inspector palettes.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use bevy::ecs::reflect::ReflectComponent;
|
||||
use bevy::prelude::*;
|
||||
use bevy::reflect::std_traits::ReflectDefault;
|
||||
use bevy::reflect::GetTypeRegistration;
|
||||
use bevy_egui::egui;
|
||||
use egui_phosphor_icons::icons;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -15,7 +9,6 @@ pub enum EditorComponentCategory {
|
||||
Rendering,
|
||||
Animation,
|
||||
Audio,
|
||||
Navigation,
|
||||
Physics,
|
||||
Gameplay,
|
||||
Volumes,
|
||||
@ -24,8 +17,6 @@ pub enum EditorComponentCategory {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditorComponentDescriptor {
|
||||
/// Stable editor/protocol identity. This must not be a Rust type path.
|
||||
pub id: &'static str,
|
||||
pub type_name: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub category: EditorComponentCategory,
|
||||
@ -44,266 +35,6 @@ pub struct EditorComponentDescriptor {
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct EditorComponentRegistry {
|
||||
pub descriptors: Vec<EditorComponentDescriptor>,
|
||||
inspectors: HashMap<&'static str, ComponentInspectorFn>,
|
||||
}
|
||||
|
||||
pub type ComponentInspectorFn = fn(&mut World, &mut egui::Ui, Entity);
|
||||
|
||||
impl EditorComponentRegistry {
|
||||
/// Registers a statically linked component extension.
|
||||
///
|
||||
/// Stable ids and reflected type paths are both unique. Returning an error
|
||||
/// here prevents a game/editor extension from silently shadowing a built-in.
|
||||
pub fn register(&mut self, descriptor: EditorComponentDescriptor) -> Result<(), String> {
|
||||
if self
|
||||
.descriptors
|
||||
.iter()
|
||||
.any(|existing| existing.id == descriptor.id)
|
||||
{
|
||||
return Err(format!(
|
||||
"duplicate authoring component id `{}`",
|
||||
descriptor.id
|
||||
));
|
||||
}
|
||||
if self
|
||||
.descriptors
|
||||
.iter()
|
||||
.any(|existing| existing.type_name == descriptor.type_name)
|
||||
{
|
||||
return Err(format!(
|
||||
"component type `{}` is already registered",
|
||||
descriptor.type_name
|
||||
));
|
||||
}
|
||||
self.descriptors.push(descriptor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register_with_inspector(
|
||||
&mut self,
|
||||
descriptor: EditorComponentDescriptor,
|
||||
inspector: ComponentInspectorFn,
|
||||
) -> Result<(), String> {
|
||||
let type_name = descriptor.type_name;
|
||||
self.register(descriptor)?;
|
||||
self.inspectors.insert(type_name, inspector);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn inspector(&self, type_name: &str) -> Option<ComponentInspectorFn> {
|
||||
self.inspectors.get(type_name).copied()
|
||||
}
|
||||
|
||||
/// Hard lifecycle requirements keyed by stable component IDs. Keeping this
|
||||
/// lookup in the registry removes requirement policy from inspector code.
|
||||
pub fn required_component_ids(
|
||||
&self,
|
||||
descriptor: &EditorComponentDescriptor,
|
||||
) -> &'static [&'static str] {
|
||||
match descriptor.id {
|
||||
shared::AUTHORING_COMPONENT_ANIMATION_CONTROLLER => {
|
||||
&[shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER]
|
||||
}
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn present_dependents(
|
||||
&self,
|
||||
world: &World,
|
||||
entity: Entity,
|
||||
required_id: &str,
|
||||
) -> Vec<&EditorComponentDescriptor> {
|
||||
self.descriptors
|
||||
.iter()
|
||||
.filter(|descriptor| {
|
||||
self.required_component_ids(descriptor)
|
||||
.contains(&required_id)
|
||||
&& self.component_present(world, entity, descriptor.type_name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn by_id(&self, id: &str) -> Option<&EditorComponentDescriptor> {
|
||||
self.descriptors
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.id == id)
|
||||
}
|
||||
|
||||
pub fn by_type_name(&self, type_name: &str) -> Option<&EditorComponentDescriptor> {
|
||||
self.descriptors
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.type_name == type_name)
|
||||
}
|
||||
|
||||
pub fn stable_id_for_type<'a>(&self, type_name: &'a str) -> &'a str {
|
||||
self.by_type_name(type_name)
|
||||
.map(|descriptor| descriptor.id)
|
||||
.unwrap_or(type_name)
|
||||
}
|
||||
|
||||
pub fn ensure_component_order(&self, order: &mut shared::InspectorOrder, present: &[&str]) {
|
||||
for type_name in present {
|
||||
let component_id = self.stable_id_for_type(type_name);
|
||||
if !order
|
||||
.component_ids
|
||||
.iter()
|
||||
.any(|existing| existing == component_id)
|
||||
{
|
||||
order.component_ids.push(component_id.to_string());
|
||||
}
|
||||
}
|
||||
order.component_type_names.clear();
|
||||
}
|
||||
|
||||
pub fn ordered_components<'a>(
|
||||
&self,
|
||||
order: &shared::InspectorOrder,
|
||||
present: &[&'a str],
|
||||
) -> Vec<&'a str> {
|
||||
let mut ordered = Vec::new();
|
||||
for component_id in &order.component_ids {
|
||||
if let Some(type_name) = present
|
||||
.iter()
|
||||
.find(|type_name| self.stable_id_for_type(type_name) == component_id)
|
||||
{
|
||||
ordered.push(*type_name);
|
||||
}
|
||||
}
|
||||
for legacy_type_name in &order.component_type_names {
|
||||
if let Some(type_name) = present
|
||||
.iter()
|
||||
.find(|type_name| **type_name == legacy_type_name)
|
||||
{
|
||||
if !ordered.contains(type_name) {
|
||||
ordered.push(*type_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for type_name in present {
|
||||
if !ordered.contains(type_name) {
|
||||
ordered.push(*type_name);
|
||||
}
|
||||
}
|
||||
ordered
|
||||
}
|
||||
|
||||
pub fn move_component(
|
||||
&self,
|
||||
order: &mut shared::InspectorOrder,
|
||||
type_name: &str,
|
||||
offset: isize,
|
||||
present: &[&str],
|
||||
) -> bool {
|
||||
self.ensure_component_order(order, present);
|
||||
let component_id = self.stable_id_for_type(type_name);
|
||||
let Some(index) = order
|
||||
.component_ids
|
||||
.iter()
|
||||
.position(|existing| existing == component_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let target =
|
||||
(index as isize + offset).clamp(0, order.component_ids.len() as isize - 1) as usize;
|
||||
if index == target {
|
||||
return false;
|
||||
}
|
||||
let item = order.component_ids.remove(index);
|
||||
order.component_ids.insert(target, item);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn component_present(&self, world: &World, entity: Entity, type_name: &str) -> bool {
|
||||
let Ok(entity_ref) = world.get_entity(entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(app_registry) = world.get_resource::<AppTypeRegistry>() else {
|
||||
return false;
|
||||
};
|
||||
let type_registry = app_registry.read();
|
||||
type_registry
|
||||
.get_with_type_path(type_name)
|
||||
.and_then(|registration| registration.data::<ReflectComponent>())
|
||||
.is_some_and(|component| component.contains(entity_ref))
|
||||
}
|
||||
|
||||
/// Validates registry metadata against Bevy reflection before the first
|
||||
/// inspector frame. This is also the contract test used by extensions.
|
||||
pub fn validate(&self, world: &World) -> Result<(), Vec<String>> {
|
||||
let Some(app_registry) = world.get_resource::<AppTypeRegistry>() else {
|
||||
return Err(vec!["AppTypeRegistry is unavailable".to_string()]);
|
||||
};
|
||||
let type_registry = app_registry.read();
|
||||
let mut errors = Vec::new();
|
||||
let mut ids = HashSet::new();
|
||||
let mut type_names = HashSet::new();
|
||||
for descriptor in &self.descriptors {
|
||||
if descriptor.id.trim().is_empty() {
|
||||
errors.push(format!(
|
||||
"{} has an empty stable id",
|
||||
descriptor.display_name
|
||||
));
|
||||
} else if !ids.insert(descriptor.id) {
|
||||
errors.push(format!("duplicate stable id `{}`", descriptor.id));
|
||||
}
|
||||
if !type_names.insert(descriptor.type_name) {
|
||||
errors.push(format!(
|
||||
"duplicate reflected type `{}`",
|
||||
descriptor.type_name
|
||||
));
|
||||
}
|
||||
match type_registry.get_with_type_path(descriptor.type_name) {
|
||||
Some(registration) if registration.data::<ReflectComponent>().is_none() => errors
|
||||
.push(format!(
|
||||
"`{}` is registered without ReflectComponent",
|
||||
descriptor.type_name
|
||||
)),
|
||||
None => errors.push(format!(
|
||||
"reflected component `{}` is not registered",
|
||||
descriptor.type_name
|
||||
)),
|
||||
Some(registration)
|
||||
if descriptor.addable && registration.data::<ReflectDefault>().is_none() =>
|
||||
{
|
||||
errors.push(format!(
|
||||
"addable component `{}` has no reflected Default",
|
||||
descriptor.type_name
|
||||
));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
for relation in descriptor
|
||||
.recommended
|
||||
.iter()
|
||||
.chain(descriptor.conflicts_with.iter())
|
||||
{
|
||||
if !self
|
||||
.descriptors
|
||||
.iter()
|
||||
.any(|candidate| candidate.type_name == *relation)
|
||||
{
|
||||
errors.push(format!(
|
||||
"{} references unregistered component `{relation}`",
|
||||
descriptor.id
|
||||
));
|
||||
}
|
||||
}
|
||||
for required_id in self.required_component_ids(descriptor) {
|
||||
if self.by_id(required_id).is_none() {
|
||||
errors.push(format!(
|
||||
"{} requires unknown stable component id `{required_id}`",
|
||||
descriptor.id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EditorComponentRegistry {
|
||||
@ -311,7 +42,6 @@ impl Default for EditorComponentRegistry {
|
||||
Self {
|
||||
descriptors: vec![
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
|
||||
type_name: "shared::components::StaticMeshRenderer",
|
||||
display_name: "Static Mesh Renderer",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
@ -326,35 +56,11 @@ impl Default for EditorComponentRegistry {
|
||||
conflicts_with: &[
|
||||
"shared::components::Primitive",
|
||||
"shared::components::BrushDesc",
|
||||
"shared::animation::SkinnedMeshRenderer",
|
||||
],
|
||||
hydration_effect:
|
||||
"Hydrates into generated mesh children and material bindings.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
|
||||
type_name: "shared::animation::SkinnedMeshRenderer",
|
||||
display_name: "Skinned Mesh Renderer",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
addable: false,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::PERSON_SIMPLE_RUN.as_str(),
|
||||
description:
|
||||
"Renders rigged geometry while preserving its imported joints and skin bindings.",
|
||||
search_terms: &["mesh", "renderer", "skinned", "skeletal", "rigged", "joints"],
|
||||
recommended: &["shared::animation::AnimationControllerDesc"],
|
||||
conflicts_with: &[
|
||||
"shared::components::Primitive",
|
||||
"shared::components::BrushDesc",
|
||||
"shared::components::StaticMeshRenderer",
|
||||
],
|
||||
hydration_effect:
|
||||
"Hydrates through a dedicated scene root containing Bevy skinned meshes and joints.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_BRUSH,
|
||||
type_name: "shared::components::BrushDesc",
|
||||
display_name: "Brush",
|
||||
category: EditorComponentCategory::Authoring,
|
||||
@ -369,12 +75,10 @@ impl Default for EditorComponentRegistry {
|
||||
conflicts_with: &[
|
||||
"shared::components::Primitive",
|
||||
"shared::components::StaticMeshRenderer",
|
||||
"shared::animation::SkinnedMeshRenderer",
|
||||
],
|
||||
hydration_effect: "Hydrates into generated brush mesh children.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_MATERIAL,
|
||||
type_name: "shared::components::MaterialDesc",
|
||||
display_name: "Authoring Material",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
@ -390,7 +94,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into Bevy material assets for renderable actors.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_RIGID_BODY,
|
||||
type_name: "shared::components::RigidBodyDesc",
|
||||
display_name: "Rigid Body",
|
||||
category: EditorComponentCategory::Physics,
|
||||
@ -406,7 +109,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into runtime Avian rigid-body state.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_COLLIDER,
|
||||
type_name: "shared::components::ColliderDesc",
|
||||
display_name: "Collider",
|
||||
category: EditorComponentCategory::Physics,
|
||||
@ -422,7 +124,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into runtime Avian collider state.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_LIGHT,
|
||||
type_name: "shared::components::LightDesc",
|
||||
display_name: "Light",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
@ -438,7 +139,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into runtime Bevy light components.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_ANIMATION_CONTROLLER,
|
||||
type_name: "shared::animation::AnimationControllerDesc",
|
||||
display_name: "Animation Controller",
|
||||
category: EditorComponentCategory::Animation,
|
||||
@ -449,13 +149,12 @@ impl Default for EditorComponentRegistry {
|
||||
icon: icons::FILM_SLATE.as_str(),
|
||||
description: "Authors named animation states for an imported model rig.",
|
||||
search_terms: &["animation", "controller", "state", "clip", "skeleton", "rig"],
|
||||
recommended: &["shared::animation::SkinnedMeshRenderer"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect:
|
||||
"Hydrates into a Bevy animation graph and player bound to the imported model.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_AUDIO_SOURCE,
|
||||
type_name: "shared::components::AudioSourceDesc",
|
||||
display_name: "Audio Source",
|
||||
category: EditorComponentCategory::Audio,
|
||||
@ -472,7 +171,6 @@ impl Default for EditorComponentRegistry {
|
||||
"Hydrates into runtime Bevy audio voices with bus and attenuation control.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_AUDIO_LISTENER,
|
||||
type_name: "shared::components::AudioListenerDesc",
|
||||
display_name: "Audio Listener",
|
||||
category: EditorComponentCategory::Audio,
|
||||
@ -489,7 +187,6 @@ impl Default for EditorComponentRegistry {
|
||||
"The highest-priority enabled listener becomes the runtime listener.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_PRIMITIVE,
|
||||
type_name: "shared::components::Primitive",
|
||||
display_name: "Primitive",
|
||||
category: EditorComponentCategory::Authoring,
|
||||
@ -504,13 +201,11 @@ impl Default for EditorComponentRegistry {
|
||||
conflicts_with: &[
|
||||
"shared::components::StaticMeshRenderer",
|
||||
"shared::components::BrushDesc",
|
||||
"shared::animation::SkinnedMeshRenderer",
|
||||
],
|
||||
hydration_effect:
|
||||
"Hydrates into generated primitive render and collision data.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_PLAYER_SPAWN,
|
||||
type_name: "shared::components::PlayerSpawn",
|
||||
display_name: "Player Spawn",
|
||||
category: EditorComponentCategory::Gameplay,
|
||||
@ -527,7 +222,6 @@ impl Default for EditorComponentRegistry {
|
||||
"Used by PIE/session startup; no saved runtime player is created.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_WEAPON_SPAWN,
|
||||
type_name: "shared::components::WeaponSpawn",
|
||||
display_name: "Weapon Spawn",
|
||||
category: EditorComponentCategory::Gameplay,
|
||||
@ -543,7 +237,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_TRIGGER_VOLUME,
|
||||
type_name: "shared::components::TriggerVolume",
|
||||
display_name: "Trigger Volume",
|
||||
category: EditorComponentCategory::Volumes,
|
||||
@ -559,7 +252,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into gameplay trigger queries and visualizers.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_TEAM_SPAWN,
|
||||
type_name: "shared::components::TeamSpawn",
|
||||
display_name: "Team Spawn",
|
||||
category: EditorComponentCategory::Gameplay,
|
||||
@ -575,7 +267,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_OBJECTIVE,
|
||||
type_name: "shared::components::ObjectiveMarker",
|
||||
display_name: "Objective Marker",
|
||||
category: EditorComponentCategory::Gameplay,
|
||||
@ -591,7 +282,6 @@ impl Default for EditorComponentRegistry {
|
||||
hydration_effect: "Hydrates into gameplay objective metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_POST_PROCESS_VOLUME,
|
||||
type_name: "shared::components::PostProcessVolumeDesc",
|
||||
display_name: "Post-Process Volume",
|
||||
category: EditorComponentCategory::Volumes,
|
||||
@ -608,215 +298,6 @@ impl Default for EditorComponentRegistry {
|
||||
"Affects active camera rendering when the camera is inside the volume.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_NAVIGATION_BOUNDS,
|
||||
type_name: "shared::navigation::NavigationBounds",
|
||||
display_name: "Navigation Bounds",
|
||||
category: EditorComponentCategory::Navigation,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::PATH.as_str(),
|
||||
description: "Defines the world region and agent profile for a navigation bake.",
|
||||
search_terms: &["navigation", "navmesh", "bounds", "agent", "bake"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[
|
||||
"shared::navigation::NavigationObstacle",
|
||||
"shared::navigation::NavigationArea",
|
||||
"shared::navigation::NavigationLink",
|
||||
],
|
||||
hydration_effect: "Feeds the generated navigation artifact; no render component is hydrated.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_NAVIGATION_OBSTACLE,
|
||||
type_name: "shared::navigation::NavigationObstacle",
|
||||
display_name: "Navigation Obstacle",
|
||||
category: EditorComponentCategory::Navigation,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SELECTION_SLASH.as_str(),
|
||||
description: "Carves an axis-aligned blocked volume from overlapping bakes.",
|
||||
search_terms: &["navigation", "navmesh", "obstacle", "block", "carve"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[
|
||||
"shared::navigation::NavigationBounds",
|
||||
"shared::navigation::NavigationArea",
|
||||
"shared::navigation::NavigationLink",
|
||||
],
|
||||
hydration_effect: "Contributes a deterministic unwalkable volume during bake.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_NAVIGATION_AREA,
|
||||
type_name: "shared::navigation::NavigationArea",
|
||||
display_name: "Navigation Area",
|
||||
category: EditorComponentCategory::Navigation,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::POLYGON.as_str(),
|
||||
description: "Labels a walkable or blocked navigation area and traversal cost.",
|
||||
search_terms: &["navigation", "navmesh", "area", "cost", "walkable"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[
|
||||
"shared::navigation::NavigationBounds",
|
||||
"shared::navigation::NavigationObstacle",
|
||||
"shared::navigation::NavigationLink",
|
||||
],
|
||||
hydration_effect: "Stores area metadata and blocked-volume contribution in the bake artifact.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_NAVIGATION_LINK,
|
||||
type_name: "shared::navigation::NavigationLink",
|
||||
display_name: "Navigation Link",
|
||||
category: EditorComponentCategory::Navigation,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::LINK.as_str(),
|
||||
description: "Connects otherwise isolated navigation regions with an explicit traversal.",
|
||||
search_terms: &["navigation", "navmesh", "link", "jump", "door", "ladder"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[
|
||||
"shared::navigation::NavigationBounds",
|
||||
"shared::navigation::NavigationObstacle",
|
||||
"shared::navigation::NavigationArea",
|
||||
],
|
||||
hydration_effect: "Adds a validated directed or bidirectional runtime path connection.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: shared::AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY,
|
||||
type_name: "shared::components::PhysicsBody",
|
||||
display_name: "Legacy Physics Body",
|
||||
category: EditorComponentCategory::Physics,
|
||||
addable: false,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SPHERE.as_str(),
|
||||
description: "Legacy combined physics authoring retained for scene compatibility.",
|
||||
search_terms: &["legacy", "physics", "body"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates through the compatibility physics path.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "prefab.instance",
|
||||
type_name: "shared::components::PrefabInstance",
|
||||
display_name: "Prefab Instance",
|
||||
category: EditorComponentCategory::Authoring,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::PACKAGE.as_str(),
|
||||
description: "Stores a linked prefab instance and its overrides.",
|
||||
search_terms: &["prefab", "instance", "link"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates linked prefab members beneath this anchor.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "prefab.reference",
|
||||
type_name: "shared::components::PrefabRef",
|
||||
display_name: "Prefab Reference",
|
||||
category: EditorComponentCategory::Authoring,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::PACKAGE.as_str(),
|
||||
description: "Stable source reference owned by a prefab anchor.",
|
||||
search_terms: &["prefab", "reference"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Resolves the prefab source asset.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "render.imported_model",
|
||||
type_name: "shared::components::ModelRef",
|
||||
display_name: "Imported Model Reference",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::CUBE_TRANSPARENT.as_str(),
|
||||
description: "Legacy full-scene imported model reference.",
|
||||
search_terms: &["model", "imported", "scene"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates the referenced imported model scene.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "render.material_override_legacy",
|
||||
type_name: "shared::components::MaterialOverride",
|
||||
display_name: "Legacy Material Override",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::PALETTE.as_str(),
|
||||
description: "Legacy renderer material slot overrides retained for migration.",
|
||||
search_terms: &["material", "override", "legacy"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Applies compatibility material overrides.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "render.project_sun",
|
||||
type_name: "shared::components::ProjectSun",
|
||||
display_name: "Project Sun",
|
||||
category: EditorComponentCategory::Rendering,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SUN.as_str(),
|
||||
description: "Marks the scene light that overrides the project sun.",
|
||||
search_terms: &["sun", "directional", "project"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Selects the authored directional project light.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "core.actor_kind",
|
||||
type_name: "shared::components::ActorKind",
|
||||
display_name: "Actor Kind Hint",
|
||||
category: EditorComponentCategory::Editor,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::TAG.as_str(),
|
||||
description: "Compatibility and presentation hint derived from primary components.",
|
||||
search_terms: &["actor", "kind", "hint"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "No runtime effect; validation uses component composition.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "editor.component_states",
|
||||
type_name: "shared::components::AuthoringComponentStates",
|
||||
display_name: "Authoring Component States",
|
||||
category: EditorComponentCategory::Editor,
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::TOGGLE_LEFT.as_str(),
|
||||
description: "Stores persisted component enable state independently of inspector layout.",
|
||||
search_terms: &["editor", "active", "enabled"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Controls whether opted-in authoring components hydrate.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
id: "editor.inspector_order",
|
||||
type_name: "shared::components::InspectorOrder",
|
||||
display_name: "Inspector Order",
|
||||
category: EditorComponentCategory::Editor,
|
||||
@ -825,62 +306,14 @@ impl Default for EditorComponentRegistry {
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::LIST_BULLETS.as_str(),
|
||||
description: "Stores editor-only component card ordering.",
|
||||
description: "Stores editor-only component ordering and active-state metadata.",
|
||||
search_terms: &["editor", "order"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Controls editor presentation only; runtime active state is stored separately.",
|
||||
hydration_effect:
|
||||
"Controls editor presentation and authoring component active state.",
|
||||
},
|
||||
],
|
||||
inspectors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers one statically linked game/editor component with reflection,
|
||||
/// lifecycle metadata, and its editor inspector in a single call.
|
||||
pub fn register_authoring_component<T>(
|
||||
app: &mut App,
|
||||
descriptor: EditorComponentDescriptor,
|
||||
inspector: ComponentInspectorFn,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
T: Component + Reflect + GetTypeRegistration,
|
||||
{
|
||||
app.register_type::<T>();
|
||||
if !app.world().contains_resource::<EditorComponentRegistry>() {
|
||||
app.init_resource::<EditorComponentRegistry>();
|
||||
}
|
||||
app.world_mut()
|
||||
.resource_mut::<EditorComponentRegistry>()
|
||||
.register_with_inspector(descriptor, inspector)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn built_in_registry_is_unique_and_reflectable() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(shared::SharedTypesPlugin)
|
||||
.init_resource::<EditorComponentRegistry>();
|
||||
|
||||
app.world()
|
||||
.resource::<EditorComponentRegistry>()
|
||||
.validate(app.world())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_extensions_cannot_shadow_stable_ids_or_types() {
|
||||
let mut registry = EditorComponentRegistry::default();
|
||||
let mut duplicate = registry.descriptors[0].clone();
|
||||
duplicate.type_name = "game::editor_ext::StaticFixture";
|
||||
assert!(registry.register(duplicate).is_err());
|
||||
|
||||
let mut duplicate = registry.descriptors[0].clone();
|
||||
duplicate.id = "game.static_fixture";
|
||||
assert!(registry.register(duplicate).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +91,6 @@ pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot {
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -110,18 +109,8 @@ pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot {
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: shared::EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
@ -153,7 +142,6 @@ pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity {
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -172,18 +160,8 @@ pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity {
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: shared::EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
},
|
||||
)
|
||||
@ -255,7 +233,6 @@ pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Ent
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -274,18 +251,8 @@ pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Ent
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: shared::EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
},
|
||||
)
|
||||
|
||||
@ -4,7 +4,6 @@ use bevy::prelude::*;
|
||||
use shared::{
|
||||
ActorKind, AuthoringLightKind, EditorVisibility, HierarchySiblingIndex, HydratedPrefabMember,
|
||||
LevelObject, LightDesc, ModelRef, PhysicsBody, PlayerSpawn, PrefabInstance,
|
||||
SkinnedMeshRenderer,
|
||||
};
|
||||
|
||||
use super::helpers::{entity_name, is_level_object, HierarchyNodeKind};
|
||||
@ -204,10 +203,7 @@ fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 {
|
||||
.copied()
|
||||
.map(|kind| match kind {
|
||||
ActorKind::Empty => 0,
|
||||
ActorKind::Brush
|
||||
| ActorKind::StaticMesh
|
||||
| ActorKind::SkinnedMesh
|
||||
| ActorKind::ImportedModel => 1,
|
||||
ActorKind::Brush | ActorKind::StaticMesh | ActorKind::ImportedModel => 1,
|
||||
ActorKind::Light => 2,
|
||||
ActorKind::AudioSource | ActorKind::AudioListener => 3,
|
||||
ActorKind::PrefabAnchor => 3,
|
||||
@ -217,7 +213,6 @@ fn actor_kind_sort_key(world: &World, entity: Entity) -> u8 {
|
||||
ActorKind::PostProcessVolume => 6,
|
||||
ActorKind::TeamSpawn => 7,
|
||||
ActorKind::Objective => 8,
|
||||
ActorKind::Navigation => 6,
|
||||
})
|
||||
.unwrap_or(9)
|
||||
}
|
||||
@ -469,7 +464,6 @@ pub fn actor_kind_icon(kind: ActorKind) -> &'static str {
|
||||
ActorKind::Empty => icons::FOLDER.as_str(),
|
||||
ActorKind::Brush => icons::CUBE.as_str(),
|
||||
ActorKind::StaticMesh => icons::CUBE.as_str(),
|
||||
ActorKind::SkinnedMesh => icons::PERSON_SIMPLE_RUN.as_str(),
|
||||
ActorKind::ImportedModel => icons::CUBE_TRANSPARENT.as_str(),
|
||||
ActorKind::Light => icons::LIGHTBULB.as_str(),
|
||||
ActorKind::AudioSource => icons::SPEAKER_HIGH.as_str(),
|
||||
@ -481,7 +475,6 @@ pub fn actor_kind_icon(kind: ActorKind) -> &'static str {
|
||||
ActorKind::PostProcessVolume => icons::CAMERA.as_str(),
|
||||
ActorKind::TeamSpawn => icons::FLAG.as_str(),
|
||||
ActorKind::Objective => icons::TARGET.as_str(),
|
||||
ActorKind::Navigation => icons::PATH.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -541,10 +534,6 @@ pub fn entity_matches_filter(world: &World, entity: Entity, filter_lower: &str)
|
||||
.is_some()
|
||||
.then_some("spawn"),
|
||||
world.get::<ModelRef>(entity).is_some().then_some("model"),
|
||||
world
|
||||
.get::<SkinnedMeshRenderer>(entity)
|
||||
.is_some()
|
||||
.then_some("skinned model"),
|
||||
world
|
||||
.get::<PhysicsBody>(entity)
|
||||
.is_some()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -158,44 +158,6 @@ pub fn top_menu_bar(
|
||||
crate::rendering_diagnostics::spawn_post_process_volume_at_camera(world);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Navigation", |ui| {
|
||||
use super::navigation_inspector::{
|
||||
spawn_navigation_actor, NavigationActorType,
|
||||
};
|
||||
for (label, actor_type) in [
|
||||
("Create Bounds", NavigationActorType::Bounds),
|
||||
("Create Obstacle", NavigationActorType::Obstacle),
|
||||
("Create Area", NavigationActorType::Area),
|
||||
("Create Link", NavigationActorType::Link),
|
||||
] {
|
||||
if menu_item(ui, label, None, true).clicked() {
|
||||
spawn_navigation_actor(world, actor_type);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let selected_bounds =
|
||||
selected.as_slice().iter().copied().find(|entity| {
|
||||
world.get::<shared::NavigationBounds>(*entity).is_some()
|
||||
});
|
||||
if menu_item(ui, "Bake Selected Bounds", None, selected_bounds.is_some())
|
||||
.clicked()
|
||||
{
|
||||
if let Some(entity) = selected_bounds {
|
||||
super::navigation_inspector::bake_bounds(world, entity);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
if menu_item(ui, "Test Selected Path", None, selected_bounds.is_some())
|
||||
.clicked()
|
||||
{
|
||||
if let Some(entity) = selected_bounds {
|
||||
super::navigation_inspector::query_preview(world, entity);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.menu_button("Build", |ui| {
|
||||
|
||||
@ -3,7 +3,7 @@ mod animation_inspector;
|
||||
mod asset_browser;
|
||||
mod audio_inspector;
|
||||
mod build;
|
||||
pub mod component_registry;
|
||||
mod component_registry;
|
||||
mod diagnostics;
|
||||
mod dock_tabs;
|
||||
mod fonts;
|
||||
@ -14,7 +14,6 @@ pub mod hierarchy_state;
|
||||
pub(crate) mod inspector;
|
||||
mod layout;
|
||||
mod menu;
|
||||
pub(crate) mod navigation_inspector;
|
||||
mod play_controls;
|
||||
mod post_process_volume_ui;
|
||||
mod scene_tabs;
|
||||
@ -39,10 +38,6 @@ use crate::project_io::UserPreferences;
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::state::EditorMode;
|
||||
|
||||
pub(crate) use asset_browser::{
|
||||
adopt_material_conflict_save_as, reload_material_after_file_conflict,
|
||||
validate_material_conflict_destination,
|
||||
};
|
||||
pub use build::BuildPanel;
|
||||
pub use diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel};
|
||||
pub use layout::LayoutSaveTimer;
|
||||
@ -219,8 +214,6 @@ impl UiState {
|
||||
project_settings_window(world, ctx, &mut panel);
|
||||
});
|
||||
|
||||
crate::collaboration::file_conflict_modal(world, ctx);
|
||||
|
||||
if ctx.input(|input| input.key_pressed(egui::Key::F1)) {
|
||||
world.resource_mut::<ViewportUiState>().shortcuts_open = true;
|
||||
}
|
||||
@ -301,7 +294,6 @@ impl Plugin for EditorUiPlugin {
|
||||
.init_resource::<inspector::InspectorClipboard>()
|
||||
.init_resource::<inspector::InspectorPanelState>()
|
||||
.init_resource::<animation_inspector::AnimationInspectorState>()
|
||||
.init_resource::<navigation_inspector::NavigationEditorState>()
|
||||
.init_resource::<ViewportUiState>()
|
||||
.init_resource::<LayoutSaveTimer>()
|
||||
.add_systems(
|
||||
@ -309,7 +301,6 @@ impl Plugin for EditorUiPlugin {
|
||||
init_hierarchy_panel_from_prefs
|
||||
.after(crate::project_io::load_user_preferences_on_startup),
|
||||
)
|
||||
.add_systems(Startup, validate_editor_component_registry)
|
||||
.add_systems(
|
||||
Startup,
|
||||
apply_ui_state_from_prefs
|
||||
@ -321,25 +312,11 @@ impl Plugin for EditorUiPlugin {
|
||||
(
|
||||
hierarchy_state::tick_hierarchy_prefs_save,
|
||||
build::tick_build_job,
|
||||
navigation_inspector::sync_navigation_state,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_editor_component_registry(world: &mut World) {
|
||||
let errors = world
|
||||
.resource::<component_registry::EditorComponentRegistry>()
|
||||
.validate(world)
|
||||
.err();
|
||||
if let Some(errors) = errors {
|
||||
panic!(
|
||||
"invalid authoring component registry:\n{}",
|
||||
errors.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_ui_state_from_prefs(mut ui_state: ResMut<UiState>, prefs: Res<UserPreferences>) {
|
||||
*ui_state = UiState::from_prefs(&prefs);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ use egui_phosphor_icons::icons;
|
||||
|
||||
use crate::history::EditorHistory;
|
||||
use crate::operators::ActiveOperator;
|
||||
use crate::project::collaboration::{file_status_indicator_ui, CollaborationState};
|
||||
use crate::scene_io::SceneIo;
|
||||
use crate::state::{EditorMode, PlayPaused};
|
||||
|
||||
@ -48,16 +47,6 @@ pub fn status_bar_ui(
|
||||
.on_hover_text(status);
|
||||
},
|
||||
|ui| {
|
||||
if let (Some(state), Some(path)) = (
|
||||
world.get_resource::<CollaborationState>(),
|
||||
world.resource::<SceneIo>().active_path.as_deref(),
|
||||
) {
|
||||
let status = state.file_status(path);
|
||||
if status.indicator().is_some() {
|
||||
let _ = file_status_indicator_ui(ui, &status, path);
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
let (mode_icon, mode_label, mode_color) = match mode {
|
||||
EditorMode::Editing => (icons::PENCIL_SIMPLE, "EDIT", ACCENT),
|
||||
EditorMode::Playing => {
|
||||
|
||||
@ -141,25 +141,6 @@ fn left_toolbar(world: &mut World, ui: &mut egui::Ui) {
|
||||
if icon_button(ui, icons::PENCIL_SIMPLE_LINE, "Draw brush (B)").clicked() {
|
||||
start_draw_brush_tool(world);
|
||||
}
|
||||
let selected_navigation_bounds = world
|
||||
.resource::<crate::selection::SelectedEntity>()
|
||||
.0
|
||||
.filter(|entity| world.get::<shared::NavigationBounds>(*entity).is_some());
|
||||
let navigation_tip = if selected_navigation_bounds.is_some() {
|
||||
"Bake selected navigation bounds"
|
||||
} else {
|
||||
"Create navigation bounds"
|
||||
};
|
||||
if icon_button(ui, icons::PATH, navigation_tip).clicked() {
|
||||
if let Some(entity) = selected_navigation_bounds {
|
||||
super::navigation_inspector::bake_bounds(world, entity);
|
||||
} else {
|
||||
super::navigation_inspector::spawn_navigation_actor(
|
||||
world,
|
||||
super::navigation_inspector::NavigationActorType::Bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toolbar_separator(ui);
|
||||
|
||||
|
||||
@ -875,7 +875,7 @@ fn viewport_asset_drop_ui(
|
||||
.as_slice()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|entity| world.get::<shared::SkinnedMeshRenderer>(*entity).is_some())
|
||||
.find(|entity| world.get::<shared::ModelRef>(*entity).is_some())
|
||||
{
|
||||
assign_animation_clip_operator(world, selection, entity);
|
||||
} else {
|
||||
@ -1011,7 +1011,7 @@ fn asset_drag_descriptor(
|
||||
} if selected
|
||||
.as_slice()
|
||||
.iter()
|
||||
.any(|entity| world.get::<shared::SkinnedMeshRenderer>(*entity).is_some()) =>
|
||||
.any(|entity| world.get::<shared::ModelRef>(*entity).is_some()) =>
|
||||
{
|
||||
"Assign clip to animated actor"
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ use bevy::shader::ShaderRef;
|
||||
use shared::{
|
||||
ActorKind, AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, LevelObject,
|
||||
LightDesc, ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
|
||||
PrefabInstance, PrefabRef, Primitive, RaytracingExcluded, SkinnedMeshRenderer,
|
||||
StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
PrefabInstance, PrefabRef, Primitive, RaytracingExcluded, StaticMeshRenderer, TeamSpawn,
|
||||
TriggerVolume, WeaponSpawn,
|
||||
};
|
||||
|
||||
use crate::camera::EditorCamera;
|
||||
@ -203,7 +203,6 @@ struct ActorIconComponents {
|
||||
light_kind: Option<AuthoringLightKind>,
|
||||
has_primitive: bool,
|
||||
has_static_mesh_renderer: bool,
|
||||
has_skinned_mesh_renderer: bool,
|
||||
has_model_ref: bool,
|
||||
has_prefab_ref: bool,
|
||||
has_prefab_instance: bool,
|
||||
@ -387,7 +386,6 @@ fn sync_actor_icons(
|
||||
),
|
||||
(With<LevelObject>, Without<EditorOnly>),
|
||||
>,
|
||||
skinned_meshes: Query<(), With<SkinnedMeshRenderer>>,
|
||||
audio: Query<(Has<AudioSourceDesc>, Has<AudioListenerDesc>)>,
|
||||
physics: Query<(), Or<(With<PhysicsBody>, With<ColliderDesc>)>>,
|
||||
) {
|
||||
@ -421,7 +419,6 @@ fn sync_actor_icons(
|
||||
has_primitive,
|
||||
has_static_mesh_renderer: static_mesh_renderer
|
||||
.is_some_and(has_resolved_static_mesh_slot),
|
||||
has_skinned_mesh_renderer: skinned_meshes.contains(target),
|
||||
has_model_ref,
|
||||
has_prefab_ref,
|
||||
has_prefab_instance,
|
||||
@ -492,7 +489,6 @@ fn sync_actor_icons(
|
||||
has_primitive,
|
||||
has_static_mesh_renderer: static_mesh_renderer
|
||||
.is_some_and(has_resolved_static_mesh_slot),
|
||||
has_skinned_mesh_renderer: skinned_meshes.contains(target),
|
||||
has_model_ref,
|
||||
has_prefab_ref,
|
||||
has_prefab_instance,
|
||||
@ -641,16 +637,6 @@ fn icon_for_actor_components(components: ActorIconComponents) -> ActorIconSpec {
|
||||
warning
|
||||
}
|
||||
}
|
||||
ActorKind::SkinnedMesh => {
|
||||
if components.has_skinned_mesh_renderer {
|
||||
ActorIconSpec {
|
||||
image: ActorIconImage::Mesh,
|
||||
category: ActorIconCategory::Mesh,
|
||||
}
|
||||
} else {
|
||||
warning
|
||||
}
|
||||
}
|
||||
ActorKind::ImportedModel => {
|
||||
if components.has_model_ref {
|
||||
ActorIconSpec {
|
||||
@ -766,10 +752,6 @@ fn icon_for_actor_components(components: ActorIconComponents) -> ActorIconSpec {
|
||||
warning
|
||||
}
|
||||
}
|
||||
ActorKind::Navigation => ActorIconSpec {
|
||||
image: ActorIconImage::GameMarker,
|
||||
category: ActorIconCategory::Volume,
|
||||
},
|
||||
ActorKind::Empty => {
|
||||
if components.has_physics {
|
||||
ActorIconSpec {
|
||||
|
||||
@ -374,7 +374,6 @@ fn brush_snapshot_from_convex_points(
|
||||
primitive: None,
|
||||
brush: Some(brush),
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: Some(MaterialDesc::default()),
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -393,18 +392,8 @@ fn brush_snapshot_from_convex_points(
|
||||
post_process_volume: None,
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -290,33 +290,14 @@ fn solari_geometry_summary(world: &World, ui: &mut egui::Ui) {
|
||||
ui.separator();
|
||||
ui.heading("Solari raytracing scene");
|
||||
ui.label(format!(
|
||||
"Project meshes: {} tagged, {} eligible, {} Solari-compatible assets, {} unsupported, {} deformed safely omitted, {} excluded helpers, {} outside level roots",
|
||||
"Project meshes: {} tagged, {} eligible, {} Solari-compatible assets, {} unsupported, {} excluded helpers, {} outside level roots",
|
||||
stats.tagged_meshes,
|
||||
stats.project_meshes,
|
||||
stats.compatible_mesh_assets,
|
||||
stats.unsupported_meshes,
|
||||
stats.deformed_meshes_excluded,
|
||||
stats.excluded_meshes,
|
||||
stats.missing_level_root_meshes
|
||||
));
|
||||
if stats.surface_material_meshes > 0 {
|
||||
ui.label(format!(
|
||||
"Surface ABI materials: {} meshes, {} missing an active evaluator generation",
|
||||
stats.surface_material_meshes, stats.surface_materials_missing_evaluator
|
||||
));
|
||||
}
|
||||
if stats.deformed_meshes_excluded > 0 {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(255, 180, 100),
|
||||
"Skinned/morphed meshes are omitted from Solari until dynamic deformed BLAS is available; no bind-pose proxy is traced.",
|
||||
);
|
||||
}
|
||||
if stats.surface_materials_missing_evaluator > 0 {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(255, 140, 140),
|
||||
"Surface meshes without a validated evaluator are omitted instead of using an approximate ray-tracing material.",
|
||||
);
|
||||
}
|
||||
if stats.incompatible_mesh_assets > 0 {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(255, 180, 100),
|
||||
@ -476,7 +457,6 @@ pub fn spawn_post_process_volume_at_camera(world: &mut World) {
|
||||
primitive: None,
|
||||
brush: None,
|
||||
static_mesh_renderer: None,
|
||||
skinned_mesh_renderer: None,
|
||||
material: None,
|
||||
material_override: None,
|
||||
rigid_body: None,
|
||||
@ -495,18 +475,8 @@ pub fn spawn_post_process_volume_at_camera(world: &mut World) {
|
||||
post_process_volume: Some(PostProcessVolumeDesc::default()),
|
||||
team_spawn: None,
|
||||
objective: None,
|
||||
|
||||
navigation_bounds: None,
|
||||
|
||||
navigation_obstacle: None,
|
||||
|
||||
navigation_area: None,
|
||||
|
||||
navigation_link: None,
|
||||
hierarchy_sibling_index: 0,
|
||||
editor_visibility: EditorVisibility::default(),
|
||||
inspector_order: None,
|
||||
component_states: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
spawn_with_history(world, snapshot);
|
||||
|
||||
@ -5,9 +5,8 @@ use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
use shared::{
|
||||
AudioListenerDesc, AudioSourceDesc, AuthoringLightKind, ColliderDesc, ColliderShapeDesc,
|
||||
LevelObject, LightDesc, ModelRef, NavigationArea, NavigationBounds, NavigationLink,
|
||||
NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc,
|
||||
PrefabRef, ProjectSun, RaytracingExcluded, SkinnedMeshRenderer, TeamSpawn, TriggerVolume,
|
||||
LevelObject, LightDesc, ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn,
|
||||
PostProcessVolumeDesc, PrefabRef, ProjectSun, RaytracingExcluded, TeamSpawn, TriggerVolume,
|
||||
WeaponSpawn,
|
||||
};
|
||||
use sim::tuning::{EYE_OFFSET_STAND, PLAYER_LENGTH, PLAYER_RADIUS};
|
||||
@ -108,15 +107,7 @@ struct VisualizerTargetQueries<'w, 's> {
|
||||
lights: Query<'w, 's, Entity, (With<LevelObject>, With<LightDesc>)>,
|
||||
spawns: Query<'w, 's, Entity, (With<LevelObject>, With<PlayerSpawn>)>,
|
||||
prefabs: Query<'w, 's, Entity, (With<LevelObject>, With<PrefabRef>)>,
|
||||
models: Query<
|
||||
'w,
|
||||
's,
|
||||
Entity,
|
||||
(
|
||||
With<LevelObject>,
|
||||
Or<(With<ModelRef>, With<SkinnedMeshRenderer>)>,
|
||||
),
|
||||
>,
|
||||
models: Query<'w, 's, Entity, (With<LevelObject>, With<ModelRef>)>,
|
||||
players: Query<'w, 's, Entity, With<Player>>,
|
||||
cameras: Query<'w, 's, Entity, With<PlayerCamera>>,
|
||||
project_suns: Query<'w, 's, Entity, With<ProjectSun>>,
|
||||
@ -136,7 +127,6 @@ impl Plugin for EditorVisualizersPlugin {
|
||||
draw_audio_visualizers,
|
||||
draw_split_collider_visualizers,
|
||||
draw_post_process_volume_gizmos,
|
||||
draw_navigation_visualizers,
|
||||
)
|
||||
.chain()
|
||||
.run_if(scene_tools_active)
|
||||
@ -145,131 +135,6 @@ impl Plugin for EditorVisualizersPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_navigation_visualizers(
|
||||
state: Res<crate::ui::navigation_inspector::NavigationEditorState>,
|
||||
mut gizmos: Gizmos,
|
||||
bounds: Query<(&NavigationBounds, &GlobalTransform), With<LevelObject>>,
|
||||
obstacles: Query<(&NavigationObstacle, &GlobalTransform), With<LevelObject>>,
|
||||
areas: Query<(&NavigationArea, &GlobalTransform), With<LevelObject>>,
|
||||
links: Query<(&NavigationLink, &GlobalTransform), With<LevelObject>>,
|
||||
) {
|
||||
for (value, global) in &bounds {
|
||||
let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
|
||||
global.affine(),
|
||||
value.half_extents,
|
||||
);
|
||||
let center = global.translation();
|
||||
draw_box(
|
||||
&mut gizmos,
|
||||
center,
|
||||
Quat::IDENTITY,
|
||||
half_extents,
|
||||
Color::srgba(0.12, 0.82, 0.78, 0.72),
|
||||
);
|
||||
}
|
||||
for (value, global) in &obstacles {
|
||||
let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
|
||||
global.affine(),
|
||||
value.half_extents,
|
||||
);
|
||||
let center = global.translation();
|
||||
draw_box(
|
||||
&mut gizmos,
|
||||
center,
|
||||
Quat::IDENTITY,
|
||||
half_extents,
|
||||
Color::srgba(1.0, 0.28, 0.22, 0.78),
|
||||
);
|
||||
}
|
||||
for (value, global) in &areas {
|
||||
let half_extents = crate::ui::navigation_inspector::world_aabb_half_extents(
|
||||
global.affine(),
|
||||
value.half_extents,
|
||||
);
|
||||
let center = global.translation();
|
||||
let color = if value.walkable {
|
||||
Color::srgba(0.32, 0.72, 1.0, 0.62)
|
||||
} else {
|
||||
Color::srgba(1.0, 0.55, 0.18, 0.72)
|
||||
};
|
||||
draw_box(&mut gizmos, center, Quat::IDENTITY, half_extents, color);
|
||||
}
|
||||
for (value, global) in &links {
|
||||
let start = global.transform_point(value.start);
|
||||
let end = global.transform_point(value.end);
|
||||
let color = if value.enabled {
|
||||
Color::srgba(0.95, 0.78, 0.22, 0.95)
|
||||
} else {
|
||||
Color::srgba(0.48, 0.48, 0.5, 0.55)
|
||||
};
|
||||
gizmos.line(start, end, color);
|
||||
gizmos
|
||||
.sphere(Isometry3d::from_translation(start), 0.11, color)
|
||||
.resolution(12);
|
||||
gizmos
|
||||
.sphere(Isometry3d::from_translation(end), 0.11, color)
|
||||
.resolution(12);
|
||||
}
|
||||
|
||||
if state.show_mesh {
|
||||
if let Some(artifact) = &state.artifact {
|
||||
for layer in &artifact.mesh.layers {
|
||||
for polygon in &layer.polygons {
|
||||
for index in 0..polygon.vertices.len() {
|
||||
let a_index = polygon.vertices[index] as usize;
|
||||
let b_index =
|
||||
polygon.vertices[(index + 1) % polygon.vertices.len()] as usize;
|
||||
let Some(a) = navigation_vertex(layer, a_index) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = navigation_vertex(layer, b_index) else {
|
||||
continue;
|
||||
};
|
||||
gizmos.line(
|
||||
a + Vec3::Y * 0.025,
|
||||
b + Vec3::Y * 0.025,
|
||||
Color::srgba(0.08, 0.9, 0.72, 0.7),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for edge in state.preview_path.windows(2) {
|
||||
gizmos.line(
|
||||
edge[0] + Vec3::Y * 0.08,
|
||||
edge[1] + Vec3::Y * 0.08,
|
||||
Color::srgba(0.98, 0.88, 0.2, 1.0),
|
||||
);
|
||||
}
|
||||
if let Some(start) = state.preview_path.first().copied() {
|
||||
gizmos
|
||||
.sphere(
|
||||
Isometry3d::from_translation(start + Vec3::Y * 0.08),
|
||||
0.16,
|
||||
Color::srgba(0.2, 1.0, 0.55, 1.0),
|
||||
)
|
||||
.resolution(14);
|
||||
}
|
||||
if let Some(end) = state.preview_path.last().copied() {
|
||||
gizmos
|
||||
.sphere(
|
||||
Isometry3d::from_translation(end + Vec3::Y * 0.08),
|
||||
0.16,
|
||||
Color::srgba(1.0, 0.28, 0.22, 1.0),
|
||||
)
|
||||
.resolution(14);
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_vertex(layer: &polyanya::Layer, index: usize) -> Option<Vec3> {
|
||||
let vertex = layer.vertices.get(index)?;
|
||||
let height = layer.height.get(index).copied().unwrap_or_default();
|
||||
let coords = vertex.coords + layer.offset;
|
||||
Some(Vec3::new(coords.x, height, coords.y))
|
||||
}
|
||||
|
||||
fn draw_audio_visualizers(
|
||||
viewport_mode: Res<EditorViewportMode>,
|
||||
settings: Res<EditorVisualizationSettings>,
|
||||
@ -534,13 +399,7 @@ fn draw_visualizers(
|
||||
lights: Query<(Entity, &LightDesc, &GlobalTransform), With<LevelObject>>,
|
||||
spawns: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<PlayerSpawn>)>,
|
||||
prefabs: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<PrefabRef>)>,
|
||||
models: Query<
|
||||
(Entity, &GlobalTransform),
|
||||
(
|
||||
With<LevelObject>,
|
||||
Or<(With<ModelRef>, With<SkinnedMeshRenderer>)>,
|
||||
),
|
||||
>,
|
||||
models: Query<(Entity, &GlobalTransform), (With<LevelObject>, With<ModelRef>)>,
|
||||
players: Query<(Entity, &GlobalTransform), With<Player>>,
|
||||
cameras: Query<(Entity, &GlobalTransform), With<PlayerCamera>>,
|
||||
project_suns: Query<(Entity, &GlobalTransform), With<ProjectSun>>,
|
||||
|
||||
@ -18,8 +18,6 @@ bevy_ufbx.workspace = true
|
||||
cpal = "0.17.3"
|
||||
game_hot = { path = "../game_hot" }
|
||||
hot-lib-reloader = { version = "0.8.2", optional = true }
|
||||
nav_glam.workspace = true
|
||||
polyanya.workspace = true
|
||||
protocol.workspace = true
|
||||
ron = "0.12"
|
||||
scene.workspace = true
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! Runtime adapter for scene-authored animation controllers.
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
@ -12,14 +12,11 @@ use bevy::animation::{
|
||||
RepeatAnimation,
|
||||
};
|
||||
use bevy::gltf::GltfAssetLabel;
|
||||
use bevy::mesh::morph::MorphWeights;
|
||||
use bevy::mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes};
|
||||
use bevy::prelude::*;
|
||||
use bevy::world_serialization::WorldInstanceReady;
|
||||
use shared::{
|
||||
animation_clip_source_index, asset_server_path, AnimationControllerDesc, AnimationEventDesc,
|
||||
AnimationManifest, AnimationStateDesc, HydratedSkinnedMeshRoot, ANIMATION_ARTIFACT_DIR,
|
||||
ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
AnimationManifest, AnimationStateDesc, HydratedModelRoot, ANIMATION_ARTIFACT_DIR,
|
||||
};
|
||||
use sim::SimEnabled;
|
||||
|
||||
@ -117,29 +114,6 @@ pub struct AuthoredAnimationRuntime {
|
||||
last_event_time: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SkinnedPoseBaselineEntry {
|
||||
entity: Entity,
|
||||
transform: Option<Transform>,
|
||||
morph_weights: Option<MorphWeights>,
|
||||
}
|
||||
|
||||
/// Imported rest-pose state captured before Blacksite starts authored animation playback.
|
||||
///
|
||||
/// This component is runtime-only. It is replaced whenever a new hydrated hierarchy becomes
|
||||
/// ready, so renderer reloads cannot restore values onto an older scene generation.
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct SkinnedPoseBaseline {
|
||||
pub root: Entity,
|
||||
entries: Vec<SkinnedPoseBaselineEntry>,
|
||||
}
|
||||
|
||||
impl SkinnedPoseBaseline {
|
||||
pub fn entry_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Editor-only transient preview request consumed by the shared runtime adapter.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AnimationPreviewRequest {
|
||||
@ -189,14 +163,10 @@ impl Plugin for GameAnimationPlugin {
|
||||
fn attach_controller_when_model_ready(
|
||||
ready: On<WorldInstanceReady>,
|
||||
mut commands: Commands,
|
||||
roots: Query<&HydratedSkinnedMeshRoot>,
|
||||
roots: Query<&HydratedModelRoot>,
|
||||
controllers: Query<&AnimationControllerDesc>,
|
||||
children: Query<&Children>,
|
||||
players: Query<Entity, With<AnimationPlayer>>,
|
||||
transforms: Query<&Transform>,
|
||||
morph_weights: Query<&MorphWeights>,
|
||||
skinned_meshes: Query<(Entity, &SkinnedMesh)>,
|
||||
inverse_bindposes: Option<Res<Assets<SkinnedMeshInverseBindposes>>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
mut diagnostics: ResMut<AnimationRuntimeDiagnostics>,
|
||||
@ -205,18 +175,6 @@ fn attach_controller_when_model_ready(
|
||||
let Ok(root) = roots.get(ready.entity) else {
|
||||
return;
|
||||
};
|
||||
let subtree = std::iter::once(ready.entity)
|
||||
.chain(children.iter_descendants(ready.entity))
|
||||
.collect::<HashSet<_>>();
|
||||
let baseline = capture_pose_baseline(ready.entity, &subtree, &transforms, &morph_weights);
|
||||
commands.entity(root.owner).insert(baseline);
|
||||
validate_skinned_bindings(
|
||||
root.owner,
|
||||
&subtree,
|
||||
&skinned_meshes,
|
||||
inverse_bindposes.as_deref(),
|
||||
&mut diagnostics,
|
||||
);
|
||||
let Ok(controller) = controllers.get(root.owner) else {
|
||||
return;
|
||||
};
|
||||
@ -224,7 +182,7 @@ fn attach_controller_when_model_ready(
|
||||
diagnostics.push(
|
||||
Some(root.owner),
|
||||
"animation.player_missing",
|
||||
"The hydrated skinned mesh contains no AnimationPlayer. Verify that the selected glTF scene contains an animated hierarchy.",
|
||||
"The hydrated model contains no AnimationPlayer. Verify that the selected glTF scene contains an animated hierarchy.",
|
||||
);
|
||||
return;
|
||||
};
|
||||
@ -241,75 +199,6 @@ fn attach_controller_when_model_ready(
|
||||
);
|
||||
}
|
||||
|
||||
fn capture_pose_baseline(
|
||||
root: Entity,
|
||||
subtree: &HashSet<Entity>,
|
||||
transforms: &Query<&Transform>,
|
||||
morph_weights: &Query<&MorphWeights>,
|
||||
) -> SkinnedPoseBaseline {
|
||||
let mut entities = subtree.iter().copied().collect::<Vec<_>>();
|
||||
entities.sort_by_key(|entity| entity.to_bits());
|
||||
let entries = entities
|
||||
.into_iter()
|
||||
.filter_map(|entity| {
|
||||
let transform = transforms.get(entity).ok().copied();
|
||||
let morph_weights = morph_weights.get(entity).ok().cloned();
|
||||
(transform.is_some() || morph_weights.is_some()).then_some(SkinnedPoseBaselineEntry {
|
||||
entity,
|
||||
transform,
|
||||
morph_weights,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
SkinnedPoseBaseline { root, entries }
|
||||
}
|
||||
|
||||
fn validate_skinned_bindings(
|
||||
actor: Entity,
|
||||
subtree: &HashSet<Entity>,
|
||||
skinned_meshes: &Query<(Entity, &SkinnedMesh)>,
|
||||
inverse_bindposes: Option<&Assets<SkinnedMeshInverseBindposes>>,
|
||||
diagnostics: &mut AnimationRuntimeDiagnostics,
|
||||
) {
|
||||
for (mesh_entity, skin) in skinned_meshes.iter() {
|
||||
if !subtree.contains(&mesh_entity) {
|
||||
continue;
|
||||
}
|
||||
if let Some(inverse_bindposes) = inverse_bindposes {
|
||||
let Some(bindposes) = inverse_bindposes.get(&skin.inverse_bindposes) else {
|
||||
diagnostics.push(
|
||||
Some(actor),
|
||||
"animation.inverse_bindposes_missing",
|
||||
format!(
|
||||
"Skinned mesh {mesh_entity} has no loaded inverse-bind-pose asset; the imported hierarchy was preserved but cannot be validated."
|
||||
),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if skin.joints.len() != bindposes.len() {
|
||||
diagnostics.push(
|
||||
Some(actor),
|
||||
"animation.skin_binding_count_mismatch",
|
||||
format!(
|
||||
"Skinned mesh {mesh_entity} has {} joints but {} inverse bind poses. Re-export the rig and reimport the model.",
|
||||
skin.joints.len(),
|
||||
bindposes.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(joint) = skin.joints.iter().find(|joint| !subtree.contains(joint)) {
|
||||
diagnostics.push(
|
||||
Some(actor),
|
||||
"animation.skin_joint_outside_hierarchy",
|
||||
format!(
|
||||
"Skinned mesh {mesh_entity} references joint {joint} outside its hydrated hierarchy. Re-export the rig as one scene hierarchy."
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn reconcile_changed_controllers(
|
||||
mut commands: Commands,
|
||||
@ -320,7 +209,7 @@ fn reconcile_changed_controllers(
|
||||
Changed<AnimationControllerDesc>,
|
||||
)>,
|
||||
>,
|
||||
roots: Query<(Entity, &HydratedSkinnedMeshRoot)>,
|
||||
roots: Query<(Entity, &HydratedModelRoot)>,
|
||||
children: Query<&Children>,
|
||||
players: Query<Entity, With<AnimationPlayer>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
@ -377,9 +266,6 @@ fn configure_runtime(
|
||||
Some(controller.default_crossfade_seconds),
|
||||
)
|
||||
});
|
||||
commands.queue(move |world: &mut World| {
|
||||
restore_pose_baseline(world, actor);
|
||||
});
|
||||
commands.entity(player).insert((
|
||||
AnimationGraphHandle(graph.clone()),
|
||||
AnimationTransitions::new(),
|
||||
@ -412,7 +298,6 @@ fn clear_failed_runtime_configuration(commands: &mut Commands, actor: Entity, pl
|
||||
if let Some(mut animation_player) = world.get_mut::<AnimationPlayer>(player) {
|
||||
animation_player.stop_all();
|
||||
}
|
||||
restore_pose_baseline(world, actor);
|
||||
if let Ok(mut player_entity) = world.get_entity_mut(player) {
|
||||
player_entity
|
||||
.remove::<AnimationGraphHandle>()
|
||||
@ -583,17 +468,7 @@ fn load_runtime_animation_manifest(
|
||||
let path = runtime_animation_manifest_path(assets_root, asset_id);
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.map_err(|error| format!("could not read {}: {error}", path.display()))?;
|
||||
let manifest: AnimationManifest = ron::from_str(&text)
|
||||
.map_err(|error| format!("could not parse {}: {error}", path.display()))?;
|
||||
if manifest.schema_version != ANIMATION_MANIFEST_SCHEMA_VERSION {
|
||||
return Err(format!(
|
||||
"animation manifest {} uses schema {} but runtime requires {}; reimport the source model",
|
||||
path.display(),
|
||||
manifest.schema_version,
|
||||
ANIMATION_MANIFEST_SCHEMA_VERSION
|
||||
));
|
||||
}
|
||||
Ok(manifest)
|
||||
ron::from_str(&text).map_err(|error| format!("could not parse {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn process_animation_state_requests(
|
||||
@ -675,18 +550,8 @@ fn sync_authored_animation_playback(
|
||||
.request
|
||||
.as_ref()
|
||||
.filter(|request| request.actor == actor);
|
||||
let desired_state = preview_request
|
||||
.map(|request| request.state_id.as_str())
|
||||
.or(runtime.desired_state.as_deref())
|
||||
.or((!controller.default_state.trim().is_empty())
|
||||
.then_some(controller.default_state.as_str()))
|
||||
.map(str::to_owned);
|
||||
let sampling_edit_rest = !sim_enabled.0
|
||||
&& preview_request.is_none()
|
||||
&& runtime.active_state.is_none()
|
||||
&& desired_state.is_some();
|
||||
let should_evaluate = sim_enabled.0 || preview_request.is_some() || sampling_edit_rest;
|
||||
if !should_evaluate {
|
||||
let should_run = sim_enabled.0 || preview_request.is_some();
|
||||
if !should_run {
|
||||
if runtime.active_state.is_some() {
|
||||
player.pause_all();
|
||||
runtime.paused_by_sim = true;
|
||||
@ -707,6 +572,12 @@ fn sync_authored_animation_playback(
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let desired_state = preview_request
|
||||
.map(|request| request.state_id.as_str())
|
||||
.or(runtime.desired_state.as_deref())
|
||||
.or((!controller.default_state.trim().is_empty())
|
||||
.then_some(controller.default_state.as_str()))
|
||||
.map(str::to_owned);
|
||||
let Some(desired_state) = desired_state else {
|
||||
continue;
|
||||
};
|
||||
@ -729,7 +600,7 @@ fn sync_authored_animation_playback(
|
||||
};
|
||||
|
||||
if runtime.active_state.as_deref() != Some(desired_state.as_str()) {
|
||||
let crossfade = if preview_request.is_some() || sampling_edit_rest {
|
||||
let crossfade = if preview_request.is_some() {
|
||||
0.0
|
||||
} else {
|
||||
runtime
|
||||
@ -959,49 +830,23 @@ fn crossed_animation_events<'a>(
|
||||
crossed
|
||||
}
|
||||
|
||||
fn cleanup_stale_animation_runtimes(world: &mut World) {
|
||||
let runtimes = world
|
||||
.query::<(Entity, &AuthoredAnimationRuntime)>()
|
||||
.iter(world)
|
||||
.map(|(actor, runtime)| (actor, runtime.player))
|
||||
.collect::<Vec<_>>();
|
||||
let stale = runtimes
|
||||
.into_iter()
|
||||
.filter(|(actor, player)| {
|
||||
let controller_exists = world.get::<AnimationControllerDesc>(*actor).is_some();
|
||||
let player_exists = world.get::<AnimationPlayer>(*player).is_some();
|
||||
!controller_exists || !player_exists
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (actor, player) in stale {
|
||||
if let Some(mut animation_player) = world.get_mut::<AnimationPlayer>(player) {
|
||||
animation_player.stop_all();
|
||||
fn cleanup_stale_animation_runtimes(
|
||||
mut commands: Commands,
|
||||
controllers: Query<(), With<AnimationControllerDesc>>,
|
||||
runtimes: Query<(Entity, &AuthoredAnimationRuntime)>,
|
||||
mut players: Query<&mut AnimationPlayer>,
|
||||
) {
|
||||
for (actor, runtime) in &runtimes {
|
||||
if controllers.get(actor).is_ok() && players.get(runtime.player).is_ok() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(mut player_entity) = world.get_entity_mut(player) {
|
||||
player_entity.remove::<SuspendedAnimationTransitions>();
|
||||
}
|
||||
restore_pose_baseline(world, actor);
|
||||
if let Ok(mut actor_entity) = world.get_entity_mut(actor) {
|
||||
actor_entity.remove::<AuthoredAnimationRuntime>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_pose_baseline(world: &mut World, actor: Entity) {
|
||||
let Some(baseline) = world.get::<SkinnedPoseBaseline>(actor).cloned() else {
|
||||
return;
|
||||
};
|
||||
for entry in baseline.entries {
|
||||
if let Some(transform) = entry.transform {
|
||||
if let Some(mut current) = world.get_mut::<Transform>(entry.entity) {
|
||||
*current = transform;
|
||||
}
|
||||
}
|
||||
if let Some(morph_weights) = entry.morph_weights {
|
||||
if let Some(mut current) = world.get_mut::<MorphWeights>(entry.entity) {
|
||||
*current = morph_weights;
|
||||
}
|
||||
if let Ok(mut player) = players.get_mut(runtime.player) {
|
||||
player.stop_all();
|
||||
commands
|
||||
.entity(runtime.player)
|
||||
.remove::<SuspendedAnimationTransitions>();
|
||||
}
|
||||
commands.entity(actor).remove::<AuthoredAnimationRuntime>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1074,15 +919,8 @@ pub fn stop_animation_preview(world: &mut World) {
|
||||
animation_player.stop_all();
|
||||
}
|
||||
}
|
||||
restore_pose_baseline(world, actor);
|
||||
let edit_rest_state = world
|
||||
.get::<AnimationControllerDesc>(actor)
|
||||
.map(|controller| controller.default_state.clone())
|
||||
.filter(|state| !state.trim().is_empty());
|
||||
if let Some(mut runtime) = world.get_mut::<AuthoredAnimationRuntime>(actor) {
|
||||
runtime.active_state = None;
|
||||
runtime.desired_state = edit_rest_state;
|
||||
runtime.requested_crossfade_seconds = Some(0.0);
|
||||
runtime.paused_by_sim = false;
|
||||
runtime.applied_preview_revision = None;
|
||||
runtime.last_event_time = None;
|
||||
@ -1105,15 +943,10 @@ pub fn reset_authored_animation_runtime(world: &mut World) {
|
||||
if let Some(mut animation_player) = world.get_mut::<AnimationPlayer>(player) {
|
||||
animation_player.stop_all();
|
||||
}
|
||||
restore_pose_baseline(world, actor);
|
||||
let edit_rest_state = world
|
||||
.get::<AnimationControllerDesc>(actor)
|
||||
.map(|controller| controller.default_state.clone())
|
||||
.filter(|state| !state.trim().is_empty());
|
||||
if let Some(mut runtime) = world.get_mut::<AuthoredAnimationRuntime>(actor) {
|
||||
runtime.active_state = None;
|
||||
runtime.desired_state = edit_rest_state;
|
||||
runtime.requested_crossfade_seconds = Some(0.0);
|
||||
runtime.desired_state = None;
|
||||
runtime.requested_crossfade_seconds = None;
|
||||
runtime.paused_by_sim = false;
|
||||
runtime.applied_preview_revision = None;
|
||||
runtime.last_event_time = None;
|
||||
@ -1127,7 +960,7 @@ mod tests {
|
||||
use bevy::asset::{AssetApp, AssetPlugin};
|
||||
use shared::{
|
||||
AnimationClipRecord, AnimationManifestSource, AnimationPlaybackRange,
|
||||
AnimationSourceFingerprint, EditorAssetRef,
|
||||
AnimationSourceFingerprint, EditorAssetRef, ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
};
|
||||
|
||||
fn state(id: &str, index: usize) -> AnimationStateDesc {
|
||||
@ -1261,7 +1094,6 @@ mod tests {
|
||||
schema_version: ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||
asset_id: state.clip.asset_id.clone(),
|
||||
label: "Moved Model".into(),
|
||||
default_animation_clip_id: None,
|
||||
source: AnimationManifestSource {
|
||||
path: "assets/models/relocated.glb".into(),
|
||||
format: "glb".into(),
|
||||
@ -1312,7 +1144,7 @@ mod tests {
|
||||
let actor = app.world_mut().spawn_empty().id();
|
||||
let root = app
|
||||
.world_mut()
|
||||
.spawn((HydratedSkinnedMeshRoot { owner: actor }, ChildOf(actor)))
|
||||
.spawn((HydratedModelRoot { owner: actor }, ChildOf(actor)))
|
||||
.id();
|
||||
let stale_node = AnimationNodeIndex::new(0);
|
||||
let mut animation_player = AnimationPlayer::default();
|
||||
@ -1530,202 +1362,6 @@ mod tests {
|
||||
.all_paused());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_mode_samples_and_pauses_the_explicit_default_state() {
|
||||
let (mut app, actor, player, _) = runtime_test_app(false);
|
||||
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
app.world()
|
||||
.get::<AuthoredAnimationRuntime>(actor)
|
||||
.and_then(|runtime| runtime.active_state.as_deref()),
|
||||
Some("idle")
|
||||
);
|
||||
assert!(app
|
||||
.world()
|
||||
.get::<AnimationPlayer>(player)
|
||||
.is_some_and(AnimationPlayer::all_paused));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stopping_preview_restores_transform_and_morph_baseline_before_resampling() {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<AnimationPreviewRuntime>();
|
||||
let baseline_transform = Transform::from_xyz(1.0, 2.0, 3.0);
|
||||
let baseline_morph = MorphWeights::new(vec![0.1, 0.9], None).unwrap();
|
||||
let target = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
Transform::from_xyz(9.0, 8.0, 7.0),
|
||||
MorphWeights::new(vec![0.8, 0.2], None).unwrap(),
|
||||
))
|
||||
.id();
|
||||
let node = AnimationNodeIndex::new(0);
|
||||
let mut animation_player = AnimationPlayer::default();
|
||||
animation_player.start(node);
|
||||
let player = app.world_mut().spawn(animation_player).id();
|
||||
let actor = app
|
||||
.world_mut()
|
||||
.spawn(AnimationControllerDesc {
|
||||
states: vec![state("idle", 0)],
|
||||
default_state: "idle".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.id();
|
||||
app.world_mut().entity_mut(actor).insert((
|
||||
SkinnedPoseBaseline {
|
||||
root: target,
|
||||
entries: vec![SkinnedPoseBaselineEntry {
|
||||
entity: target,
|
||||
transform: Some(baseline_transform),
|
||||
morph_weights: Some(baseline_morph),
|
||||
}],
|
||||
},
|
||||
AuthoredAnimationRuntime {
|
||||
player,
|
||||
graph: Handle::default(),
|
||||
state_nodes: HashMap::from([("idle".into(), node)]),
|
||||
active_state: Some("idle".into()),
|
||||
desired_state: Some("idle".into()),
|
||||
requested_crossfade_seconds: None,
|
||||
paused_by_sim: false,
|
||||
applied_preview_revision: None,
|
||||
metadata_by_state: HashMap::new(),
|
||||
last_event_time: None,
|
||||
},
|
||||
));
|
||||
set_animation_preview(
|
||||
app.world_mut(),
|
||||
AnimationPreviewRequest {
|
||||
actor,
|
||||
state_id: "idle".into(),
|
||||
seek_seconds: Some(0.7),
|
||||
playing: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
stop_animation_preview(app.world_mut());
|
||||
|
||||
assert_eq!(
|
||||
app.world().get::<Transform>(target),
|
||||
Some(&baseline_transform)
|
||||
);
|
||||
assert_eq!(
|
||||
app.world().get::<MorphWeights>(target).unwrap().weights(),
|
||||
&[0.1, 0.9]
|
||||
);
|
||||
assert!(app
|
||||
.world()
|
||||
.get::<AnimationPlayer>(player)
|
||||
.unwrap()
|
||||
.playing_animations()
|
||||
.next()
|
||||
.is_none());
|
||||
let runtime = app.world().get::<AuthoredAnimationRuntime>(actor).unwrap();
|
||||
assert!(runtime.active_state.is_none());
|
||||
assert_eq!(runtime.desired_state.as_deref(), Some("idle"));
|
||||
assert_eq!(runtime.requested_crossfade_seconds, Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_controller_restores_baseline_and_clears_runtime() {
|
||||
let mut world = World::new();
|
||||
let target = world.spawn(Transform::from_xyz(9.0, 0.0, 0.0)).id();
|
||||
let player = world.spawn(AnimationPlayer::default()).id();
|
||||
let actor = world
|
||||
.spawn((
|
||||
SkinnedPoseBaseline {
|
||||
root: target,
|
||||
entries: vec![SkinnedPoseBaselineEntry {
|
||||
entity: target,
|
||||
transform: Some(Transform::from_xyz(1.0, 0.0, 0.0)),
|
||||
morph_weights: None,
|
||||
}],
|
||||
},
|
||||
AuthoredAnimationRuntime {
|
||||
player,
|
||||
graph: Handle::default(),
|
||||
state_nodes: HashMap::new(),
|
||||
active_state: Some("idle".into()),
|
||||
desired_state: Some("idle".into()),
|
||||
requested_crossfade_seconds: None,
|
||||
paused_by_sim: false,
|
||||
applied_preview_revision: None,
|
||||
metadata_by_state: HashMap::new(),
|
||||
last_event_time: None,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
|
||||
cleanup_stale_animation_runtimes(&mut world);
|
||||
|
||||
assert_eq!(world.get::<Transform>(target).unwrap().translation, Vec3::X);
|
||||
assert!(world.get::<AuthoredAnimationRuntime>(actor).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_skin_records_are_validated_independently_against_shared_joints() {
|
||||
let mut world = World::new();
|
||||
let actor = world.spawn_empty().id();
|
||||
let root = world.spawn_empty().id();
|
||||
let joint = world.spawn_empty().id();
|
||||
let mut inverse_bindposes = Assets::<SkinnedMeshInverseBindposes>::default();
|
||||
let first_bindposes =
|
||||
inverse_bindposes.add(SkinnedMeshInverseBindposes::from(vec![Mat4::IDENTITY]));
|
||||
let second_bindposes =
|
||||
inverse_bindposes.add(SkinnedMeshInverseBindposes::from(vec![Mat4::IDENTITY]));
|
||||
let first_mesh = world
|
||||
.spawn(SkinnedMesh {
|
||||
inverse_bindposes: first_bindposes,
|
||||
joints: vec![joint],
|
||||
})
|
||||
.id();
|
||||
let second_mesh = world
|
||||
.spawn(SkinnedMesh {
|
||||
inverse_bindposes: second_bindposes,
|
||||
joints: vec![joint],
|
||||
})
|
||||
.id();
|
||||
let subtree = HashSet::from([root, joint, first_mesh, second_mesh]);
|
||||
let mut diagnostics = AnimationRuntimeDiagnostics::default();
|
||||
let mut query_state =
|
||||
bevy::ecs::system::SystemState::<Query<(Entity, &SkinnedMesh)>>::new(&mut world);
|
||||
|
||||
{
|
||||
let skins = query_state.get(&world).unwrap();
|
||||
validate_skinned_bindings(
|
||||
actor,
|
||||
&subtree,
|
||||
&skins,
|
||||
Some(&inverse_bindposes),
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
assert!(diagnostics.entries().next().is_none());
|
||||
|
||||
world
|
||||
.get_mut::<SkinnedMesh>(second_mesh)
|
||||
.unwrap()
|
||||
.joints
|
||||
.push(joint);
|
||||
{
|
||||
let skins = query_state.get(&world).unwrap();
|
||||
validate_skinned_bindings(
|
||||
actor,
|
||||
&subtree,
|
||||
&skins,
|
||||
Some(&inverse_bindposes),
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
diagnostics.latest().map(|diagnostic| diagnostic.code),
|
||||
Some("animation.skin_binding_count_mismatch")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_state_request_is_rejected_without_message_side_effect() {
|
||||
let mut app = App::new();
|
||||
|
||||
@ -12,9 +12,9 @@ use settings::{
|
||||
AudioSettings, ProjectSettings, AUDIO_BUS_MASTER_ID, AUDIO_GAIN_DB_MAX, AUDIO_GAIN_DB_MIN,
|
||||
};
|
||||
use shared::{
|
||||
asset_server_path, authoring_component_active, ActorId, AudioAttenuationDesc,
|
||||
AudioListenerDesc, AudioRolloff, AudioSourceDesc, AuthoringComponentStates, InspectorOrder,
|
||||
LevelObject, COMPONENT_AUDIO_LISTENER_DESC, COMPONENT_AUDIO_SOURCE_DESC,
|
||||
asset_server_path, inspector_component_active, ActorId, AudioAttenuationDesc,
|
||||
AudioListenerDesc, AudioRolloff, AudioSourceDesc, InspectorOrder, LevelObject,
|
||||
COMPONENT_AUDIO_LISTENER_DESC, COMPONENT_AUDIO_SOURCE_DESC,
|
||||
};
|
||||
use sim::{PlayerCamera, SimEnabled};
|
||||
|
||||
@ -265,13 +265,10 @@ fn reconcile_audio_listener(world: &mut World) {
|
||||
Entity,
|
||||
&AudioListenerDesc,
|
||||
Option<&ActorId>,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
), With<LevelObject>>();
|
||||
for (entity, desc, actor_id, states, legacy_order) in query.iter(world) {
|
||||
if desc.enabled
|
||||
&& authoring_component_active(states, legacy_order, COMPONENT_AUDIO_LISTENER_DESC)
|
||||
{
|
||||
for (entity, desc, actor_id, order) in query.iter(world) {
|
||||
if desc.enabled && inspector_component_active(order, COMPONENT_AUDIO_LISTENER_DESC) {
|
||||
candidates.push((
|
||||
entity,
|
||||
*desc,
|
||||
@ -346,11 +343,10 @@ fn reconcile_audio_sources(world: &mut World) {
|
||||
let mut query = world.query_filtered::<(
|
||||
Entity,
|
||||
&AudioSourceDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
), With<LevelObject>>();
|
||||
for (entity, desc, states, legacy_order) in query.iter(world) {
|
||||
if authoring_component_active(states, legacy_order, COMPONENT_AUDIO_SOURCE_DESC) {
|
||||
for (entity, desc, order) in query.iter(world) {
|
||||
if inspector_component_active(order, COMPONENT_AUDIO_SOURCE_DESC) {
|
||||
sources.push((entity, desc.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ pub mod animation;
|
||||
pub mod audio;
|
||||
pub mod editor_ext;
|
||||
pub mod launch;
|
||||
pub mod navigation;
|
||||
mod scene_bootstrap;
|
||||
mod schema_world_loader;
|
||||
|
||||
@ -94,7 +93,7 @@ mod hot {
|
||||
ActiveCameraRenderProfile, ProjectRenderCamera, ProjectSettings, RenderingCapabilities,
|
||||
SimTuning,
|
||||
};
|
||||
use shared::{AuthoringComponentStates, InspectorOrder, LevelObject, LightDesc, ProjectSun};
|
||||
use shared::{InspectorOrder, LevelObject, LightDesc, ProjectSun};
|
||||
use sim::{
|
||||
CameraSensitivity, Crouching, GameInputEnabled, GameInputFocused, Grounded, JumpState,
|
||||
Player, PlayerCamera, PlayerVelocity, PlayerYawSensitivity,
|
||||
|
||||
@ -2,33 +2,6 @@ use bevy::prelude::*;
|
||||
use game::{launch, GamePlugin};
|
||||
|
||||
fn main() {
|
||||
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
match game::navigation::navigation_validation_argument(&arguments) {
|
||||
Ok(Some(path)) => match game::navigation::validate_navigation_artifact(&path) {
|
||||
Ok(summary) => {
|
||||
println!(
|
||||
"navigation validation passed: {} sample(s), {} link(s), {}",
|
||||
summary.enabled_sample_count,
|
||||
summary.enabled_link_count,
|
||||
summary.artifact_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"navigation validation failed for {}:\n{error}",
|
||||
path.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
App::new()
|
||||
.add_plugins(launch::default_plugins("Bevy FPS Foundation"))
|
||||
.add_plugins(GamePlugin)
|
||||
|
||||
@ -1,294 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
use scene::navigation::{NavigationBakeArtifact, NavigationQueryPath, NavigationQueryRuntime};
|
||||
|
||||
/// Loaded navigation artifact and its shared Polyanya query runtime.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigationRuntime {
|
||||
query: NavigationQueryRuntime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NavigationPath {
|
||||
pub points: Vec<Vec3>,
|
||||
pub length: f32,
|
||||
pub used_link_actor_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl NavigationPath {
|
||||
pub fn used_link_actor_id(&self) -> Option<&str> {
|
||||
self.used_link_actor_ids.first().map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NavigationQueryPath> for NavigationPath {
|
||||
fn from(path: NavigationQueryPath) -> Self {
|
||||
Self {
|
||||
points: path.points.into_iter().map(Vec3::from_array).collect(),
|
||||
length: path.length,
|
||||
used_link_actor_ids: path.used_link_actor_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NavigationRuntime {
|
||||
pub fn from_artifact(artifact: NavigationBakeArtifact) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
query: NavigationQueryRuntime::from_artifact(artifact)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
query: NavigationQueryRuntime::load(path)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn artifact(&self) -> &NavigationBakeArtifact {
|
||||
self.query.artifact()
|
||||
}
|
||||
|
||||
pub fn query(&self, start: Vec3, end: Vec3) -> Result<NavigationPath, String> {
|
||||
self.query
|
||||
.query(start.to_array(), end.to_array())
|
||||
.map(NavigationPath::from)
|
||||
}
|
||||
|
||||
pub fn validate_links(&self) -> Vec<String> {
|
||||
self.query.validate_links()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_navigation_path(
|
||||
artifact_path: &Path,
|
||||
start: Vec3,
|
||||
end: Vec3,
|
||||
) -> Result<NavigationPath, String> {
|
||||
NavigationRuntime::load(artifact_path)?.query(start, end)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NavigationValidationSummary {
|
||||
pub artifact_path: PathBuf,
|
||||
pub enabled_link_count: usize,
|
||||
pub enabled_sample_count: usize,
|
||||
}
|
||||
|
||||
pub fn validate_navigation_artifact(
|
||||
artifact_path: &Path,
|
||||
) -> Result<NavigationValidationSummary, String> {
|
||||
let runtime = NavigationQueryRuntime::load(artifact_path)?;
|
||||
let mut findings = runtime.validate_links();
|
||||
findings.extend(runtime.validate_samples());
|
||||
if !findings.is_empty() {
|
||||
return Err(findings.join("\n"));
|
||||
}
|
||||
Ok(NavigationValidationSummary {
|
||||
artifact_path: artifact_path.to_path_buf(),
|
||||
enabled_link_count: runtime
|
||||
.artifact()
|
||||
.links
|
||||
.iter()
|
||||
.filter(|link| link.enabled)
|
||||
.count(),
|
||||
enabled_sample_count: runtime
|
||||
.artifact()
|
||||
.samples
|
||||
.iter()
|
||||
.filter(|sample| sample.enabled)
|
||||
.count(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn navigation_validation_argument(args: &[String]) -> Result<Option<PathBuf>, String> {
|
||||
let Some(index) = args
|
||||
.iter()
|
||||
.position(|argument| argument == "--validate-navigation")
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if args.len() != 2 || index != 0 {
|
||||
return Err(
|
||||
"usage: game --validate-navigation <project-relative-or-absolute-artifact-path>".into(),
|
||||
);
|
||||
}
|
||||
let path = args[1].trim();
|
||||
if path.is_empty() {
|
||||
return Err("navigation validation artifact path must not be empty".into());
|
||||
}
|
||||
Ok(Some(PathBuf::from(path)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use scene::navigation::{
|
||||
bake_navigation, NavigationBakeInput, NavigationGeometryInput, NavigationLinkInput,
|
||||
NavigationVolumeInput,
|
||||
};
|
||||
use shared::NavigationAgentProfile;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn floor_geometry(
|
||||
actor_id: &str,
|
||||
min_x: f32,
|
||||
max_x: f32,
|
||||
min_z: f32,
|
||||
max_z: f32,
|
||||
y: f32,
|
||||
) -> NavigationGeometryInput {
|
||||
NavigationGeometryInput {
|
||||
actor_id: actor_id.into(),
|
||||
triangles: vec![
|
||||
[[min_x, y, min_z], [max_x, y, max_z], [max_x, y, min_z]],
|
||||
[[min_x, y, min_z], [min_x, y, max_z], [max_x, y, max_z]],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn input() -> NavigationBakeInput {
|
||||
NavigationBakeInput {
|
||||
source_scene: "assets/levels/navigation_showcase.scn.ron".into(),
|
||||
bounds_actor_id: "bounds".into(),
|
||||
center: [0.0, 0.0, 0.0],
|
||||
half_extents: [6.0, 2.0, 6.0],
|
||||
agent: NavigationAgentProfile {
|
||||
min_region_size: 1,
|
||||
merge_region_size: 2,
|
||||
..Default::default()
|
||||
},
|
||||
geometry: vec![floor_geometry("floor", -6.0, 6.0, -6.0, 6.0, 0.0)],
|
||||
obstacles: vec![NavigationVolumeInput {
|
||||
actor_id: "wall".into(),
|
||||
center: [0.0, 0.5, 0.0],
|
||||
half_extents: [0.5, 1.0, 4.5],
|
||||
}],
|
||||
areas: Vec::new(),
|
||||
links: Vec::new(),
|
||||
samples: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_query_routes_on_the_baked_mesh() {
|
||||
let runtime = NavigationRuntime::from_artifact(bake_navigation(&input()).unwrap()).unwrap();
|
||||
let path = runtime
|
||||
.query(Vec3::new(-4.0, 0.0, -5.0), Vec3::new(4.0, 0.0, -5.0))
|
||||
.unwrap();
|
||||
assert!(path.length >= 8.0);
|
||||
assert!(path.points.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_link_can_connect_isolated_regions() {
|
||||
let mut input = input();
|
||||
input.obstacles[0].half_extents[2] = 6.0;
|
||||
input.links.push(NavigationLinkInput {
|
||||
actor_id: "door-link".into(),
|
||||
start: [-1.0, 0.0, 0.0],
|
||||
end: [1.0, 0.0, 0.0],
|
||||
bidirectional: true,
|
||||
cost: 1.0,
|
||||
enabled: true,
|
||||
});
|
||||
let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
|
||||
let path = runtime
|
||||
.query(Vec3::new(-4.0, 0.0, 0.0), Vec3::new(4.0, 0.0, 0.0))
|
||||
.unwrap();
|
||||
assert_eq!(path.used_link_actor_id(), Some("door-link"));
|
||||
assert_eq!(path.used_link_actor_ids, ["door-link"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_can_traverse_multiple_explicit_links() {
|
||||
let mut input = input();
|
||||
input.geometry = vec![
|
||||
floor_geometry("island-a", -6.0, -3.0, -2.0, 2.0, 0.0),
|
||||
floor_geometry("island-b", -1.5, 1.5, -2.0, 2.0, 0.0),
|
||||
floor_geometry("island-c", 3.0, 6.0, -2.0, 2.0, 0.0),
|
||||
];
|
||||
input.obstacles.clear();
|
||||
input.links = vec![
|
||||
NavigationLinkInput {
|
||||
actor_id: "link-a-b".into(),
|
||||
start: [-3.6, 0.0, 0.0],
|
||||
end: [-1.0, 0.0, 0.0],
|
||||
bidirectional: true,
|
||||
cost: 1.0,
|
||||
enabled: true,
|
||||
},
|
||||
NavigationLinkInput {
|
||||
actor_id: "link-b-c".into(),
|
||||
start: [1.0, 0.0, 0.0],
|
||||
end: [3.6, 0.0, 0.0],
|
||||
bidirectional: true,
|
||||
cost: 1.0,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
|
||||
let path = runtime
|
||||
.query(Vec3::new(-5.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 0.0))
|
||||
.unwrap();
|
||||
assert_eq!(path.used_link_actor_ids, ["link-a-b", "link-b-c"]);
|
||||
assert!(path.length >= 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_selects_vertically_nearest_walkable_surface() {
|
||||
let mut input = input();
|
||||
input.center = [0.0, 2.0, 0.0];
|
||||
input.half_extents = [6.0, 3.0, 6.0];
|
||||
input.geometry = vec![
|
||||
floor_geometry("lower-floor", -5.0, 5.0, -5.0, 5.0, 0.0),
|
||||
floor_geometry("upper-floor", -5.0, 5.0, -5.0, 5.0, 4.0),
|
||||
];
|
||||
input.obstacles.clear();
|
||||
let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
|
||||
|
||||
let lower = runtime
|
||||
.query(Vec3::new(-3.0, 0.1, 0.0), Vec3::new(3.0, 0.1, 0.0))
|
||||
.unwrap();
|
||||
let upper = runtime
|
||||
.query(Vec3::new(-3.0, 3.9, 0.0), Vec3::new(3.0, 3.9, 0.0))
|
||||
.unwrap();
|
||||
assert!(lower.points.iter().all(|point| point.y < 1.0));
|
||||
assert!(upper.points.iter().all(|point| point.y > 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_link_endpoint_is_reported_with_actor_identity() {
|
||||
let mut input = input();
|
||||
input.links.push(NavigationLinkInput {
|
||||
actor_id: "bad-link".into(),
|
||||
start: [100.0, 0.0, 100.0],
|
||||
end: [0.0, 0.0, 0.0],
|
||||
bidirectional: false,
|
||||
cost: 1.0,
|
||||
enabled: true,
|
||||
});
|
||||
let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
|
||||
assert!(runtime
|
||||
.validate_links()
|
||||
.iter()
|
||||
.any(|finding| finding.contains("bad-link start")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_cli_argument_is_strict_and_window_free() {
|
||||
assert_eq!(
|
||||
navigation_validation_argument(&[
|
||||
"--validate-navigation".into(),
|
||||
"assets/navigation/generated/main.nav.ron".into(),
|
||||
])
|
||||
.unwrap(),
|
||||
Some(PathBuf::from("assets/navigation/generated/main.nav.ron"))
|
||||
);
|
||||
assert!(navigation_validation_argument(&["--validate-navigation".into()]).is_err());
|
||||
assert_eq!(
|
||||
navigation_validation_argument(&["--project".into(), ".".into()]).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,6 @@ avian3d.workspace = true
|
||||
bevy.workspace = true
|
||||
bevy_core_pipeline.workspace = true
|
||||
bevy_solari.workspace = true
|
||||
blacksite_surface.workspace = true
|
||||
protocol.workspace = true
|
||||
shared.workspace = true
|
||||
sim.workspace = true
|
||||
|
||||
@ -5,7 +5,7 @@ use bevy::core_pipeline::prepass::{
|
||||
DeferredPrepass, DeferredPrepassDoubleBuffer, DepthPrepass, DepthPrepassDoubleBuffer,
|
||||
MotionVectorPrepass,
|
||||
};
|
||||
use bevy::mesh::{morph::MeshMorphWeights, skinning::SkinnedMesh, Indices, PrimitiveTopology};
|
||||
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||
use bevy::pbr::{DefaultOpaqueRendererMethod, ExtractedDirectionalLight};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_resource::TextureUsages;
|
||||
@ -14,7 +14,6 @@ use bevy::render::{Render, RenderApp, RenderSystems};
|
||||
use bevy_solari::prelude::{RaytracingMesh3d, SolariLighting};
|
||||
use bevy_solari::scene::RaytracingSceneBindings;
|
||||
use bevy_solari::SolariPlugins;
|
||||
use blacksite_surface::{SurfaceEvaluatorRegistry, SurfaceMaterial};
|
||||
use settings::{GiPath, ProjectSettings, RenderingCapabilities};
|
||||
use shared::{LevelObject, RaytracingExcluded};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@ -25,10 +24,6 @@ pub struct SolariRaytracingSceneStats {
|
||||
pub tagged_meshes: usize,
|
||||
pub excluded_meshes: usize,
|
||||
pub unsupported_meshes: usize,
|
||||
/// Skinned or morphed meshes deliberately omitted until Solari consumes deformed vertices.
|
||||
pub deformed_meshes_excluded: usize,
|
||||
pub surface_material_meshes: usize,
|
||||
pub surface_materials_missing_evaluator: usize,
|
||||
pub missing_level_root_meshes: usize,
|
||||
pub emissive_meshes: usize,
|
||||
pub compatible_mesh_assets: usize,
|
||||
@ -66,8 +61,7 @@ pub struct SolariRenderingPlugin;
|
||||
|
||||
impl Plugin for SolariRenderingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(blacksite_surface::SurfaceMaterialPlugin)
|
||||
.add_plugins(SolariPlugins)
|
||||
app.add_plugins(SolariPlugins)
|
||||
.init_resource::<RenderingCapabilities>()
|
||||
.init_resource::<settings::EffectiveRenderStack>()
|
||||
.init_resource::<SolariRaytracingSceneStats>()
|
||||
@ -219,16 +213,11 @@ fn sync_hydrated_raytracing_meshes(
|
||||
mut commands: Commands,
|
||||
mut stats: ResMut<SolariRaytracingSceneStats>,
|
||||
materials: Res<Assets<StandardMaterial>>,
|
||||
surface_materials: Res<Assets<SurfaceMaterial>>,
|
||||
surface_evaluators: Res<SurfaceEvaluatorRegistry>,
|
||||
mesh_assets: Res<Assets<Mesh>>,
|
||||
meshes: Query<(
|
||||
Entity,
|
||||
&Mesh3d,
|
||||
Option<&MeshMaterial3d<StandardMaterial>>,
|
||||
Option<&MeshMaterial3d<SurfaceMaterial>>,
|
||||
Option<&SkinnedMesh>,
|
||||
Option<&MeshMorphWeights>,
|
||||
Option<&RaytracingMesh3d>,
|
||||
Option<&RaytracingExcluded>,
|
||||
)>,
|
||||
@ -241,9 +230,6 @@ fn sync_hydrated_raytracing_meshes(
|
||||
next_stats.tagged_meshes = 0;
|
||||
next_stats.excluded_meshes = 0;
|
||||
next_stats.unsupported_meshes = 0;
|
||||
next_stats.deformed_meshes_excluded = 0;
|
||||
next_stats.surface_material_meshes = 0;
|
||||
next_stats.surface_materials_missing_evaluator = 0;
|
||||
next_stats.missing_level_root_meshes = 0;
|
||||
next_stats.emissive_meshes = 0;
|
||||
next_stats.compatible_mesh_assets = 0;
|
||||
@ -269,9 +255,7 @@ fn sync_hydrated_raytracing_meshes(
|
||||
return;
|
||||
}
|
||||
|
||||
for (entity, mesh, standard_material, surface_material, skin, morph, rt_mesh, excluded) in
|
||||
&meshes
|
||||
{
|
||||
for (entity, mesh, material, rt_mesh, excluded) in &meshes {
|
||||
if excluded.is_some() {
|
||||
next_stats.excluded_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
@ -287,79 +271,19 @@ fn sync_hydrated_raytracing_meshes(
|
||||
|
||||
next_stats.project_meshes += 1;
|
||||
|
||||
// Bevy 0.19's mesh allocator exposes the source vertex slab to Solari, not the
|
||||
// morph-then-skin output consumed by raster rendering. Tagging this mesh would trace the
|
||||
// bind pose, so omit it until the fork owns an instance-keyed deformed vertex/BLAS path.
|
||||
let mesh_has_morph_targets = mesh_assets
|
||||
.get(&mesh.0)
|
||||
.and_then(Mesh::get_morph_targets)
|
||||
.is_some();
|
||||
if !solari_deformation_is_supported(skin.is_some(), morph.is_some(), mesh_has_morph_targets)
|
||||
{
|
||||
next_stats.deformed_meshes_excluded += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if standard_material.is_none() && surface_material.is_none() {
|
||||
let Some(material) = material else {
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(surface_handle) = surface_material {
|
||||
next_stats.surface_material_meshes += 1;
|
||||
let Some(material) = surface_materials.get(&surface_handle.0) else {
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if !solari_alpha_mode_is_supported(material.base.alpha_mode) {
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !surface_evaluators
|
||||
.evaluators
|
||||
.contains_key(&material.extension.uniform.shader_id)
|
||||
{
|
||||
next_stats.surface_materials_missing_evaluator += 1;
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A custom evaluator may emit light as a function of parameters, textures, or world
|
||||
// position. Conservatively register Surface instances as potential emissive sources;
|
||||
// zero-emission samples contribute no energy.
|
||||
if materials
|
||||
.get(&material.0)
|
||||
.is_some_and(|material| material.emissive.to_vec3() != Vec3::ZERO)
|
||||
{
|
||||
next_stats.emissive_meshes += 1;
|
||||
} else if let Some(standard_handle) = standard_material {
|
||||
let Some(material) = materials.get(&standard_handle.0) else {
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if !solari_alpha_mode_is_supported(material.alpha_mode) {
|
||||
next_stats.unsupported_meshes += 1;
|
||||
if rt_mesh.is_some() {
|
||||
commands.entity(entity).remove::<RaytracingMesh3d>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if material.emissive.to_vec3() != Vec3::ZERO {
|
||||
next_stats.emissive_meshes += 1;
|
||||
}
|
||||
}
|
||||
|
||||
match mesh_assets
|
||||
@ -399,7 +323,7 @@ fn sync_hydrated_raytracing_meshes(
|
||||
|
||||
if *stats != next_stats {
|
||||
info!(
|
||||
"Solari mesh eligibility: requested={:?} eligible={} tagged={} compatible_assets={} incompatible_assets={} missing_tangents={} missing_u32_indices={} unsupported={} deformed_excluded={} surface_materials={} surface_missing_evaluator={} excluded={} outside_level_roots={} emissive_candidates={}",
|
||||
"Solari mesh eligibility: requested={:?} eligible={} tagged={} compatible_assets={} incompatible_assets={} missing_tangents={} missing_u32_indices={} unsupported={} excluded={} outside_level_roots={} emissive={}",
|
||||
caps.requested_gi_path,
|
||||
next_stats.project_meshes,
|
||||
next_stats.tagged_meshes,
|
||||
@ -408,9 +332,6 @@ fn sync_hydrated_raytracing_meshes(
|
||||
next_stats.mesh_assets_missing_tangents,
|
||||
next_stats.mesh_assets_missing_u32_indices,
|
||||
next_stats.unsupported_meshes,
|
||||
next_stats.deformed_meshes_excluded,
|
||||
next_stats.surface_material_meshes,
|
||||
next_stats.surface_materials_missing_evaluator,
|
||||
next_stats.excluded_meshes,
|
||||
next_stats.missing_level_root_meshes,
|
||||
next_stats.emissive_meshes,
|
||||
@ -419,21 +340,6 @@ fn sync_hydrated_raytracing_meshes(
|
||||
}
|
||||
}
|
||||
|
||||
fn solari_deformation_is_supported(
|
||||
has_skin: bool,
|
||||
has_morph_weights: bool,
|
||||
mesh_has_morph_targets: bool,
|
||||
) -> bool {
|
||||
!has_skin && !has_morph_weights && !mesh_has_morph_targets
|
||||
}
|
||||
|
||||
fn solari_alpha_mode_is_supported(alpha_mode: AlphaMode) -> bool {
|
||||
matches!(
|
||||
alpha_mode,
|
||||
AlphaMode::Opaque | AlphaMode::Mask(_) | AlphaMode::AlphaToCoverage
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SolariMeshCompatibility {
|
||||
Compatible,
|
||||
@ -474,24 +380,17 @@ fn mesh_is_solari_project_geometry(
|
||||
Entity,
|
||||
&Mesh3d,
|
||||
Option<&MeshMaterial3d<StandardMaterial>>,
|
||||
Option<&MeshMaterial3d<SurfaceMaterial>>,
|
||||
Option<&SkinnedMesh>,
|
||||
Option<&MeshMorphWeights>,
|
||||
Option<&RaytracingMesh3d>,
|
||||
Option<&RaytracingExcluded>,
|
||||
)>,
|
||||
parents: &Query<&ChildOf>,
|
||||
level_roots: &Query<(), With<LevelObject>>,
|
||||
) -> bool {
|
||||
let Ok((_, _, standard_material, surface_material, skin, morph, _, excluded)) =
|
||||
meshes.get(entity)
|
||||
else {
|
||||
let Ok((_, _, material, _, excluded)) = meshes.get(entity) else {
|
||||
return false;
|
||||
};
|
||||
excluded.is_none()
|
||||
&& skin.is_none()
|
||||
&& morph.is_none()
|
||||
&& (standard_material.is_some() || surface_material.is_some())
|
||||
&& material.is_some()
|
||||
&& has_level_object_ancestor(entity, parents, level_roots)
|
||||
}
|
||||
|
||||
@ -670,22 +569,4 @@ mod tests {
|
||||
GiPath::SolariDeferred
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deformation_is_excluded_instead_of_tracing_the_bind_pose() {
|
||||
assert!(solari_deformation_is_supported(false, false, false));
|
||||
assert!(!solari_deformation_is_supported(true, false, false));
|
||||
assert!(!solari_deformation_is_supported(false, true, false));
|
||||
assert!(!solari_deformation_is_supported(false, false, true));
|
||||
assert!(!solari_deformation_is_supported(true, true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solari_accepts_only_opaque_and_cutout_materials() {
|
||||
assert!(solari_alpha_mode_is_supported(AlphaMode::Opaque));
|
||||
assert!(solari_alpha_mode_is_supported(AlphaMode::Mask(0.5)));
|
||||
assert!(solari_alpha_mode_is_supported(AlphaMode::AlphaToCoverage));
|
||||
assert!(!solari_alpha_mode_is_supported(AlphaMode::Blend));
|
||||
assert!(!solari_alpha_mode_is_supported(AlphaMode::Premultiplied));
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,8 @@ use settings::{
|
||||
ProjectSettings, RenderingCapabilities, RenderingSettings, VolumeContribution,
|
||||
};
|
||||
use shared::{
|
||||
authoring_component_active, AuthoringComponentStates, InspectorOrder, LevelObject,
|
||||
PostProcessVolumeDesc, PostProcessVolumeOverrides, COMPONENT_POST_PROCESS_VOLUME,
|
||||
inspector_component_active, InspectorOrder, LevelObject, PostProcessVolumeDesc,
|
||||
PostProcessVolumeOverrides, COMPONENT_POST_PROCESS_VOLUME,
|
||||
};
|
||||
|
||||
use super::solari::SolariRaytracingSceneStats;
|
||||
@ -264,7 +264,6 @@ pub fn sync_active_camera_render_profile(
|
||||
Entity,
|
||||
&PostProcessVolumeDesc,
|
||||
&GlobalTransform,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
@ -306,10 +305,10 @@ pub fn sync_active_camera_render_profile(
|
||||
|
||||
let volume_list: Vec<_> = volumes
|
||||
.iter()
|
||||
.filter(|(_, _, _, states, legacy_order)| {
|
||||
authoring_component_active(*states, *legacy_order, COMPONENT_POST_PROCESS_VOLUME)
|
||||
.filter(|(_, _, _, order)| {
|
||||
inspector_component_active(*order, COMPONENT_POST_PROCESS_VOLUME)
|
||||
})
|
||||
.map(|(entity, desc, transform, _, _)| (entity, desc, transform))
|
||||
.map(|(entity, desc, transform, _)| (entity, desc, transform))
|
||||
.collect();
|
||||
let (resolved, entries) = resolve_active_camera_render_profile(
|
||||
camera_tf.translation(),
|
||||
|
||||
@ -4,8 +4,8 @@ use bevy::light::CascadeShadowConfig;
|
||||
use bevy::prelude::*;
|
||||
use settings::ProjectSettings;
|
||||
use shared::{
|
||||
authoring_component_active, cascade_config_from_rendering, AuthoringComponentStates,
|
||||
AuthoringLightKind, InspectorOrder, LevelObject, LightDesc, ProjectSun, COMPONENT_LIGHT_DESC,
|
||||
cascade_config_from_rendering, inspector_component_active, AuthoringLightKind, InspectorOrder,
|
||||
LevelObject, LightDesc, ProjectSun, COMPONENT_LIGHT_DESC,
|
||||
};
|
||||
|
||||
/// Spawns the directional "sun" from project rendering settings.
|
||||
@ -14,14 +14,7 @@ pub fn spawn_sun(
|
||||
mut commands: Commands,
|
||||
settings: Res<ProjectSettings>,
|
||||
existing: Query<(), With<DirectionalLight>>,
|
||||
scene_suns: Query<
|
||||
(
|
||||
&LightDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
>,
|
||||
scene_suns: Query<(&LightDesc, Option<&InspectorOrder>), With<LevelObject>>,
|
||||
) {
|
||||
if !existing.is_empty() {
|
||||
return;
|
||||
@ -46,14 +39,7 @@ pub fn spawn_sun(
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub fn sync_project_sun_visibility(
|
||||
scene_suns: Query<
|
||||
(
|
||||
&LightDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
>,
|
||||
scene_suns: Query<(&LightDesc, Option<&InspectorOrder>), With<LevelObject>>,
|
||||
mut project_suns: Query<(&mut DirectionalLight, &mut Visibility), With<ProjectSun>>,
|
||||
) {
|
||||
let scene_override = has_scene_sun_override(&scene_suns);
|
||||
@ -68,17 +54,10 @@ pub fn sync_project_sun_visibility(
|
||||
}
|
||||
|
||||
fn has_scene_sun_override(
|
||||
scene_suns: &Query<
|
||||
(
|
||||
&LightDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
>,
|
||||
scene_suns: &Query<(&LightDesc, Option<&InspectorOrder>), With<LevelObject>>,
|
||||
) -> bool {
|
||||
scene_suns.iter().any(|(light, states, legacy_order)| {
|
||||
authoring_component_active(states, legacy_order, COMPONENT_LIGHT_DESC)
|
||||
scene_suns.iter().any(|(light, order)| {
|
||||
inspector_component_active(order, COMPONENT_LIGHT_DESC)
|
||||
&& matches!(light.kind, AuthoringLightKind::Directional)
|
||||
})
|
||||
}
|
||||
@ -120,14 +99,7 @@ pub fn sync_scene_directional_shadow_cascades(
|
||||
#[unsafe(no_mangle)]
|
||||
pub fn sync_project_sun_from_settings(
|
||||
settings: Res<ProjectSettings>,
|
||||
scene_suns: Query<
|
||||
(
|
||||
&LightDesc,
|
||||
Option<&AuthoringComponentStates>,
|
||||
Option<&InspectorOrder>,
|
||||
),
|
||||
With<LevelObject>,
|
||||
>,
|
||||
scene_suns: Query<(&LightDesc, Option<&InspectorOrder>), With<LevelObject>>,
|
||||
mut project_suns: Query<(&mut DirectionalLight, &mut Visibility), With<ProjectSun>>,
|
||||
) {
|
||||
let scene_override = has_scene_sun_override(&scene_suns);
|
||||
|
||||
@ -10,8 +10,5 @@ blake3 = "1"
|
||||
serde = { workspace = true }
|
||||
ron = "0.8"
|
||||
ron-edit = "=0.2.0"
|
||||
nav_glam.workspace = true
|
||||
polyanya.workspace = true
|
||||
rerecast.workspace = true
|
||||
shared.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@ -743,7 +743,7 @@ mod tests {
|
||||
let body = "(resources: {}, entities: {})";
|
||||
let document = SceneDocument::from_ron_text(body).unwrap();
|
||||
let text = document.to_ron_text().unwrap();
|
||||
assert!(text.starts_with("(schema_version: 4,"));
|
||||
assert!(text.starts_with("(schema_version: 2,"));
|
||||
assert!(SceneDocument::from_ron_text(&text)
|
||||
.unwrap()
|
||||
.entities
|
||||
|
||||
@ -6,19 +6,15 @@
|
||||
mod composition;
|
||||
pub mod document;
|
||||
mod migrate;
|
||||
pub mod navigation;
|
||||
mod prefab;
|
||||
mod project_validation;
|
||||
mod upgrade;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use composition::{validate_composition_graph, validate_scene_composition};
|
||||
pub use migrate::{
|
||||
migrate_v1_to_v2, migrate_v2_to_v3, migrate_v3_to_v4, DEFAULT_SUN_ILLUMINANCE_LUX,
|
||||
};
|
||||
pub use migrate::{migrate_v1_to_v2, DEFAULT_SUN_ILLUMINANCE_LUX};
|
||||
pub use prefab::{
|
||||
prefab_graph_revision, validate_prefab_graph, validate_prefab_graph_text,
|
||||
validate_prefab_references, validate_prefab_references_text,
|
||||
@ -27,9 +23,8 @@ pub use project_validation::{
|
||||
validate_project, ProjectDependency, ProjectValidationFinding, ProjectValidationReport,
|
||||
ValidationSeverity,
|
||||
};
|
||||
pub use upgrade::{upgrade_project, ProjectUpgradeChange, ProjectUpgradeReport};
|
||||
|
||||
pub const CURRENT_SCENE_SCHEMA_VERSION: u32 = 4;
|
||||
pub const CURRENT_SCENE_SCHEMA_VERSION: u32 = 2;
|
||||
|
||||
/// Whether an asset-relative path names a tree excluded from runtime packages.
|
||||
pub fn is_runtime_package_excluded_tree(relative: &Path) -> bool {
|
||||
@ -134,10 +129,8 @@ pub fn migrate_scene_text(text: &str) -> Result<String, String> {
|
||||
};
|
||||
|
||||
let migrated_body = match version {
|
||||
0 | 1 => migrate_v3_to_v4(&migrate_v2_to_v3(&migrate_v1_to_v2(&body))),
|
||||
2 => migrate_v3_to_v4(&migrate_v2_to_v3(&body)),
|
||||
3 => migrate_v3_to_v4(&body),
|
||||
4 => body,
|
||||
0 | 1 => migrate_v1_to_v2(&body),
|
||||
2 => body,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"scene schema version {version} is newer than supported version {CURRENT_SCENE_SCHEMA_VERSION}"
|
||||
@ -227,16 +220,16 @@ mod tests {
|
||||
fn stamp_and_strip_round_trip() {
|
||||
let body = "(resources: {}, entities: {})";
|
||||
let stamped = stamp_schema_version(body).unwrap();
|
||||
assert!(stamped.starts_with("(schema_version: 4,"));
|
||||
assert!(stamped.starts_with("(schema_version: 2,"));
|
||||
let stripped = strip_schema_version(&stamped).unwrap();
|
||||
assert_eq!(stripped, body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_scene_migrates_to_current_schema() {
|
||||
fn legacy_scene_migrates_to_v2() {
|
||||
let legacy = "(resources: {}, entities: {4294967133: ()})";
|
||||
let migrated = migrate_scene_text(legacy).unwrap();
|
||||
assert!(migrated.contains("schema_version: 4"));
|
||||
assert!(migrated.contains("schema_version: 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -12,57 +12,6 @@ pub fn migrate_v1_to_v2(body: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Migrates v2 animated imported-model actors to the dedicated skinned renderer contract.
|
||||
///
|
||||
/// `ModelRef` and `SkinnedMeshRenderer` intentionally share the same serialized source fields, so
|
||||
/// the migration can preserve stable asset IDs and cached paths without lossy reconstruction.
|
||||
pub fn migrate_v2_to_v3(body: &str) -> String {
|
||||
let mut result = String::with_capacity(body.len() + 256);
|
||||
let mut rest = body;
|
||||
while let Some(index) = rest.find("components: {") {
|
||||
result.push_str(&rest[..index]);
|
||||
let after = &rest[index..];
|
||||
let Some(close) = find_components_close(after) else {
|
||||
result.push_str(after);
|
||||
return result;
|
||||
};
|
||||
let block = &after[..close];
|
||||
if block.contains("\"shared::animation::AnimationControllerDesc\"")
|
||||
&& block.contains("\"shared::components::ModelRef\"")
|
||||
{
|
||||
result.push_str(
|
||||
&block
|
||||
.replace(
|
||||
"\"shared::components::ModelRef\"",
|
||||
"\"shared::animation::SkinnedMeshRenderer\"",
|
||||
)
|
||||
.replace(
|
||||
"\"shared::components::ActorKind\": ImportedModel",
|
||||
"\"shared::components::ActorKind\": SkinnedMesh",
|
||||
)
|
||||
.replace(
|
||||
"\"shared::components::ActorKind\":ImportedModel",
|
||||
"\"shared::components::ActorKind\":SkinnedMesh",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
result.push_str(block);
|
||||
}
|
||||
rest = &after[close..];
|
||||
}
|
||||
result.push_str(rest);
|
||||
result
|
||||
}
|
||||
|
||||
/// Migrates schema-v3 documents into the registry-driven renderer/component foundation.
|
||||
///
|
||||
/// The v4 authoring types deliberately use serde defaults for material-slot sets and component
|
||||
/// enable state, so normal loading can remain read-only. The explicit project upgrader later
|
||||
/// materializes shared Material assets and canonical slot IDs on disk.
|
||||
pub fn migrate_v3_to_v4(body: &str) -> String {
|
||||
body.to_string()
|
||||
}
|
||||
|
||||
fn backfill_actor_kinds(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len() + 1024);
|
||||
let mut rest = text;
|
||||
@ -140,9 +89,6 @@ fn infer_kind_from_block(block: &str) -> &'static str {
|
||||
if block.contains("\"shared::components::LightDesc\"") {
|
||||
return actor_kind_ron(ActorKind::Light);
|
||||
}
|
||||
if block.contains("\"shared::animation::SkinnedMeshRenderer\"") {
|
||||
return actor_kind_ron(ActorKind::SkinnedMesh);
|
||||
}
|
||||
if block.contains("\"shared::components::ModelRef\"") {
|
||||
return actor_kind_ron(ActorKind::ImportedModel);
|
||||
}
|
||||
@ -165,9 +111,6 @@ fn infer_kind_from_block(block: &str) -> &'static str {
|
||||
if block.contains("\"shared::components::AudioListenerDesc\"") {
|
||||
return actor_kind_ron(ActorKind::AudioListener);
|
||||
}
|
||||
if block.contains("\"shared::navigation::Navigation") {
|
||||
return actor_kind_ron(ActorKind::Navigation);
|
||||
}
|
||||
actor_kind_ron(ActorKind::Empty)
|
||||
}
|
||||
|
||||
@ -176,7 +119,6 @@ fn actor_kind_ron(kind: ActorKind) -> &'static str {
|
||||
ActorKind::Empty => "Empty",
|
||||
ActorKind::Brush => "Brush",
|
||||
ActorKind::StaticMesh => "StaticMesh",
|
||||
ActorKind::SkinnedMesh => "SkinnedMesh",
|
||||
ActorKind::ImportedModel => "ImportedModel",
|
||||
ActorKind::Light => "Light",
|
||||
ActorKind::PrefabAnchor => "PrefabAnchor",
|
||||
@ -188,7 +130,6 @@ fn actor_kind_ron(kind: ActorKind) -> &'static str {
|
||||
ActorKind::Objective => "Objective",
|
||||
ActorKind::AudioSource => "AudioSource",
|
||||
ActorKind::AudioListener => "AudioListener",
|
||||
ActorKind::Navigation => "Navigation",
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,35 +241,6 @@ mod tests {
|
||||
assert!(migrated.contains("Brush"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_animation_actor_moves_to_dedicated_skinned_renderer() {
|
||||
let body = r#"(
|
||||
resources: {},
|
||||
entities: {
|
||||
1: (components: {
|
||||
"shared::components::LevelObject": (),
|
||||
"shared::components::ActorKind": ImportedModel,
|
||||
"shared::components::ModelRef": (asset_id: "hero", path: "models/hero.glb", scene_index: 0),
|
||||
"shared::animation::AnimationControllerDesc": (skeleton: None, states: [], default_state: "", default_crossfade_seconds: 0.2),
|
||||
}),
|
||||
2: (components: {
|
||||
"shared::components::ActorKind": ImportedModel,
|
||||
"shared::components::ModelRef": (asset_id: "prop", path: "models/prop.glb", scene_index: 0),
|
||||
}),
|
||||
},
|
||||
)"#;
|
||||
|
||||
let migrated = migrate_v2_to_v3(body);
|
||||
|
||||
assert!(migrated.contains("\"shared::animation::SkinnedMeshRenderer\""));
|
||||
assert!(migrated.contains("\"shared::components::ActorKind\": SkinnedMesh"));
|
||||
assert_eq!(
|
||||
migrated.matches("\"shared::components::ModelRef\"").count(),
|
||||
1,
|
||||
"generic scene instance must remain a ModelRef"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backfills_actor_kind_for_audio_source_entity() {
|
||||
let body = r#"(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,450 +0,0 @@
|
||||
//! Explicit, transactional project content upgrade support.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::Serialize;
|
||||
use shared::{
|
||||
AuthoringComponentStates, ComponentInstanceId, InspectorOrder, MaterialAsset,
|
||||
MaterialInstanceAsset, MaterialRef, RendererMaterialSlot, ShaderSchemaAsset,
|
||||
StaticMeshRenderer, COMPONENT_STATIC_MESH_RENDERER,
|
||||
};
|
||||
|
||||
use crate::document::{SceneComponentBlob, SceneDocument};
|
||||
|
||||
const INSPECTOR_ORDER_COMPONENT: &str = "shared::components::InspectorOrder";
|
||||
const AUTHORING_COMPONENT_STATES_COMPONENT: &str = "shared::components::AuthoringComponentStates";
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ProjectUpgradeChange {
|
||||
pub path: String,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ProjectUpgradeReport {
|
||||
pub applied: bool,
|
||||
pub backup_path: Option<String>,
|
||||
pub changes: Vec<ProjectUpgradeChange>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
struct PendingWrite {
|
||||
path: PathBuf,
|
||||
contents: String,
|
||||
kind: &'static str,
|
||||
}
|
||||
|
||||
pub fn upgrade_project(root: &Path, apply: bool) -> Result<ProjectUpgradeReport, String> {
|
||||
let assets = root.join("assets");
|
||||
let mut files = Vec::new();
|
||||
collect_files(&assets, &mut files)?;
|
||||
files.sort();
|
||||
|
||||
let mut pending = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
for path in files {
|
||||
if path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name.starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = path.strip_prefix(root).unwrap_or(&path);
|
||||
let normalized = relative.to_string_lossy().replace('\\', "/");
|
||||
let Some(original) = fs::read_to_string(&path).ok() else {
|
||||
continue;
|
||||
};
|
||||
let upgraded = if normalized.ends_with(".scn.ron") || normalized.ends_with(".prefab.ron") {
|
||||
match canonical_scene_document(&original) {
|
||||
Ok(value) => Some((value, "scene-schema")),
|
||||
Err(error) => {
|
||||
warnings.push(format!("{normalized}: {error}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if normalized.contains("/materials/") && normalized.ends_with(".ron") {
|
||||
canonical_material_document(&path, &original)
|
||||
} else if normalized.contains("/shaders/") && normalized.ends_with(".shader.ron") {
|
||||
canonical_shader_document(&original)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some((contents, kind)) = upgraded else {
|
||||
continue;
|
||||
};
|
||||
if normalize_text(&contents) != normalize_text(&original) {
|
||||
pending.push(PendingWrite {
|
||||
path,
|
||||
contents,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let changes = pending
|
||||
.iter()
|
||||
.map(|write| ProjectUpgradeChange {
|
||||
path: write
|
||||
.path
|
||||
.strip_prefix(root)
|
||||
.unwrap_or(&write.path)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"),
|
||||
kind: write.kind.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !apply || pending.is_empty() {
|
||||
return Ok(ProjectUpgradeReport {
|
||||
applied: false,
|
||||
backup_path: None,
|
||||
changes,
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| error.to_string())?
|
||||
.as_secs();
|
||||
let backup_root = root
|
||||
.join(".blacksite")
|
||||
.join("backups")
|
||||
.join(format!("material-component-v4-{timestamp}"));
|
||||
for write in &pending {
|
||||
let relative = write
|
||||
.path
|
||||
.strip_prefix(root)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let backup = backup_root.join(relative);
|
||||
if let Some(parent) = backup.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||
}
|
||||
fs::copy(&write.path, &backup).map_err(|error| {
|
||||
format!(
|
||||
"could not back up {} to {}: {error}",
|
||||
write.path.display(),
|
||||
backup.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut staged = Vec::new();
|
||||
for (index, write) in pending.iter().enumerate() {
|
||||
let temporary = write
|
||||
.path
|
||||
.with_extension(format!("blacksite-upgrade-{index}.tmp"));
|
||||
fs::write(&temporary, &write.contents)
|
||||
.map_err(|error| format!("could not stage {}: {error}", write.path.display()))?;
|
||||
staged.push((temporary, write.path.clone()));
|
||||
}
|
||||
for (temporary, destination) in &staged {
|
||||
if let Err(error) = fs::rename(temporary, destination) {
|
||||
for change in &changes {
|
||||
let backup = backup_root.join(&change.path);
|
||||
let destination = root.join(&change.path);
|
||||
let _ = fs::copy(backup, destination);
|
||||
}
|
||||
return Err(format!(
|
||||
"project upgrade failed while replacing {} and was rolled back: {error}",
|
||||
destination.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ProjectUpgradeReport {
|
||||
applied: true,
|
||||
backup_path: Some(
|
||||
backup_root
|
||||
.strip_prefix(root)
|
||||
.unwrap_or(&backup_root)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"),
|
||||
),
|
||||
changes,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
fn canonical_scene_document(text: &str) -> Result<String, String> {
|
||||
let mut document = SceneDocument::from_ron_text(text)?;
|
||||
for entity in &mut document.entities {
|
||||
let present_types = entity
|
||||
.components
|
||||
.iter()
|
||||
.filter(|component| shared::authoring_component_id(&component.type_name).is_some())
|
||||
.map(|component| component.type_name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(component) = entity
|
||||
.components
|
||||
.iter_mut()
|
||||
.find(|component| component.type_name == COMPONENT_STATIC_MESH_RENDERER)
|
||||
{
|
||||
let mut renderer: StaticMeshRenderer =
|
||||
ron::from_str(&component.ron).map_err(|error| {
|
||||
format!("could not upgrade StaticMeshRenderer component: {error}")
|
||||
})?;
|
||||
normalize_static_renderer(&mut renderer);
|
||||
*component =
|
||||
SceneComponentBlob::from_serializable(COMPONENT_STATIC_MESH_RENDERER, &renderer)?;
|
||||
}
|
||||
|
||||
let states_index = entity
|
||||
.components
|
||||
.iter()
|
||||
.position(|component| component.type_name == AUTHORING_COMPONENT_STATES_COMPONENT);
|
||||
let mut states = states_index
|
||||
.and_then(|index| {
|
||||
ron::from_str::<AuthoringComponentStates>(&entity.components[index].ron).ok()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
normalize_component_states(&mut states);
|
||||
|
||||
let mut migrated_legacy_state = false;
|
||||
if let Some(order_index) = entity
|
||||
.components
|
||||
.iter()
|
||||
.position(|component| component.type_name == INSPECTOR_ORDER_COMPONENT)
|
||||
{
|
||||
let mut order: InspectorOrder = ron::from_str(&entity.components[order_index].ron)
|
||||
.map_err(|error| format!("could not upgrade InspectorOrder component: {error}"))?;
|
||||
for legacy in std::mem::take(&mut order.component_states) {
|
||||
let key = if legacy.component_id.trim().is_empty() {
|
||||
legacy.type_name
|
||||
} else {
|
||||
legacy.component_id
|
||||
};
|
||||
if !key.trim().is_empty() {
|
||||
states.set_component_active(key, legacy.active);
|
||||
migrated_legacy_state = true;
|
||||
}
|
||||
}
|
||||
let present = present_types.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
order.ensure_component_order(&present);
|
||||
entity.components[order_index] =
|
||||
SceneComponentBlob::from_serializable(INSPECTOR_ORDER_COMPONENT, &order)?;
|
||||
}
|
||||
if states_index.is_some() || migrated_legacy_state {
|
||||
let component = SceneComponentBlob::from_serializable(
|
||||
AUTHORING_COMPONENT_STATES_COMPONENT,
|
||||
&states,
|
||||
)?;
|
||||
if let Some(index) = states_index {
|
||||
entity.components[index] = component;
|
||||
} else {
|
||||
entity.components.push(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.to_ron_text()
|
||||
}
|
||||
|
||||
fn normalize_static_renderer(renderer: &mut StaticMeshRenderer) {
|
||||
for (index, part) in renderer.slots.iter_mut().enumerate() {
|
||||
if part.id.is_empty() {
|
||||
let id = if part.mesh.sub_asset_id.trim().is_empty() {
|
||||
format!("draw:legacy:{index}")
|
||||
} else {
|
||||
part.mesh.sub_asset_id.clone()
|
||||
};
|
||||
part.id = ComponentInstanceId::new(id);
|
||||
}
|
||||
if part.material_slot_id.is_empty() {
|
||||
part.material_slot_id = ComponentInstanceId::new(format!("slot:{}", part.id.0));
|
||||
}
|
||||
let legacy_material = part.material.take().map(MaterialRef::new);
|
||||
if let Some(slot) = renderer.materials.slot_mut(&part.material_slot_id) {
|
||||
if slot.source_material.is_none() {
|
||||
slot.source_material = legacy_material;
|
||||
}
|
||||
if slot.name.trim().is_empty() {
|
||||
slot.name = part.name.clone();
|
||||
}
|
||||
} else {
|
||||
renderer.materials.slots.push(RendererMaterialSlot {
|
||||
id: part.material_slot_id.clone(),
|
||||
name: part.name.clone(),
|
||||
source_material: legacy_material,
|
||||
material: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_component_states(states: &mut AuthoringComponentStates) {
|
||||
let old = std::mem::take(&mut states.states);
|
||||
for state in old {
|
||||
let key = if state.component_id.trim().is_empty() {
|
||||
state.type_name
|
||||
} else {
|
||||
state.component_id
|
||||
};
|
||||
if !key.trim().is_empty() {
|
||||
states.set_component_active(key, state.active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_material_document(path: &Path, text: &str) -> Option<(String, &'static str)> {
|
||||
if let Ok(asset) = ron::from_str::<MaterialAsset>(text) {
|
||||
return ron::ser::to_string_pretty(&asset, ron::ser::PrettyConfig::default())
|
||||
.ok()
|
||||
.map(|text| (text, "material-schema"));
|
||||
}
|
||||
if let Ok(instance) = ron::from_str::<MaterialInstanceAsset>(text) {
|
||||
return ron::ser::to_string_pretty(&instance, ron::ser::PrettyConfig::default())
|
||||
.ok()
|
||||
.map(|text| (text, "material-instance-schema"));
|
||||
}
|
||||
let _ = path;
|
||||
None
|
||||
}
|
||||
|
||||
fn canonical_shader_document(text: &str) -> Option<(String, &'static str)> {
|
||||
let schema = ron::from_str::<ShaderSchemaAsset>(text).ok()?;
|
||||
ron::ser::to_string_pretty(&schema, ron::ser::PrettyConfig::default())
|
||||
.ok()
|
||||
.map(|text| (text, "surface-shader-schema"))
|
||||
}
|
||||
|
||||
fn collect_files(path: &Path, output: &mut Vec<PathBuf>) -> Result<(), String> {
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in fs::read_dir(path).map_err(|error| error.to_string())? {
|
||||
let entry = entry.map_err(|error| error.to_string())?;
|
||||
let path = entry.path();
|
||||
if entry
|
||||
.file_type()
|
||||
.map_err(|error| error.to_string())?
|
||||
.is_dir()
|
||||
{
|
||||
collect_files(&path, output)?;
|
||||
} else {
|
||||
output.push(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_text(text: &str) -> String {
|
||||
text.trim().replace("\r\n", "\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use shared::{
|
||||
EditorAssetRef, InspectorComponentState, MeshRenderSlot,
|
||||
AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn dry_run_does_not_write_and_apply_creates_backup() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-upgrader-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
let level = root.join("assets/levels/main.scn.ron");
|
||||
fs::create_dir_all(level.parent().unwrap()).unwrap();
|
||||
fs::write(&level, "(schema_version: 3, resources: {}, entities: {})").unwrap();
|
||||
|
||||
let dry_run = upgrade_project(&root, false).unwrap();
|
||||
assert!(!dry_run.applied);
|
||||
assert_eq!(dry_run.changes.len(), 1);
|
||||
assert!(fs::read_to_string(&level)
|
||||
.unwrap()
|
||||
.contains("schema_version: 3"));
|
||||
|
||||
let applied = upgrade_project(&root, true).unwrap();
|
||||
assert!(applied.applied);
|
||||
assert!(fs::read_to_string(&level)
|
||||
.unwrap()
|
||||
.contains("schema_version: 4"));
|
||||
assert!(root.join(applied.backup_path.unwrap()).exists());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_upgrade_materializes_slots_and_separates_active_state() {
|
||||
let renderer = StaticMeshRenderer {
|
||||
slots: vec![MeshRenderSlot {
|
||||
id: ComponentInstanceId::new("draw:body"),
|
||||
name: "Body".into(),
|
||||
mesh: EditorAssetRef::new("model", "mesh:body", "Body"),
|
||||
material: Some(EditorAssetRef::new(
|
||||
"model",
|
||||
"material:body",
|
||||
"Body Material",
|
||||
)),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let order = InspectorOrder {
|
||||
component_type_names: vec![COMPONENT_STATIC_MESH_RENDERER.into()],
|
||||
component_states: vec![InspectorComponentState {
|
||||
type_name: COMPONENT_STATIC_MESH_RENDERER.into(),
|
||||
active: false,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let text = format!(
|
||||
"(schema_version: 3, resources: {{}}, entities: {{1: (components: {{\"{COMPONENT_STATIC_MESH_RENDERER}\": {}, \"{INSPECTOR_ORDER_COMPONENT}\": {}}})}})",
|
||||
ron::to_string(&renderer).unwrap(),
|
||||
ron::to_string(&order).unwrap(),
|
||||
);
|
||||
|
||||
let upgraded = canonical_scene_document(&text).unwrap();
|
||||
let document = SceneDocument::from_ron_text(&upgraded).unwrap();
|
||||
let components = &document.entities[0].components;
|
||||
let renderer: StaticMeshRenderer = ron::from_str(
|
||||
&components
|
||||
.iter()
|
||||
.find(|component| component.type_name == COMPONENT_STATIC_MESH_RENDERER)
|
||||
.unwrap()
|
||||
.ron,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(renderer.slots[0].material.is_none());
|
||||
assert_eq!(renderer.materials.slots.len(), 1);
|
||||
assert_eq!(
|
||||
renderer.materials.slots[0]
|
||||
.source_material
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.0
|
||||
.sub_asset_id,
|
||||
"material:body"
|
||||
);
|
||||
let order: InspectorOrder = ron::from_str(
|
||||
&components
|
||||
.iter()
|
||||
.find(|component| component.type_name == INSPECTOR_ORDER_COMPONENT)
|
||||
.unwrap()
|
||||
.ron,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(order.component_states.is_empty());
|
||||
assert_eq!(
|
||||
order.component_ids,
|
||||
vec![AUTHORING_COMPONENT_STATIC_MESH_RENDERER]
|
||||
);
|
||||
let states: AuthoringComponentStates = ron::from_str(
|
||||
&components
|
||||
.iter()
|
||||
.find(|component| component.type_name == AUTHORING_COMPONENT_STATES_COMPONENT)
|
||||
.unwrap()
|
||||
.ron,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!states.is_component_active(COMPONENT_STATIC_MESH_RENDERER));
|
||||
}
|
||||
}
|
||||
@ -227,22 +227,17 @@ impl Default for InputSettings {
|
||||
|
||||
/// Loads settings from disk, falling back to defaults.
|
||||
pub fn load_project_settings_from_path(path: &str) -> ProjectSettings {
|
||||
load_project_settings_with_source(path).0
|
||||
}
|
||||
|
||||
/// Loads settings plus the exact source text used when the file was readable.
|
||||
pub fn load_project_settings_with_source(path: &str) -> (ProjectSettings, Option<String>) {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(contents) => match ron::from_str(&contents) {
|
||||
Ok(settings) => (settings, Some(contents)),
|
||||
Ok(settings) => settings,
|
||||
Err(error) => {
|
||||
warn!("Failed to parse {path}: {error}; using defaults");
|
||||
(ProjectSettings::default(), Some(contents))
|
||||
ProjectSettings::default()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
warn!("Could not read {path}: {error}; using defaults");
|
||||
(ProjectSettings::default(), None)
|
||||
ProjectSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -271,27 +266,6 @@ mod tests {
|
||||
assert_eq!(parsed.rendering.gi_mode, settings.rendering.gi_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_load_returns_the_exact_source_for_revision_guards() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"blacksite-settings-source-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
let path = root.join("project.ron");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let source = save_project_settings_to_string(&ProjectSettings::default()).unwrap();
|
||||
std::fs::write(&path, &source).unwrap();
|
||||
|
||||
let (settings, loaded_source) = load_project_settings_with_source(path.to_str().unwrap());
|
||||
|
||||
assert_eq!(settings.name, ProjectSettings::default().name);
|
||||
assert_eq!(loaded_source.as_deref(), Some(source.as_str()));
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_manifest_defaults_project_identity_fields() {
|
||||
let legacy = r#"(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
load_project_settings_with_source, save_project_settings_to_string, PhysicsSettings,
|
||||
load_project_settings_from_path, save_project_settings_to_string, PhysicsSettings,
|
||||
ProjectSettings, DEFAULT_PROJECT_PATH,
|
||||
};
|
||||
|
||||
@ -56,8 +56,6 @@ impl SimTuning {
|
||||
pub struct ProjectSettingsIo {
|
||||
pub path: String,
|
||||
pub dirty: bool,
|
||||
/// Exact source loaded or last saved by this process; collaboration guards hash it.
|
||||
pub loaded_source: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ProjectSettingsIo {
|
||||
@ -65,7 +63,6 @@ impl Default for ProjectSettingsIo {
|
||||
Self {
|
||||
path: DEFAULT_PROJECT_PATH.into(),
|
||||
dirty: false,
|
||||
loaded_source: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,9 +91,8 @@ impl Plugin for ProjectSettingsPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_settings(mut commands: Commands, mut io: ResMut<ProjectSettingsIo>) {
|
||||
let (settings, source) = load_project_settings_with_source(&io.path);
|
||||
io.loaded_source = source;
|
||||
fn load_settings(mut commands: Commands, io: Res<ProjectSettingsIo>) {
|
||||
let settings = load_project_settings_from_path(&io.path);
|
||||
let tuning = SimTuning::from_physics(&settings.physics);
|
||||
commands.insert_resource(settings);
|
||||
commands.insert_resource(tuning);
|
||||
@ -107,17 +103,13 @@ pub fn sync_sim_tuning(settings: &ProjectSettings, mut tuning: ResMut<SimTuning>
|
||||
*tuning = SimTuning::from_physics(&settings.physics);
|
||||
}
|
||||
|
||||
/// Low-level unconditional persistence for non-editor callers.
|
||||
///
|
||||
/// The editor uses its guarded authored-file publication path instead so external revisions cannot
|
||||
/// be overwritten.
|
||||
/// Persists settings to the path in [`ProjectSettingsIo`].
|
||||
pub fn save_project_settings(
|
||||
settings: &ProjectSettings,
|
||||
io: &mut ProjectSettingsIo,
|
||||
) -> Result<(), String> {
|
||||
let text = save_project_settings_to_string(settings).map_err(|error| error.to_string())?;
|
||||
std::fs::write(&io.path, &text).map_err(|error| error.to_string())?;
|
||||
io.loaded_source = Some(text);
|
||||
std::fs::write(&io.path, text).map_err(|error| error.to_string())?;
|
||||
io.dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ description = "Shared reflectable authoring types and scene hydration for the FP
|
||||
[dependencies]
|
||||
avian3d.workspace = true
|
||||
bevy.workspace = true
|
||||
blake3 = "1"
|
||||
ron = "0.8"
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@ -9,37 +9,14 @@ use bevy::prelude::*;
|
||||
use crate::{
|
||||
animation_clip_source_index, animation_skeleton_source_index, brush_math::validate_brush,
|
||||
ActorKind, AnimationControllerDesc, AudioListenerDesc, AudioSourceDesc, BrushDesc, LevelObject,
|
||||
LightDesc, ModelRef, NavigationArea, NavigationBounds, NavigationLink, NavigationObstacle,
|
||||
ObjectiveMarker, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive,
|
||||
SkinnedMeshRenderer, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
LightDesc, ModelRef, ObjectiveMarker, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance,
|
||||
PrefabRef, Primitive, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
AUDIO_CLIP_SUB_ASSET_ID,
|
||||
};
|
||||
|
||||
/// Deterministic presentation/compatibility hint derived from authoring components.
|
||||
///
|
||||
/// Validation is component-driven; this priority only chooses an icon/default
|
||||
/// label when an actor composes several compatible behaviors (for example a
|
||||
/// rendered mesh with a light).
|
||||
/// One-shot deterministic kind from authoring components (scene migration only).
|
||||
pub fn infer_actor_kind(entity: EntityRef<'_>) -> Option<ActorKind> {
|
||||
entity.get::<LevelObject>()?;
|
||||
if entity.get::<PrefabRef>().is_some() || entity.get::<PrefabInstance>().is_some() {
|
||||
return Some(ActorKind::PrefabAnchor);
|
||||
}
|
||||
if entity.get::<SkinnedMeshRenderer>().is_some() {
|
||||
return Some(ActorKind::SkinnedMesh);
|
||||
}
|
||||
if entity.get::<BrushDesc>().is_some() {
|
||||
return Some(ActorKind::Brush);
|
||||
}
|
||||
if entity.get::<Primitive>().is_some() || entity.get::<StaticMeshRenderer>().is_some() {
|
||||
return Some(ActorKind::StaticMesh);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Some(ActorKind::ImportedModel);
|
||||
}
|
||||
if entity.get::<LightDesc>().is_some() {
|
||||
return Some(ActorKind::Light);
|
||||
}
|
||||
if entity.get::<PlayerSpawn>().is_some() {
|
||||
return Some(ActorKind::PlayerSpawn);
|
||||
}
|
||||
@ -58,19 +35,27 @@ pub fn infer_actor_kind(entity: EntityRef<'_>) -> Option<ActorKind> {
|
||||
if entity.get::<ObjectiveMarker>().is_some() {
|
||||
return Some(ActorKind::Objective);
|
||||
}
|
||||
if entity.get::<LightDesc>().is_some() {
|
||||
return Some(ActorKind::Light);
|
||||
}
|
||||
if entity.get::<BrushDesc>().is_some() {
|
||||
return Some(ActorKind::Brush);
|
||||
}
|
||||
if entity.get::<Primitive>().is_some() || entity.get::<StaticMeshRenderer>().is_some() {
|
||||
return Some(ActorKind::StaticMesh);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Some(ActorKind::ImportedModel);
|
||||
}
|
||||
if entity.get::<PrefabRef>().is_some() || entity.get::<PrefabInstance>().is_some() {
|
||||
return Some(ActorKind::PrefabAnchor);
|
||||
}
|
||||
if entity.get::<AudioSourceDesc>().is_some() {
|
||||
return Some(ActorKind::AudioSource);
|
||||
}
|
||||
if entity.get::<AudioListenerDesc>().is_some() {
|
||||
return Some(ActorKind::AudioListener);
|
||||
}
|
||||
if entity.get::<NavigationBounds>().is_some()
|
||||
|| entity.get::<NavigationObstacle>().is_some()
|
||||
|| entity.get::<NavigationArea>().is_some()
|
||||
|| entity.get::<NavigationLink>().is_some()
|
||||
{
|
||||
return Some(ActorKind::Navigation);
|
||||
}
|
||||
Some(ActorKind::Empty)
|
||||
}
|
||||
|
||||
@ -90,13 +75,6 @@ pub enum ActorValidationError {
|
||||
ImportedModelMissingModelRef,
|
||||
ImportedModelHasPrimitive,
|
||||
ImportedModelHasStaticMeshRenderer,
|
||||
SkinnedMeshMissingRenderer,
|
||||
SkinnedMeshInvalidRenderer,
|
||||
SkinnedMeshHasPrimitive,
|
||||
SkinnedMeshHasStaticMeshRenderer,
|
||||
SkinnedMeshHasModelRef,
|
||||
SkinnedMeshRendererActorKindMismatch,
|
||||
ConflictingGeometrySources,
|
||||
LightMissingLightDesc,
|
||||
LightHasPrimitive,
|
||||
LightHasModelRef,
|
||||
@ -115,7 +93,7 @@ pub enum ActorValidationError {
|
||||
AudioSourceMissingBus,
|
||||
AudioListenerMissingDesc,
|
||||
AudioListenerInvalidEarGap,
|
||||
AnimationControllerMissingSkinnedMeshRenderer,
|
||||
AnimationControllerMissingModelRef,
|
||||
AnimationControllerMissingSkeleton,
|
||||
AnimationControllerInvalidSkeletonReference,
|
||||
AnimationControllerEmptyStateId,
|
||||
@ -126,7 +104,6 @@ pub enum ActorValidationError {
|
||||
AnimationControllerInvalidCrossfade,
|
||||
AnimationControllerMissingDefaultState,
|
||||
AnimationControllerUnknownDefaultState,
|
||||
InvalidNavigation(String),
|
||||
}
|
||||
|
||||
fn is_finite_positive(v: f32) -> bool {
|
||||
@ -143,10 +120,9 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(_stored_kind) = entity.get::<ActorKind>() else {
|
||||
let Some(kind) = entity.get::<ActorKind>() else {
|
||||
return Err(ActorValidationError::MissingActorKind);
|
||||
};
|
||||
let kind = infer_actor_kind(entity).unwrap_or(ActorKind::Empty);
|
||||
|
||||
if entity.get::<Transform>().is_none() {
|
||||
return Err(ActorValidationError::MissingTransform);
|
||||
@ -159,50 +135,13 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
validate_audio_listener(listener)?;
|
||||
}
|
||||
if let Some(controller) = entity.get::<AnimationControllerDesc>() {
|
||||
if entity.get::<SkinnedMeshRenderer>().is_none() {
|
||||
return Err(ActorValidationError::AnimationControllerMissingSkinnedMeshRenderer);
|
||||
if entity.get::<ModelRef>().is_none() {
|
||||
return Err(ActorValidationError::AnimationControllerMissingModelRef);
|
||||
}
|
||||
validate_animation_controller(controller)?;
|
||||
}
|
||||
let geometry_source_count = usize::from(entity.get::<Primitive>().is_some())
|
||||
+ usize::from(entity.get::<BrushDesc>().is_some())
|
||||
+ usize::from(entity.get::<StaticMeshRenderer>().is_some())
|
||||
+ usize::from(entity.get::<SkinnedMeshRenderer>().is_some())
|
||||
+ usize::from(entity.get::<ModelRef>().is_some());
|
||||
if geometry_source_count > 1 {
|
||||
return Err(ActorValidationError::ConflictingGeometrySources);
|
||||
}
|
||||
if let Some(brush) = entity.get::<BrushDesc>() {
|
||||
let report = validate_brush(brush);
|
||||
if !report.is_valid() {
|
||||
let message = report
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.find(|diagnostic| {
|
||||
diagnostic.severity == crate::brush_math::BrushDiagnosticSeverity::Error
|
||||
})
|
||||
.map(|diagnostic| diagnostic.message)
|
||||
.unwrap_or_else(|| "Brush geometry is invalid.".to_string());
|
||||
return Err(ActorValidationError::InvalidBrushGeometry(message));
|
||||
}
|
||||
}
|
||||
if let Some(renderer) = entity.get::<SkinnedMeshRenderer>() {
|
||||
if renderer.path.trim().is_empty() {
|
||||
return Err(ActorValidationError::SkinnedMeshInvalidRenderer);
|
||||
}
|
||||
}
|
||||
if let Some(desc) = entity.get::<PostProcessVolumeDesc>() {
|
||||
validate_post_process_volume(desc)?;
|
||||
}
|
||||
if entity.get::<NavigationBounds>().is_some()
|
||||
|| entity.get::<NavigationObstacle>().is_some()
|
||||
|| entity.get::<NavigationArea>().is_some()
|
||||
|| entity.get::<NavigationLink>().is_some()
|
||||
{
|
||||
validate_navigation_actor(entity)?;
|
||||
}
|
||||
|
||||
match &kind {
|
||||
match kind {
|
||||
ActorKind::Brush => {
|
||||
let Some(brush) = entity.get::<BrushDesc>() else {
|
||||
return Err(ActorValidationError::BrushMissingDesc);
|
||||
@ -213,10 +152,24 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
if entity.get::<StaticMeshRenderer>().is_some() {
|
||||
return Err(ActorValidationError::BrushHasStaticMeshRenderer);
|
||||
}
|
||||
if entity.get::<LightDesc>().is_some() {
|
||||
return Err(ActorValidationError::BrushHasLight);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Err(ActorValidationError::BrushHasModelRef);
|
||||
}
|
||||
let _ = brush;
|
||||
let report = validate_brush(brush);
|
||||
if !report.is_valid() {
|
||||
let message = report
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.find(|diagnostic| {
|
||||
diagnostic.severity == crate::brush_math::BrushDiagnosticSeverity::Error
|
||||
})
|
||||
.map(|diagnostic| diagnostic.message)
|
||||
.unwrap_or_else(|| "Brush geometry is invalid.".to_string());
|
||||
return Err(ActorValidationError::InvalidBrushGeometry(message));
|
||||
}
|
||||
}
|
||||
ActorKind::StaticMesh => {
|
||||
let has_static_mesh_renderer = entity
|
||||
@ -225,24 +178,13 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
if entity.get::<Primitive>().is_none() && !has_static_mesh_renderer {
|
||||
return Err(ActorValidationError::StaticMeshMissingPrimitive);
|
||||
}
|
||||
if entity.get::<LightDesc>().is_some() {
|
||||
return Err(ActorValidationError::StaticMeshHasLight);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Err(ActorValidationError::StaticMeshHasModelRef);
|
||||
}
|
||||
}
|
||||
ActorKind::SkinnedMesh => {
|
||||
let Some(_renderer) = entity.get::<SkinnedMeshRenderer>() else {
|
||||
return Err(ActorValidationError::SkinnedMeshMissingRenderer);
|
||||
};
|
||||
if entity.get::<Primitive>().is_some() {
|
||||
return Err(ActorValidationError::SkinnedMeshHasPrimitive);
|
||||
}
|
||||
if entity.get::<StaticMeshRenderer>().is_some() {
|
||||
return Err(ActorValidationError::SkinnedMeshHasStaticMeshRenderer);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Err(ActorValidationError::SkinnedMeshHasModelRef);
|
||||
}
|
||||
}
|
||||
ActorKind::ImportedModel => {
|
||||
if entity.get::<ModelRef>().is_none() {
|
||||
return Err(ActorValidationError::ImportedModelMissingModelRef);
|
||||
@ -258,13 +200,40 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
if entity.get::<LightDesc>().is_none() {
|
||||
return Err(ActorValidationError::LightMissingLightDesc);
|
||||
}
|
||||
// Other compatible authoring components may be composed with a light.
|
||||
if entity.get::<Primitive>().is_some() {
|
||||
return Err(ActorValidationError::LightHasPrimitive);
|
||||
}
|
||||
if entity.get::<ModelRef>().is_some() {
|
||||
return Err(ActorValidationError::LightHasModelRef);
|
||||
}
|
||||
if entity.get::<StaticMeshRenderer>().is_some() {
|
||||
return Err(ActorValidationError::LightHasStaticMeshRenderer);
|
||||
}
|
||||
}
|
||||
ActorKind::PostProcessVolume => {
|
||||
let Some(desc) = entity.get::<PostProcessVolumeDesc>() else {
|
||||
return Err(ActorValidationError::PostProcessVolumeMissingDesc);
|
||||
};
|
||||
let _ = desc;
|
||||
if !is_finite_positive(desc.half_extents.x)
|
||||
|| !is_finite_positive(desc.half_extents.y)
|
||||
|| !is_finite_positive(desc.half_extents.z)
|
||||
{
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidHalfExtents);
|
||||
}
|
||||
if !is_finite_non_negative(desc.blend_distance) {
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidBlendDistance);
|
||||
}
|
||||
let o = &desc.overrides;
|
||||
if let Some(v) = o.exposure_ev100 {
|
||||
if !v.is_finite() {
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidOverrideScalar);
|
||||
}
|
||||
}
|
||||
if let Some(v) = o.fog_density {
|
||||
if !v.is_finite() || v < 0.0 {
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidOverrideScalar);
|
||||
}
|
||||
}
|
||||
}
|
||||
ActorKind::AudioSource => {
|
||||
if entity.get::<AudioSourceDesc>().is_none() {
|
||||
@ -276,7 +245,6 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
return Err(ActorValidationError::AudioListenerMissingDesc);
|
||||
}
|
||||
}
|
||||
ActorKind::Navigation => {}
|
||||
ActorKind::Empty
|
||||
| ActorKind::PrefabAnchor
|
||||
| ActorKind::PlayerSpawn
|
||||
@ -289,109 +257,6 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_post_process_volume(desc: &PostProcessVolumeDesc) -> Result<(), ActorValidationError> {
|
||||
if !is_finite_positive(desc.half_extents.x)
|
||||
|| !is_finite_positive(desc.half_extents.y)
|
||||
|| !is_finite_positive(desc.half_extents.z)
|
||||
{
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidHalfExtents);
|
||||
}
|
||||
if !is_finite_non_negative(desc.blend_distance) {
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidBlendDistance);
|
||||
}
|
||||
let overrides = &desc.overrides;
|
||||
if overrides
|
||||
.exposure_ev100
|
||||
.is_some_and(|value| !value.is_finite())
|
||||
|| overrides
|
||||
.fog_density
|
||||
.is_some_and(|value| !value.is_finite() || value < 0.0)
|
||||
{
|
||||
return Err(ActorValidationError::PostProcessVolumeInvalidOverrideScalar);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_navigation_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError> {
|
||||
let mut component_count = 0;
|
||||
if let Some(bounds) = entity.get::<NavigationBounds>() {
|
||||
component_count += 1;
|
||||
validate_vec3_positive(bounds.half_extents, "navigation bounds")?;
|
||||
bounds
|
||||
.agent
|
||||
.validate()
|
||||
.map_err(|error| ActorValidationError::InvalidNavigation(error.into()))?;
|
||||
crate::navigation_generated_artifact_path(&bounds.artifact_path)
|
||||
.map_err(|error| ActorValidationError::InvalidNavigation(error.into()))?;
|
||||
let mut sample_ids = HashSet::new();
|
||||
for sample in &bounds.validation_samples {
|
||||
let sample_id = sample.id.trim();
|
||||
if sample_id.is_empty() {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation validation sample ID must not be empty".into(),
|
||||
));
|
||||
}
|
||||
if !sample_ids.insert(sample_id) {
|
||||
return Err(ActorValidationError::InvalidNavigation(format!(
|
||||
"navigation validation sample ID `{sample_id}` must be unique within its bounds"
|
||||
)));
|
||||
}
|
||||
if !sample.start.is_finite() || !sample.end.is_finite() || sample.start == sample.end {
|
||||
return Err(ActorValidationError::InvalidNavigation(format!(
|
||||
"navigation validation sample `{sample_id}` endpoints must be finite and distinct"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(obstacle) = entity.get::<NavigationObstacle>() {
|
||||
component_count += 1;
|
||||
validate_vec3_positive(obstacle.half_extents, "navigation obstacle")?;
|
||||
}
|
||||
if let Some(area) = entity.get::<NavigationArea>() {
|
||||
component_count += 1;
|
||||
validate_vec3_positive(area.half_extents, "navigation area")?;
|
||||
if area.id.trim().is_empty() {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation area ID must not be empty".into(),
|
||||
));
|
||||
}
|
||||
if !is_finite_positive(area.cost) {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation area cost must be finite and greater than zero".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(link) = entity.get::<NavigationLink>() {
|
||||
component_count += 1;
|
||||
if !link.start.is_finite() || !link.end.is_finite() || link.start == link.end {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation link endpoints must be finite and distinct".into(),
|
||||
));
|
||||
}
|
||||
if !is_finite_positive(link.cost) {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation link cost must be finite and greater than zero".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if component_count != 1 {
|
||||
return Err(ActorValidationError::InvalidNavigation(
|
||||
"navigation actors must own exactly one bounds, obstacle, area, or link component"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_vec3_positive(value: Vec3, label: &str) -> Result<(), ActorValidationError> {
|
||||
if !value.is_finite() || value.min_element() <= 0.0 {
|
||||
return Err(ActorValidationError::InvalidNavigation(format!(
|
||||
"{label} half extents must be finite and greater than zero"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_audio_source(source: &AudioSourceDesc) -> Result<(), ActorValidationError> {
|
||||
let Some(clip) = source.clip.as_ref() else {
|
||||
return Err(ActorValidationError::AudioSourceMissingClip);
|
||||
@ -558,7 +423,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_static_mesh_composes_with_light_and_uses_derived_hint() {
|
||||
fn validate_static_mesh_rejects_light() {
|
||||
let mut world = World::new();
|
||||
let e = level_entity(
|
||||
&mut world,
|
||||
@ -568,10 +433,9 @@ mod tests {
|
||||
LightDesc::default(),
|
||||
),
|
||||
);
|
||||
assert!(validate_actor(world.entity(e)).is_ok());
|
||||
assert_eq!(
|
||||
infer_actor_kind(world.entity(e)),
|
||||
Some(ActorKind::StaticMesh)
|
||||
validate_actor(world.entity(e)),
|
||||
Err(ActorValidationError::StaticMeshHasLight)
|
||||
);
|
||||
}
|
||||
|
||||
@ -595,7 +459,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
validate_actor(world.entity(e)),
|
||||
Err(ActorValidationError::ConflictingGeometrySources)
|
||||
Err(ActorValidationError::BrushHasStaticMeshRenderer)
|
||||
);
|
||||
}
|
||||
|
||||
@ -723,17 +587,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_controller_requires_skinned_renderer_and_resolved_skeleton() {
|
||||
fn animation_controller_requires_model_and_resolved_skeleton() {
|
||||
let mut world = World::new();
|
||||
let entity = level_entity(&mut world, (ActorKind::SkinnedMesh, animation_controller()));
|
||||
let entity = level_entity(
|
||||
&mut world,
|
||||
(ActorKind::ImportedModel, animation_controller()),
|
||||
);
|
||||
assert_eq!(
|
||||
validate_actor(world.entity(entity)),
|
||||
Err(ActorValidationError::AnimationControllerMissingSkinnedMeshRenderer)
|
||||
Err(ActorValidationError::AnimationControllerMissingModelRef)
|
||||
);
|
||||
|
||||
world
|
||||
.entity_mut(entity)
|
||||
.insert(SkinnedMeshRenderer::new("assets/models/animated.glb"));
|
||||
.insert(ModelRef::new("assets/models/animated.glb"));
|
||||
let mut controller = animation_controller();
|
||||
controller.skeleton = None;
|
||||
world.entity_mut(entity).insert(controller);
|
||||
@ -760,8 +627,8 @@ mod tests {
|
||||
let entity = level_entity(
|
||||
&mut world,
|
||||
(
|
||||
ActorKind::SkinnedMesh,
|
||||
SkinnedMeshRenderer::new("assets/models/animated.glb"),
|
||||
ActorKind::ImportedModel,
|
||||
ModelRef::new("assets/models/animated.glb"),
|
||||
animation_controller(),
|
||||
),
|
||||
);
|
||||
@ -799,8 +666,8 @@ mod tests {
|
||||
let entity = level_entity(
|
||||
&mut world,
|
||||
(
|
||||
ActorKind::SkinnedMesh,
|
||||
SkinnedMeshRenderer::new("assets/models/animated.glb"),
|
||||
ActorKind::ImportedModel,
|
||||
ModelRef::new("assets/models/animated.glb"),
|
||||
animation_controller(),
|
||||
),
|
||||
);
|
||||
@ -848,37 +715,4 @@ mod tests {
|
||||
Err(ActorValidationError::AnimationControllerUnknownDefaultState)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_bounds_validate_generated_path_and_unique_samples() {
|
||||
let mut world = World::new();
|
||||
let mut bounds = NavigationBounds::for_actor("bounds-main");
|
||||
bounds.validation_samples.push(crate::NavigationPathSample {
|
||||
id: "entry-to-exit".into(),
|
||||
start: Vec3::new(-2.0, 0.0, 0.0),
|
||||
end: Vec3::new(2.0, 0.0, 0.0),
|
||||
enabled: true,
|
||||
});
|
||||
let entity = level_entity(&mut world, (ActorKind::Navigation, bounds.clone()));
|
||||
assert!(validate_actor(world.entity(entity)).is_ok());
|
||||
|
||||
bounds
|
||||
.validation_samples
|
||||
.push(bounds.validation_samples[0].clone());
|
||||
world.entity_mut(entity).insert(bounds.clone());
|
||||
assert!(matches!(
|
||||
validate_actor(world.entity(entity)),
|
||||
Err(ActorValidationError::InvalidNavigation(message))
|
||||
if message.contains("must be unique")
|
||||
));
|
||||
|
||||
bounds.validation_samples.pop();
|
||||
bounds.artifact_path = "assets/navigation/manual.nav.ron".into();
|
||||
world.entity_mut(entity).insert(bounds);
|
||||
assert!(matches!(
|
||||
validate_actor(world.entity(entity)),
|
||||
Err(ActorValidationError::InvalidNavigation(message))
|
||||
if message.contains("assets/navigation/generated/")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::EditorAssetRef;
|
||||
|
||||
pub const ANIMATION_MANIFEST_SCHEMA_VERSION: u32 = 3;
|
||||
pub const ANIMATION_MANIFEST_SCHEMA_VERSION: u32 = 2;
|
||||
pub const ANIMATION_ARTIFACT_DIR: &str = "assets/animations/generated";
|
||||
pub const ANIMATION_CLIP_SUB_ASSET_PREFIX: &str = "animation:clip:";
|
||||
pub const ANIMATION_SKELETON_SUB_ASSET_PREFIX: &str = "animation:skeleton:";
|
||||
pub const COMPONENT_SKINNED_MESH_RENDERER: &str = "shared::animation::SkinnedMeshRenderer";
|
||||
pub const COMPONENT_ANIMATION_CONTROLLER_DESC: &str = "shared::animation::AnimationControllerDesc";
|
||||
|
||||
const DEFAULT_CROSSFADE_SECONDS: f32 = 0.2;
|
||||
@ -76,54 +75,6 @@ impl AnimationSkeletonSignature {
|
||||
}
|
||||
}
|
||||
|
||||
/// Authoring renderer for rigged geometry that must retain its imported joint hierarchy.
|
||||
///
|
||||
/// Unlike [`crate::StaticMeshRenderer`], this component never flattens source primitives into
|
||||
/// independent mesh slots. Hydration instantiates the selected model scene so Bevy can preserve
|
||||
/// its `SkinnedMesh`, joint entities, inverse bind poses, and animation player bindings.
|
||||
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SkinnedMeshRenderer {
|
||||
/// Stable asset-registry UUID. Empty only for legacy or unresolved references.
|
||||
#[serde(default)]
|
||||
pub asset_id: String,
|
||||
/// Asset path relative to `assets/`, e.g. `models/character.glb`.
|
||||
pub path: String,
|
||||
/// Scene index whose hierarchy owns the skinned primitives and joints.
|
||||
#[serde(default)]
|
||||
pub scene_index: usize,
|
||||
/// Stable material slots for every draw binding in the imported animated hierarchy.
|
||||
#[serde(default)]
|
||||
pub materials: crate::RendererMaterialSet,
|
||||
}
|
||||
|
||||
impl SkinnedMeshRenderer {
|
||||
pub fn new(path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
asset_id: String::new(),
|
||||
path: path.into(),
|
||||
scene_index: 0,
|
||||
materials: crate::RendererMaterialSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_asset_id(mut self, asset_id: impl Into<String>) -> Self {
|
||||
self.asset_id = asset_id.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Asset-server path for FBX `#SceneN` labels (see `bevy_ufbx`).
|
||||
pub fn fbx_scene_asset_path(path: &str, scene_index: usize) -> String {
|
||||
format!("{path}#Scene{scene_index}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SkinnedMeshRenderer {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Authored playback window inside an imported clip.
|
||||
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@ -184,7 +135,7 @@ impl Default for AnimationStateDesc {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scene-authored single-layer animation controller for a sibling [`SkinnedMeshRenderer`].
|
||||
/// Scene-authored single-layer animation controller.
|
||||
///
|
||||
/// Bevy graph, player, transition, and instantiated-world state are derived at runtime.
|
||||
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@ -240,12 +191,6 @@ pub struct AnimationManifest {
|
||||
pub schema_version: u32,
|
||||
pub asset_id: String,
|
||||
pub label: String,
|
||||
/// Explicit imported clip shown as the asset's edit-mode rest presentation.
|
||||
///
|
||||
/// This is a stable clip sub-asset ID. `None` deliberately preserves the imported node pose;
|
||||
/// importers and placement code must never guess the first clip.
|
||||
#[serde(default)]
|
||||
pub default_animation_clip_id: Option<String>,
|
||||
pub source: AnimationManifestSource,
|
||||
/// Whether this source format can hydrate the extracted animation data at runtime.
|
||||
pub runtime_supported: bool,
|
||||
@ -370,26 +315,4 @@ mod tests {
|
||||
assert_eq!(animation_skeleton_source_index(&skeleton), Some(3));
|
||||
assert_eq!(animation_clip_source_index(&skeleton), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_v2_manifest_defaults_to_imported_rest_pose() {
|
||||
let legacy = r#"(
|
||||
schema_version: 2,
|
||||
asset_id: "model-id",
|
||||
label: "Legacy",
|
||||
source: (
|
||||
path: "assets/models/legacy.glb",
|
||||
format: "glb",
|
||||
fingerprint: (byte_len: 1, modified_unix_secs: 0, content_hash: "hash"),
|
||||
dependencies: [],
|
||||
),
|
||||
runtime_supported: true,
|
||||
skeletons: [],
|
||||
clips: [],
|
||||
diagnostics: [],
|
||||
)"#;
|
||||
|
||||
let manifest: AnimationManifest = ron::from_str(legacy).unwrap();
|
||||
assert!(manifest.default_animation_clip_id.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user