Compare commits
3 Commits
c55f34780f
...
62b538999d
| Author | SHA1 | Date | |
|---|---|---|---|
| 62b538999d | |||
| d52cc2e3b7 | |||
| 06010c5922 |
@ -7,6 +7,7 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
|||||||
[alias]
|
[alias]
|
||||||
clean-target = "run -p xtask --bin clean-target --"
|
clean-target = "run -p xtask --bin clean-target --"
|
||||||
validate-levels = "run -p xtask --features validate-levels --bin validate-levels --"
|
validate-levels = "run -p xtask --features validate-levels --bin validate-levels --"
|
||||||
|
validate-samples = "run -p xtask --features validate-samples --bin validate-samples --"
|
||||||
bake-navigation = "run -p xtask --features validate-levels --bin bake-navigation --"
|
bake-navigation = "run -p xtask --features validate-levels --bin bake-navigation --"
|
||||||
package-project = "run -p xtask --features validate-levels --bin package-project --"
|
package-project = "run -p xtask --features validate-levels --bin package-project --"
|
||||||
upgrade-project = "run -p xtask --features validate-levels --bin upgrade-project --"
|
upgrade-project = "run -p xtask --features validate-levels --bin upgrade-project --"
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
# Editor Sample Regression Pack
|
||||||
|
|
||||||
|
Date: 2026-07-13
|
||||||
|
Issue: BS-JD-501 / Gitea #32
|
||||||
|
Milestone: M5 - Regression, docs, and first-hour UX
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Ship a deterministic five-scene editor regression pack that is easy to open from the editor and
|
||||||
|
that fails headless validation when its catalog, authored data, or referenced assets drift. The pack
|
||||||
|
must cover Brush, Material, Terrain, Physics Placement, and Rendering workflows.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- `assets/samples/editor_samples.ron` is the conventional, versioned pack manifest. This remains a
|
||||||
|
QA/editor convention and does not extend `ProjectSettings`.
|
||||||
|
- The manifest is parsed and validated in the `scene` crate. Editor UI and headless tooling consume
|
||||||
|
that one representation.
|
||||||
|
- Every entry has a stable ID, label, area, project-relative `.scn.ron` path, summary, and nonempty
|
||||||
|
operator checks. Manifest order is menu and report order.
|
||||||
|
- Exactly one entry for each required area is release-gated. Scene paths must remain regular files
|
||||||
|
inside `assets/levels/`; traversal, absolute paths, symlink escapes, duplicates, and future schemas
|
||||||
|
are blocking.
|
||||||
|
- Existing accepted terrain, physics-placement, and rendering fixtures remain canonical entries.
|
||||||
|
Add dedicated brush and material scenes under `assets/levels/samples/` and strengthen weak visual
|
||||||
|
anchors in the existing fixtures instead of duplicating them.
|
||||||
|
- `cargo validate-samples --project .` combines pack-specific structure/feature checks with the
|
||||||
|
authoritative `validate_project` report. It verifies stable actor IDs, required area components,
|
||||||
|
authoring-only scene documents, and referenced content.
|
||||||
|
- Terrain descriptors and their shared material layers become first-class project-validation input.
|
||||||
|
Actor-owned invalid descriptors or missing material references are blocking findings.
|
||||||
|
- `File > Open Sample` is backed by a cached project catalog and opens through normal scene-tab I/O.
|
||||||
|
Invalid or unavailable catalogs produce a disabled, actionable row rather than a panic.
|
||||||
|
- CI hydrates Git LFS before content validation and runs both validation commands. Packaged-runtime
|
||||||
|
tests remain deferred by project-owner request.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
1. Add the manifest schema, deterministic report, safe path resolver, and sample feature checks to
|
||||||
|
`scene::sample_pack`, with malformed/missing/duplicate/path-escape fixture tests.
|
||||||
|
2. Add typed `TerrainDesc` validation and base/layer material dependency collection to authoritative
|
||||||
|
project validation, including owner-attributed tests.
|
||||||
|
3. Add the `validate-samples` xtask binary, Cargo alias, JSON mode, stable console summary, and CI
|
||||||
|
steps. Enable LFS hydration at checkout and verify hydrated objects.
|
||||||
|
4. Add the five-entry manifest, `brush_blockout.scn.ron`, and `material_lab.scn.ron`. Give every
|
||||||
|
sample actor a stable `ActorId`; add visible rendering anchors and distinct physics shapes where
|
||||||
|
the current fixtures are ambiguous.
|
||||||
|
5. Load the catalog after project startup, render `File > Open Sample` in manifest order, and route a
|
||||||
|
selected stable ID through scene I/O. Add unknown-entry and deterministic-row tests.
|
||||||
|
6. Add a no-GPU typed-deserialization test over every manifest scene so unknown Bevy component
|
||||||
|
registrations fail before native QA.
|
||||||
|
7. Document pack contracts, per-scene checks, release commands, and the editor workflow. Update ADR
|
||||||
|
0028, the root README checklist/controls, editor/docs indexes, feature guides, roadmap status,
|
||||||
|
debt audit, and production-readiness evidence.
|
||||||
|
8. Run source gates, open all five samples through the native menu, exercise one representative tool
|
||||||
|
interaction per area, inspect logs, and attach representative native issue images when useful.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- Five manifest entries cover all required areas and open through `File > Open Sample`.
|
||||||
|
- Every entry type-deserializes and has stable actor IDs plus its required authored feature.
|
||||||
|
- `validate-levels` reports terrain material dependencies and has no blocking findings.
|
||||||
|
- `validate-samples` is deterministic, CI-backed, and exits nonzero for pack or project blockers.
|
||||||
|
- Native viewport captures are nonblank, correctly framed, and free of missing-reference, hydration,
|
||||||
|
render, or operator errors.
|
||||||
|
- Documentation gives contributors a single release-use workflow and explains every sample check.
|
||||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -17,6 +17,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Verify Git LFS checkout
|
||||||
|
run: git lfs fsck
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -55,6 +60,9 @@ jobs:
|
|||||||
- name: Validate level scenes
|
- name: Validate level scenes
|
||||||
run: cargo validate-levels
|
run: cargo validate-levels
|
||||||
|
|
||||||
|
- name: Validate editor sample pack
|
||||||
|
run: cargo validate-samples
|
||||||
|
|
||||||
- name: Clippy launch feature matrix
|
- name: Clippy launch feature matrix
|
||||||
run: cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
run: cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ cargo clippy --workspace --all-targets -- -D warnings
|
|||||||
cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
cargo validate-levels
|
cargo validate-levels
|
||||||
|
cargo validate-samples
|
||||||
cargo bake-navigation --project . --check
|
cargo bake-navigation --project . --check
|
||||||
# Validate one artifact without opening a game window
|
# Validate one artifact without opening a game window
|
||||||
cargo run -p game -- --validate-navigation assets/navigation/generated/navigation_showcase_humanoid.nav.ron
|
cargo run -p game -- --validate-navigation assets/navigation/generated/navigation_showcase_humanoid.nav.ron
|
||||||
@ -164,7 +165,7 @@ deep-stale variants.
|
|||||||
| Drag actor between rows / onto Scene Root (Hierarchy, Manual sort) | Reorder siblings / unparent to the root |
|
| Drag actor between rows / onto Scene Root (Hierarchy, Manual sort) | Reorder siblings / unparent to the root |
|
||||||
| Hierarchy lock | Excludes the actor from selection, gizmos, multi-drag, structural drops, and mutating context actions |
|
| 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 |
|
| 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, non-blocking native Open/Save As, recovery restore/keep-copy/discard when available, Import Assets, Export Selection, Save Selection As Prefab (including linked-root variants), Recent Scenes |
|
| File menu | New, non-blocking native Open/Save As, **Open Sample** for the five-area regression pack, 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 |
|
| 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 |
|
| 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 |
|
| Main toolbar, right side | Switch or close independent scene tabs, create an untitled tab, and manage loaded/locked composed subscenes |
|
||||||
@ -417,6 +418,7 @@ crates/
|
|||||||
- [x] Cursor/VSCode workspace tasks + launch configs
|
- [x] Cursor/VSCode workspace tasks + launch configs
|
||||||
- [x] Opaque Wayland window launch defaults + SDR/HDR runtime toggle
|
- [x] Opaque Wayland window launch defaults + SDR/HDR runtime toggle
|
||||||
- [x] CI workflow for format, check, clippy, tests, and binary builds
|
- [x] CI workflow for format, check, clippy, tests, and binary builds
|
||||||
|
- [x] Deterministic five-area editor sample regression pack with stable actor IDs, **File > Open Sample**, typed scene checks, headless validation, and committed brush/material/terrain/physics/rendering fixtures ([sample guide](docs/editor/sample-regression-pack.md), [Gitea #32](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/32))
|
||||||
- [x] ADRs for roadmap architecture and Bevy migration policy
|
- [x] ADRs for roadmap architecture and Bevy migration policy
|
||||||
- [x] Determinism harness: same inputs over same ticks produce the same state summary/hash
|
- [x] Determinism harness: same inputs over same ticks produce the same state summary/hash
|
||||||
- [x] Production operator invariants across palette dispatch, assets/material drops, brush/terrain/physics modal tools, grouping/lighting, and the transform finalizer: stable terminal status, exact cancel/failure rollback, helper cleanup, grouped history, and repeated undo/redo projections ([testing contract](docs/editor/operator-regression-testing.md), [evaluation](docs/editor/evaluations/operator-invariants/), [Gitea #33](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/33))
|
- [x] Production operator invariants across palette dispatch, assets/material drops, brush/terrain/physics modal tools, grouping/lighting, and the transform finalizer: stable terminal status, exact cancel/failure rollback, helper cleanup, grouped history, and repeated undo/redo projections ([testing contract](docs/editor/operator-regression-testing.md), [evaluation](docs/editor/evaluations/operator-invariants/), [Gitea #33](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/33))
|
||||||
|
|||||||
@ -287,6 +287,42 @@
|
|||||||
),
|
),
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
id: ("607864bf-fa77-4f9e-acc9-2c0906d34163"),
|
||||||
|
path: "assets/levels/samples/material_lab.scn.ron",
|
||||||
|
label: "material_lab.scn",
|
||||||
|
kind_tag: "Level",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
|
animation_manifest_path: None,
|
||||||
|
default_animation_clip_id: None,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("bca13177-afa9-4691-9b40-288059c7a57e"),
|
||||||
|
path: "assets/levels/samples/brush_blockout.scn.ron",
|
||||||
|
label: "brush_blockout.scn",
|
||||||
|
kind_tag: "Level",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
|
animation_manifest_path: None,
|
||||||
|
default_animation_clip_id: None,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
(
|
(
|
||||||
id: ("5a2307d0-48e5-40d3-a5c9-527c0cfd30f4"),
|
id: ("5a2307d0-48e5-40d3-a5c9-527c0cfd30f4"),
|
||||||
path: "assets/levels/editor_scene.scn.ron",
|
path: "assets/levels/editor_scene.scn.ron",
|
||||||
@ -395,6 +431,24 @@
|
|||||||
),
|
),
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
id: ("e42185de-cda2-4a9e-9df0-df67ccd86123"),
|
||||||
|
path: "assets/samples/editor_samples.ron",
|
||||||
|
label: "editor_samples",
|
||||||
|
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: ("04a4e00e-4732-43db-8e99-9cd99b94667b"),
|
id: ("04a4e00e-4732-43db-8e99-9cd99b94667b"),
|
||||||
path: "assets/animations/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.animation.ron",
|
path: "assets/animations/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.animation.ron",
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
2: (components: {
|
2: (components: {
|
||||||
"bevy_ecs::name::Name": "Placement Prop A",
|
"bevy_ecs::name::Name": "Placement Cuboid",
|
||||||
"bevy_transform::components::transform::Transform": (
|
"bevy_transform::components::transform::Transform": (
|
||||||
translation: (-2.0, 3.0, 0.0),
|
translation: (-2.0, 3.0, 0.0),
|
||||||
rotation: (0.1305262, 0.0, 0.0, 0.9914449),
|
rotation: (0.1305262, 0.0, 0.0, 0.9914449),
|
||||||
@ -70,7 +70,7 @@
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
3: (components: {
|
3: (components: {
|
||||||
"bevy_ecs::name::Name": "Placement Prop B",
|
"bevy_ecs::name::Name": "Placement Sphere",
|
||||||
"bevy_transform::components::transform::Transform": (
|
"bevy_transform::components::transform::Transform": (
|
||||||
translation: (0.0, 5.0, 0.0),
|
translation: (0.0, 5.0, 0.0),
|
||||||
rotation: (0.0, 0.2164396, 0.0, 0.976296),
|
rotation: (0.0, 0.2164396, 0.0, 0.976296),
|
||||||
@ -96,16 +96,16 @@
|
|||||||
parameters: [],
|
parameters: [],
|
||||||
textures: [],
|
textures: [],
|
||||||
),
|
),
|
||||||
"shared::components::Primitive": (shape: Box, size: (1.0, 1.6, 1.0)),
|
"shared::components::Primitive": (shape: Sphere, size: (1.6, 1.6, 1.6)),
|
||||||
"shared::components::RigidBodyDesc": (body: Static),
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
"shared::components::ColliderDesc": (
|
"shared::components::ColliderDesc": (
|
||||||
enabled: true,
|
enabled: true,
|
||||||
is_trigger: false,
|
is_trigger: false,
|
||||||
shape: Cuboid(x_length: 1.0, y_length: 1.6, z_length: 1.0),
|
shape: Sphere(radius: 0.8),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
4: (components: {
|
4: (components: {
|
||||||
"bevy_ecs::name::Name": "Placement Prop C",
|
"bevy_ecs::name::Name": "Placement Capsule",
|
||||||
"bevy_transform::components::transform::Transform": (
|
"bevy_transform::components::transform::Transform": (
|
||||||
translation: (2.0, 7.0, 0.0),
|
translation: (2.0, 7.0, 0.0),
|
||||||
rotation: (0.092296, 0.092296, -0.008077, 0.991405),
|
rotation: (0.092296, 0.092296, -0.008077, 0.991405),
|
||||||
@ -131,12 +131,12 @@
|
|||||||
parameters: [],
|
parameters: [],
|
||||||
textures: [],
|
textures: [],
|
||||||
),
|
),
|
||||||
"shared::components::Primitive": (shape: Box, size: (1.4, 0.8, 1.0)),
|
"shared::components::Primitive": (shape: Box, size: (0.9, 1.8, 0.9)),
|
||||||
"shared::components::RigidBodyDesc": (body: Static),
|
"shared::components::RigidBodyDesc": (body: Static),
|
||||||
"shared::components::ColliderDesc": (
|
"shared::components::ColliderDesc": (
|
||||||
enabled: true,
|
enabled: true,
|
||||||
is_trigger: false,
|
is_trigger: false,
|
||||||
shape: Cuboid(x_length: 1.4, y_length: 0.8, z_length: 1.0),
|
shape: Capsule(radius: 0.45, height: 1.8),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
5: (components: {
|
5: (components: {
|
||||||
|
|||||||
@ -1,68 +1,241 @@
|
|||||||
(schema_version: 4,resources: {
|
(schema_version: 4, resources: {}, entities: {
|
||||||
},
|
1: (components: {
|
||||||
entities: {
|
"bevy_ecs::name::Name": "Rendering Lab Floor",
|
||||||
4294968001: (components: {
|
"bevy_transform::components::transform::Transform": (
|
||||||
"bevy_ecs::name::Name": "Fog Volume",
|
translation: (1.0, -0.25, 0.0),
|
||||||
"bevy_transform::components::transform::Transform": (
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
translation: (0.0, 2.0, 0.0),
|
scale: (1.0, 1.0, 1.0),
|
||||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
),
|
||||||
"shared::components::ActorKind": PostProcessVolume,
|
"shared::components::ActorId": ("rendering-showcase-floor"),
|
||||||
"shared::components::LevelObject": (),
|
"shared::components::ActorKind": StaticMesh,
|
||||||
"shared::components::PostProcessVolumeDesc": (
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
half_extents: (6.0, 3.0, 6.0),
|
"shared::components::HierarchySiblingIndex": (0),
|
||||||
priority: 0,
|
"shared::components::LevelObject": (),
|
||||||
blend_distance: 2.0,
|
"shared::components::Primitive": (shape: Box, size: (38.0, 0.5, 12.0)),
|
||||||
overrides: (
|
"shared::components::MaterialDesc": (
|
||||||
fog_density: Some(0.004),
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
fog_color: Some((r: 0.5, g: 0.55, b: 0.65, a: 1.0)),
|
base_color: (r: 0.12, g: 0.14, b: 0.16, a: 1.0),
|
||||||
),
|
metallic: 0.05,
|
||||||
fullscreen_effect: None,
|
roughness: 0.72,
|
||||||
profile: None,
|
emissive_color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||||
label: Some("Foggy courtyard"),
|
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: [],
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
4294968002: (components: {
|
2: (components: {
|
||||||
"bevy_ecs::name::Name": "Dark Exposure Volume",
|
"bevy_ecs::name::Name": "Vignette Anchor",
|
||||||
"bevy_transform::components::transform::Transform": (
|
"bevy_transform::components::transform::Transform": (
|
||||||
translation: (14.0, 2.0, 0.0),
|
translation: (-12.0, 1.2, 0.0),
|
||||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
scale: (1.0, 1.0, 1.0),
|
scale: (1.0, 1.0, 1.0),
|
||||||
),
|
),
|
||||||
"shared::components::ActorKind": PostProcessVolume,
|
"shared::components::ActorId": ("rendering-showcase-vignette-anchor"),
|
||||||
"shared::components::LevelObject": (),
|
"shared::components::ActorKind": StaticMesh,
|
||||||
"shared::components::PostProcessVolumeDesc": (
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
half_extents: (4.0, 2.5, 4.0),
|
"shared::components::HierarchySiblingIndex": (1),
|
||||||
priority: 5,
|
"shared::components::LevelObject": (),
|
||||||
blend_distance: 1.5,
|
"shared::components::Primitive": (shape: Sphere, size: (2.4, 2.4, 2.4)),
|
||||||
overrides: (
|
"shared::components::MaterialDesc": (
|
||||||
exposure_ev100: Some(9.5),
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
bloom: Some(false),
|
base_color: (r: 0.82, g: 0.18, b: 0.2, a: 1.0),
|
||||||
),
|
metallic: 0.08,
|
||||||
fullscreen_effect: None,
|
roughness: 0.38,
|
||||||
profile: Some("assets/rendering_profiles/cave_dark.ron"),
|
emissive_color: (r: 1.0, g: 0.2, b: 0.2, a: 1.0),
|
||||||
label: Some("Cave mouth"),
|
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: [],
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
4294968003: (components: {
|
3: (components: {
|
||||||
"bevy_ecs::name::Name": "Vignette FX Volume",
|
"bevy_ecs::name::Name": "Fog Anchor",
|
||||||
"bevy_transform::components::transform::Transform": (
|
"bevy_transform::components::transform::Transform": (
|
||||||
translation: (-12.0, 2.0, 0.0),
|
translation: (0.0, 1.4, 0.0),
|
||||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
rotation: (0.0, 0.258819, 0.0, 0.9659258),
|
||||||
scale: (1.0, 1.0, 1.0),
|
scale: (1.0, 1.0, 1.0),
|
||||||
),
|
),
|
||||||
"shared::components::ActorKind": PostProcessVolume,
|
"shared::components::ActorId": ("rendering-showcase-fog-anchor"),
|
||||||
"shared::components::LevelObject": (),
|
"shared::components::ActorKind": StaticMesh,
|
||||||
"shared::components::PostProcessVolumeDesc": (
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
half_extents: (5.0, 2.0, 5.0),
|
"shared::components::HierarchySiblingIndex": (2),
|
||||||
priority: 2,
|
"shared::components::LevelObject": (),
|
||||||
blend_distance: 2.0,
|
"shared::components::Primitive": (shape: Box, size: (2.8, 2.8, 2.8)),
|
||||||
overrides: (),
|
"shared::components::MaterialDesc": (
|
||||||
fullscreen_effect: Some("assets/post_fx/vignette.ron"),
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
profile: None,
|
base_color: (r: 0.1, g: 0.58, b: 0.72, a: 1.0),
|
||||||
label: Some("Vignette demo"),
|
metallic: 0.15,
|
||||||
|
roughness: 0.35,
|
||||||
|
emissive_color: (r: 0.1, g: 0.58, b: 0.72, 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: [],
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
},
|
4: (components: {
|
||||||
)
|
"bevy_ecs::name::Name": "Exposure Anchor",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (14.0, 1.3, 0.0),
|
||||||
|
rotation: (0.0, -0.1736482, 0.0, 0.9848078),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("rendering-showcase-exposure-anchor"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (3),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (3.0, 2.6, 3.0)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.92, g: 0.62, b: 0.12, a: 1.0),
|
||||||
|
metallic: 0.12,
|
||||||
|
roughness: 0.32,
|
||||||
|
emissive_color: (r: 1.0, g: 0.65, b: 0.12, 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: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
5: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Emissive Comparison Panel",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 2.6, -4.8),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("rendering-showcase-emissive-panel"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (4),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (26.0, 1.2, 0.25)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.15, g: 0.22, b: 0.28, a: 1.0),
|
||||||
|
metallic: 0.0,
|
||||||
|
roughness: 0.35,
|
||||||
|
emissive_color: (r: 0.55, g: 0.85, b: 1.0, a: 1.0),
|
||||||
|
emissive_intensity: 2500.0,
|
||||||
|
base_color_texture: None,
|
||||||
|
emissive_texture: None,
|
||||||
|
normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None,
|
||||||
|
material_asset_path: Some("assets/materials/emissive_panel.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
6: (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::ActorId": ("rendering-showcase-fog-volume"),
|
||||||
|
"shared::components::ActorKind": PostProcessVolume,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (5),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::PostProcessVolumeDesc": (
|
||||||
|
half_extents: (6.0, 3.0, 6.0),
|
||||||
|
priority: 0,
|
||||||
|
blend_distance: 2.0,
|
||||||
|
overrides: (
|
||||||
|
fog_density: Some(0.004),
|
||||||
|
fog_color: Some((r: 0.5, g: 0.55, b: 0.65, a: 1.0)),
|
||||||
|
),
|
||||||
|
fullscreen_effect: None,
|
||||||
|
profile: None,
|
||||||
|
label: Some("Foggy courtyard"),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
7: (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::ActorId": ("rendering-showcase-exposure-volume"),
|
||||||
|
"shared::components::ActorKind": PostProcessVolume,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (6),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::PostProcessVolumeDesc": (
|
||||||
|
half_extents: (4.0, 2.5, 4.0),
|
||||||
|
priority: 5,
|
||||||
|
blend_distance: 1.5,
|
||||||
|
overrides: (
|
||||||
|
exposure_ev100: Some(9.5),
|
||||||
|
bloom: Some(false),
|
||||||
|
),
|
||||||
|
fullscreen_effect: None,
|
||||||
|
profile: Some("assets/rendering_profiles/cave_dark.ron"),
|
||||||
|
label: Some("Cave mouth"),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
8: (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::ActorId": ("rendering-showcase-vignette-volume"),
|
||||||
|
"shared::components::ActorKind": PostProcessVolume,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (7),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::PostProcessVolumeDesc": (
|
||||||
|
half_extents: (5.0, 2.0, 5.0),
|
||||||
|
priority: 2,
|
||||||
|
blend_distance: 2.0,
|
||||||
|
overrides: (),
|
||||||
|
fullscreen_effect: Some("assets/post_fx/vignette.ron"),
|
||||||
|
profile: None,
|
||||||
|
label: Some("Vignette demo"),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
9: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Rendering Lab Sun",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 10.0, 5.0),
|
||||||
|
rotation: (-0.3826834, 0.0, 0.0, 0.9238795),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("rendering-showcase-sun"),
|
||||||
|
"shared::components::ActorKind": Light,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (8),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::LightDesc": (
|
||||||
|
kind: Directional,
|
||||||
|
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||||
|
intensity: 100000.0,
|
||||||
|
range: 0.0,
|
||||||
|
shadows: true,
|
||||||
|
inner_angle_deg: 25.0,
|
||||||
|
outer_angle_deg: 35.0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|||||||
332
assets/levels/samples/brush_blockout.scn.ron
Normal file
332
assets/levels/samples/brush_blockout.scn.ron
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
(schema_version: 4, resources: {}, entities: {
|
||||||
|
1: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Foundation Brush",
|
||||||
|
"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": ("sample-brush-foundation"),
|
||||||
|
"shared::components::ActorKind": Brush,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (0),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.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: Some("assets/materials/concrete.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::BrushDesc": (
|
||||||
|
kind: Additive,
|
||||||
|
faces: [
|
||||||
|
(
|
||||||
|
id: ("face:+x"),
|
||||||
|
plane: (normal: (1.0, 0.0, 0.0), distance: 5.0),
|
||||||
|
vertices: [(5.0, -0.5, -4.0), (5.0, 0.5, -4.0), (5.0, 0.5, 4.0), (5.0, -0.5, 4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-x"),
|
||||||
|
plane: (normal: (-1.0, 0.0, 0.0), distance: 5.0),
|
||||||
|
vertices: [(-5.0, -0.5, 4.0), (-5.0, 0.5, 4.0), (-5.0, 0.5, -4.0), (-5.0, -0.5, -4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+y"),
|
||||||
|
plane: (normal: (0.0, 1.0, 0.0), distance: 0.5),
|
||||||
|
vertices: [(-5.0, 0.5, -4.0), (-5.0, 0.5, 4.0), (5.0, 0.5, 4.0), (5.0, 0.5, -4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-y"),
|
||||||
|
plane: (normal: (0.0, -1.0, 0.0), distance: 0.5),
|
||||||
|
vertices: [(-5.0, -0.5, 4.0), (-5.0, -0.5, -4.0), (5.0, -0.5, -4.0), (5.0, -0.5, 4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+z"),
|
||||||
|
plane: (normal: (0.0, 0.0, 1.0), distance: 4.0),
|
||||||
|
vertices: [(5.0, -0.5, 4.0), (5.0, 0.5, 4.0), (-5.0, 0.5, 4.0), (-5.0, -0.5, 4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-z"),
|
||||||
|
plane: (normal: (0.0, 0.0, -1.0), distance: 4.0),
|
||||||
|
vertices: [(-5.0, -0.5, -4.0), (-5.0, 0.5, -4.0), (5.0, 0.5, -4.0), (5.0, -0.5, -4.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
2: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Material Face Tower",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (-2.5, 3.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-brush-material-tower"),
|
||||||
|
"shared::components::ActorKind": Brush,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (1),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.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: Some("assets/materials/concrete.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::BrushDesc": (
|
||||||
|
kind: Additive,
|
||||||
|
faces: [
|
||||||
|
(
|
||||||
|
id: ("face:+x"),
|
||||||
|
plane: (normal: (1.0, 0.0, 0.0), distance: 1.25),
|
||||||
|
vertices: [(1.25, -2.0, -1.25), (1.25, 2.0, -1.25), (1.25, 2.0, 1.25), (1.25, -2.0, 1.25)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-x"),
|
||||||
|
plane: (normal: (-1.0, 0.0, 0.0), distance: 1.25),
|
||||||
|
vertices: [(-1.25, -2.0, 1.25), (-1.25, 2.0, 1.25), (-1.25, 2.0, -1.25), (-1.25, -2.0, -1.25)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+y"),
|
||||||
|
plane: (normal: (0.0, 1.0, 0.0), distance: 2.0),
|
||||||
|
vertices: [(-1.25, 2.0, -1.25), (-1.25, 2.0, 1.25), (1.25, 2.0, 1.25), (1.25, 2.0, -1.25)],
|
||||||
|
material: Some((
|
||||||
|
asset_id: "cc2769da-c9a5-4cb3-866a-cc81d0688ee2",
|
||||||
|
sub_asset_id: "material:instance",
|
||||||
|
label: "surface_tint_instance",
|
||||||
|
source_path: Some("assets/materials/surface_tint_instance.ron"),
|
||||||
|
)),
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-y"),
|
||||||
|
plane: (normal: (0.0, -1.0, 0.0), distance: 2.0),
|
||||||
|
vertices: [(-1.25, -2.0, 1.25), (-1.25, -2.0, -1.25), (1.25, -2.0, -1.25), (1.25, -2.0, 1.25)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+z"),
|
||||||
|
plane: (normal: (0.0, 0.0, 1.0), distance: 1.25),
|
||||||
|
vertices: [(1.25, -2.0, 1.25), (1.25, 2.0, 1.25), (-1.25, 2.0, 1.25), (-1.25, -2.0, 1.25)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-z"),
|
||||||
|
plane: (normal: (0.0, 0.0, -1.0), distance: 1.25),
|
||||||
|
vertices: [(-1.25, -2.0, -1.25), (-1.25, 2.0, -1.25), (1.25, 2.0, -1.25), (1.25, -2.0, -1.25)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
3: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Subtractive Marker",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (2.5, 2.0, 0.0),
|
||||||
|
rotation: (0.0, 0.258819, 0.0, 0.9659258),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-brush-subtractive-marker"),
|
||||||
|
"shared::components::ActorKind": Brush,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (2),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.72, g: 0.14, b: 0.16, a: 1.0),
|
||||||
|
metallic: 0.05,
|
||||||
|
roughness: 0.4,
|
||||||
|
emissive_color: (r: 1.0, g: 0.25, b: 0.2, a: 1.0),
|
||||||
|
emissive_intensity: 0.0,
|
||||||
|
base_color_texture: None,
|
||||||
|
emissive_texture: None,
|
||||||
|
normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None,
|
||||||
|
material_asset_path: None,
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
"shared::components::BrushDesc": (
|
||||||
|
kind: SubtractiveMarker,
|
||||||
|
faces: [
|
||||||
|
(
|
||||||
|
id: ("face:+x"),
|
||||||
|
plane: (normal: (1.0, 0.0, 0.0), distance: 1.0),
|
||||||
|
vertices: [(1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (1.0, 1.0, 1.0), (1.0, -1.0, 1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-x"),
|
||||||
|
plane: (normal: (-1.0, 0.0, 0.0), distance: 1.0),
|
||||||
|
vertices: [(-1.0, -1.0, 1.0), (-1.0, 1.0, 1.0), (-1.0, 1.0, -1.0), (-1.0, -1.0, -1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+y"),
|
||||||
|
plane: (normal: (0.0, 1.0, 0.0), distance: 1.0),
|
||||||
|
vertices: [(-1.0, 1.0, -1.0), (-1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, -1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-y"),
|
||||||
|
plane: (normal: (0.0, -1.0, 0.0), distance: 1.0),
|
||||||
|
vertices: [(-1.0, -1.0, 1.0), (-1.0, -1.0, -1.0), (1.0, -1.0, -1.0), (1.0, -1.0, 1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:+z"),
|
||||||
|
plane: (normal: (0.0, 0.0, 1.0), distance: 1.0),
|
||||||
|
vertices: [(1.0, -1.0, 1.0), (1.0, 1.0, 1.0), (-1.0, 1.0, 1.0), (-1.0, -1.0, 1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("face:-z"),
|
||||||
|
plane: (normal: (0.0, 0.0, -1.0), distance: 1.0),
|
||||||
|
vertices: [(-1.0, -1.0, -1.0), (-1.0, 1.0, -1.0), (1.0, 1.0, -1.0), (1.0, -1.0, -1.0)],
|
||||||
|
material: None,
|
||||||
|
texture: None,
|
||||||
|
uv_offset: (0.0, 0.0),
|
||||||
|
uv_scale: (1.0, 1.0),
|
||||||
|
uv_rotation: 0.0,
|
||||||
|
smoothing_group: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
4: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Brush Lab Sun",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 8.0, 4.0),
|
||||||
|
rotation: (-0.3826834, 0.0, 0.0, 0.9238795),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-brush-sun"),
|
||||||
|
"shared::components::ActorKind": Light,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (3),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::LightDesc": (
|
||||||
|
kind: Directional,
|
||||||
|
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||||
|
intensity: 100000.0,
|
||||||
|
range: 0.0,
|
||||||
|
shadows: true,
|
||||||
|
inner_angle_deg: 25.0,
|
||||||
|
outer_angle_deg: 35.0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
189
assets/levels/samples/material_lab.scn.ron
Normal file
189
assets/levels/samples/material_lab.scn.ron
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
(schema_version: 4, resources: {}, entities: {
|
||||||
|
1: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Material Lab Floor",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, -0.2, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-floor"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (0),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (14.0, 0.4, 8.0)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
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: Some("assets/materials/concrete.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
2: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Concrete Reference",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (-4.0, 1.0, 0.0),
|
||||||
|
rotation: (0.0, 0.2164396, 0.0, 0.976296),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-concrete"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (1),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (2.0, 2.0, 2.0)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
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: Some("assets/materials/concrete.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
3: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Custom Surface Reference",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 1.2, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-custom-surface"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (2),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Sphere, size: (2.4, 2.4, 2.4)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
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: Some("assets/materials/surface_tint.ron"),
|
||||||
|
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: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
4: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Material Instance Reference",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (4.0, 1.0, 0.0),
|
||||||
|
rotation: (0.0, -0.1305262, 0.0, 0.9914449),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-instance"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (3),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (2.6, 2.0, 2.6)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
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: Some("assets/materials/surface_tint_instance.ron"),
|
||||||
|
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: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
5: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Emissive Reference Panel",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 2.3, -3.6),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-emissive-panel"),
|
||||||
|
"shared::components::ActorKind": StaticMesh,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (4),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::Primitive": (shape: Box, size: (9.0, 3.0, 0.25)),
|
||||||
|
"shared::components::MaterialDesc": (
|
||||||
|
shader: (kind: StandardLit, schema_path: None, shader_path: None),
|
||||||
|
base_color: (r: 0.15, g: 0.22, b: 0.28, a: 1.0),
|
||||||
|
metallic: 0.0,
|
||||||
|
roughness: 0.35,
|
||||||
|
emissive_color: (r: 0.55, g: 0.85, b: 1.0, a: 1.0),
|
||||||
|
emissive_intensity: 2500.0,
|
||||||
|
base_color_texture: None,
|
||||||
|
emissive_texture: None,
|
||||||
|
normal_map_texture: None,
|
||||||
|
metallic_roughness_texture: None,
|
||||||
|
material_asset_path: Some("assets/materials/emissive_panel.ron"),
|
||||||
|
parameters: [],
|
||||||
|
textures: [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
6: (components: {
|
||||||
|
"bevy_ecs::name::Name": "Material Lab Sun",
|
||||||
|
"bevy_transform::components::transform::Transform": (
|
||||||
|
translation: (0.0, 8.0, 4.0),
|
||||||
|
rotation: (-0.3826834, 0.0, 0.0, 0.9238795),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
"shared::components::ActorId": ("sample-material-sun"),
|
||||||
|
"shared::components::ActorKind": Light,
|
||||||
|
"shared::components::EditorVisibility": (visible: true),
|
||||||
|
"shared::components::HierarchySiblingIndex": (5),
|
||||||
|
"shared::components::LevelObject": (),
|
||||||
|
"shared::components::LightDesc": (
|
||||||
|
kind: Directional,
|
||||||
|
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||||
|
intensity: 100000.0,
|
||||||
|
range: 0.0,
|
||||||
|
shadows: true,
|
||||||
|
inner_angle_deg: 25.0,
|
||||||
|
outer_angle_deg: 35.0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
65
assets/samples/editor_samples.ron
Normal file
65
assets/samples/editor_samples.ron
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
(
|
||||||
|
schema_version: 1,
|
||||||
|
samples: [
|
||||||
|
(
|
||||||
|
id: "brush-blockout",
|
||||||
|
label: "Brush Blockout",
|
||||||
|
area: Brush,
|
||||||
|
scene: "assets/levels/samples/brush_blockout.scn.ron",
|
||||||
|
summary: "Convex additive brushes, a subtractive marker, and a shared face material for brush editing and CSG regression checks.",
|
||||||
|
checks: [
|
||||||
|
"valid authored BrushDesc geometry",
|
||||||
|
"shared brush-face material resolution",
|
||||||
|
"object, face, clip, and CSG selection targets",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: "material-lab",
|
||||||
|
label: "Material Lab",
|
||||||
|
area: Material,
|
||||||
|
scene: "assets/levels/samples/material_lab.scn.ron",
|
||||||
|
summary: "A compact look-development stage for shared materials, material instances, custom surfaces, and emissive output.",
|
||||||
|
checks: [
|
||||||
|
"Material and Material Instance dependency resolution",
|
||||||
|
"standard, custom, and emissive surface hydration",
|
||||||
|
"primitive material assignment and targeted drops",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: "terrain-authoring",
|
||||||
|
label: "Terrain Authoring",
|
||||||
|
area: Terrain,
|
||||||
|
scene: "assets/levels/terrain_authoring_showcase.scn.ron",
|
||||||
|
summary: "A deterministic height grid with shared material layers, normalized paint weights, chunks, and collision.",
|
||||||
|
checks: [
|
||||||
|
"TerrainDesc schema and normalized layer weights",
|
||||||
|
"shared terrain material dependency resolution",
|
||||||
|
"sculpt, paint, chunk, and collider hydration",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: "physics-placement",
|
||||||
|
label: "Physics Placement",
|
||||||
|
area: PhysicsPlacement,
|
||||||
|
scene: "assets/levels/physics_placement_showcase.scn.ron",
|
||||||
|
summary: "Three visually distinct collider cases above a static floor for transactional multi-object settling.",
|
||||||
|
checks: [
|
||||||
|
"cuboid, sphere, and capsule collider diversity",
|
||||||
|
"multi-selection settle and grouped history",
|
||||||
|
"static floor collision and cancel restoration",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: "rendering-lab",
|
||||||
|
label: "Rendering Lab",
|
||||||
|
area: Rendering,
|
||||||
|
scene: "assets/levels/rendering_showcase.scn.ron",
|
||||||
|
summary: "Visible comparison anchors for fog, exposure, rendering profiles, fullscreen effects, and emissive lighting.",
|
||||||
|
checks: [
|
||||||
|
"stable post-process volume ownership",
|
||||||
|
"rendering profile and fullscreen effect dependency resolution",
|
||||||
|
"visible fog, exposure, vignette, and emissive comparison anchors",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -6,6 +6,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
use bevy::material::OpaqueRendererMethod;
|
||||||
use bevy::mesh::MeshVertexBufferLayoutRef;
|
use bevy::mesh::MeshVertexBufferLayoutRef;
|
||||||
use bevy::pbr::{
|
use bevy::pbr::{
|
||||||
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
|
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
|
||||||
@ -393,6 +394,9 @@ fn build_terrain_layer_material(
|
|||||||
base: StandardMaterial {
|
base: StandardMaterial {
|
||||||
base_color: Color::WHITE,
|
base_color: Color::WHITE,
|
||||||
perceptual_roughness: 1.0,
|
perceptual_roughness: 1.0,
|
||||||
|
// Terrain is intentionally absent from the Solari acceleration structure until its
|
||||||
|
// ray-tracing evaluator exists, so keep the layer blend in the raster forward pass.
|
||||||
|
opaque_render_method: OpaqueRendererMethod::Forward,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
extension,
|
extension,
|
||||||
@ -1079,6 +1083,10 @@ mod tests {
|
|||||||
assert_eq!(material.extension.uniform.uv_scales.y, 10.0);
|
assert_eq!(material.extension.uniform.uv_scales.y, 10.0);
|
||||||
assert_eq!(material.extension.uniform.properties[0].w, 1.0);
|
assert_eq!(material.extension.uniform.properties[0].w, 1.0);
|
||||||
assert_eq!(material.extension.uniform.properties[1].w, 1.0);
|
assert_eq!(material.extension.uniform.properties[1].w, 1.0);
|
||||||
|
assert_eq!(
|
||||||
|
material.base.opaque_render_method,
|
||||||
|
OpaqueRendererMethod::Forward
|
||||||
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
material.extension.uniform.base_colors[0],
|
material.extension.uniform.base_colors[0],
|
||||||
material.extension.uniform.base_colors[1]
|
material.extension.uniform.base_colors[1]
|
||||||
|
|||||||
@ -27,6 +27,7 @@ pub use project::diagnostics_bundle;
|
|||||||
pub use project::launcher;
|
pub use project::launcher;
|
||||||
pub use project::native_dialog;
|
pub use project::native_dialog;
|
||||||
pub use project::project_io;
|
pub use project::project_io;
|
||||||
|
pub use project::samples;
|
||||||
pub use project::session;
|
pub use project::session;
|
||||||
pub use project::settings_ui;
|
pub use project::settings_ui;
|
||||||
pub use scene::scene_io;
|
pub use scene::scene_io;
|
||||||
@ -74,6 +75,7 @@ use play::audio_preview::AudioPreviewPlugin;
|
|||||||
use play::PlaySessionPlugin;
|
use play::PlaySessionPlugin;
|
||||||
use project::collaboration::CollaborationPlugin;
|
use project::collaboration::CollaborationPlugin;
|
||||||
use project::native_dialog::NativeDialogPlugin;
|
use project::native_dialog::NativeDialogPlugin;
|
||||||
|
use project::samples::SampleCatalogPlugin;
|
||||||
use project_io::ProjectIoPlugin;
|
use project_io::ProjectIoPlugin;
|
||||||
use render_view::RenderViewPlugin;
|
use render_view::RenderViewPlugin;
|
||||||
use scene_io::SceneIoPlugin;
|
use scene_io::SceneIoPlugin;
|
||||||
@ -98,6 +100,7 @@ impl PluginGroup for EditorPluginGroup {
|
|||||||
let group = PluginGroupBuilder::start::<Self>()
|
let group = PluginGroupBuilder::start::<Self>()
|
||||||
.add(EditorInfraPlugin)
|
.add(EditorInfraPlugin)
|
||||||
.add(ProjectIoPlugin)
|
.add(ProjectIoPlugin)
|
||||||
|
.add(SampleCatalogPlugin)
|
||||||
.add(NativeDialogPlugin)
|
.add(NativeDialogPlugin)
|
||||||
.add(scene_schema::SceneSchemaPlugin)
|
.add(scene_schema::SceneSchemaPlugin)
|
||||||
.add(net_editor::NetEditorPlugin)
|
.add(net_editor::NetEditorPlugin)
|
||||||
|
|||||||
@ -5,5 +5,6 @@ pub mod diagnostics_bundle;
|
|||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
pub mod native_dialog;
|
pub mod native_dialog;
|
||||||
pub mod project_io;
|
pub mod project_io;
|
||||||
|
pub mod samples;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod settings_ui;
|
pub mod settings_ui;
|
||||||
|
|||||||
208
crates/editor/src/project/samples.rs
Normal file
208
crates/editor/src/project/samples.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
//! Cached project sample catalog used by editor scene-opening UI.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use scene::sample_pack::{
|
||||||
|
validate_editor_sample_pack, EditorSampleEntry, EDITOR_SAMPLE_MANIFEST_PATH,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::project_io::ProjectWorkspace;
|
||||||
|
|
||||||
|
const VALIDATION_HINT: &str = "Run `cargo validate-samples --project .` for details.";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SampleCatalogEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub scene_path: PathBuf,
|
||||||
|
pub project_relative_scene: String,
|
||||||
|
pub summary: String,
|
||||||
|
pub checks: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleCatalogEntry {
|
||||||
|
pub fn hover_text(&self) -> String {
|
||||||
|
let mut lines = vec![self.summary.clone()];
|
||||||
|
if !self.checks.is_empty() {
|
||||||
|
lines.push(format!("Checks: {}", self.checks.join(", ")));
|
||||||
|
}
|
||||||
|
lines.push(self.project_relative_scene.clone());
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Clone)]
|
||||||
|
pub struct SampleCatalog {
|
||||||
|
entries: Vec<SampleCatalogEntry>,
|
||||||
|
unavailable_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SampleCatalog {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::unavailable("Sample catalog is loading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleCatalog {
|
||||||
|
pub fn entries(&self) -> &[SampleCatalogEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entry(&self, stable_id: &str) -> Option<&SampleCatalogEntry> {
|
||||||
|
self.unavailable_reason
|
||||||
|
.is_none()
|
||||||
|
.then(|| self.entries.iter().find(|entry| entry.id == stable_id))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unavailable_reason(&self) -> Option<&str> {
|
||||||
|
self.unavailable_reason.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn available_entries(entries: Vec<SampleCatalogEntry>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries,
|
||||||
|
unavailable_reason: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available(samples: Vec<EditorSampleEntry>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: samples.into_iter().map(catalog_entry).collect(),
|
||||||
|
unavailable_reason: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unavailable(reason: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: Vec::new(),
|
||||||
|
unavailable_reason: Some(reason.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn catalog_entry(entry: EditorSampleEntry) -> SampleCatalogEntry {
|
||||||
|
SampleCatalogEntry {
|
||||||
|
id: entry.id,
|
||||||
|
label: entry.label,
|
||||||
|
scene_path: PathBuf::from(&entry.scene),
|
||||||
|
project_relative_scene: entry.scene,
|
||||||
|
summary: entry.summary,
|
||||||
|
checks: entry.checks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_catalog(project_root: &Path) -> SampleCatalog {
|
||||||
|
let validation = validate_editor_sample_pack(project_root);
|
||||||
|
if !validation.is_release_ready() {
|
||||||
|
let first_error = validation
|
||||||
|
.report
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.find(|finding| finding.severity == scene::ValidationSeverity::Error)
|
||||||
|
.map(|finding| finding.message.as_str())
|
||||||
|
.unwrap_or("sample pack validation failed");
|
||||||
|
return SampleCatalog::unavailable(format!(
|
||||||
|
"Sample manifest `{EDITOR_SAMPLE_MANIFEST_PATH}` is invalid: {first_error}\n{VALIDATION_HINT}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if validation.samples.is_empty() {
|
||||||
|
return SampleCatalog::unavailable(format!(
|
||||||
|
"Sample manifest contains no scenes.\n{VALIDATION_HINT}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
SampleCatalog::available(validation.samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_sample_catalog(world: &mut World) {
|
||||||
|
let project_root = PathBuf::from(&world.resource::<ProjectWorkspace>().root);
|
||||||
|
world.insert_resource(load_catalog(&project_root));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SampleCatalogPlugin;
|
||||||
|
|
||||||
|
impl Plugin for SampleCatalogPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<SampleCatalog>()
|
||||||
|
.add_systems(Startup, load_sample_catalog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use scene::sample_pack::EditorSampleArea;
|
||||||
|
|
||||||
|
fn sample(id: &str, label: &str, scene: &str) -> EditorSampleEntry {
|
||||||
|
EditorSampleEntry {
|
||||||
|
id: id.into(),
|
||||||
|
label: label.into(),
|
||||||
|
area: EditorSampleArea::Brush,
|
||||||
|
scene: scene.into(),
|
||||||
|
summary: format!("Summary for {label}"),
|
||||||
|
checks: vec!["selection".into(), "undo".into()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn available_catalog_preserves_manifest_order_and_resolves_paths() {
|
||||||
|
let catalog = SampleCatalog::available(vec![
|
||||||
|
sample(
|
||||||
|
"terrain",
|
||||||
|
"Terrain",
|
||||||
|
"assets/levels/samples/terrain.scn.ron",
|
||||||
|
),
|
||||||
|
sample("brush", "Brush", "assets/levels/samples/brush.scn.ron"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
catalog
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.id.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["terrain", "brush"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.entry("brush").unwrap().scene_path,
|
||||||
|
PathBuf::from("assets/levels/samples/brush.scn.ron")
|
||||||
|
);
|
||||||
|
let hover = catalog.entry("brush").unwrap().hover_text();
|
||||||
|
assert!(hover.contains("Summary for Brush"));
|
||||||
|
assert!(hover.contains("Checks: selection, undo"));
|
||||||
|
assert!(hover.contains("assets/levels/samples/brush.scn.ron"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unavailable_catalog_never_resolves_entries() {
|
||||||
|
let catalog = SampleCatalog {
|
||||||
|
entries: vec![catalog_entry(sample(
|
||||||
|
"brush",
|
||||||
|
"Brush",
|
||||||
|
"assets/levels/samples/brush.scn.ron",
|
||||||
|
))],
|
||||||
|
unavailable_reason: Some("manifest invalid".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(catalog.entry("brush").is_none());
|
||||||
|
assert_eq!(catalog.unavailable_reason(), Some("manifest invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_manifest_reports_repair_command() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-missing-sample-catalog-{}",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
|
||||||
|
let catalog = load_catalog(&root);
|
||||||
|
|
||||||
|
let reason = catalog.unavailable_reason().unwrap();
|
||||||
|
assert!(reason.contains(EDITOR_SAMPLE_MANIFEST_PATH));
|
||||||
|
assert!(reason.contains("cargo validate-samples --project ."));
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@ use crate::assets::{import_external_assets, EditorAssets, IMPORTABLE_ASSET_EXTEN
|
|||||||
use crate::history::{clear_level_objects, snapshot_entity, EditorHistory};
|
use crate::history::{clear_level_objects, snapshot_entity, EditorHistory};
|
||||||
use crate::native_dialog::NativeDialogBroker;
|
use crate::native_dialog::NativeDialogBroker;
|
||||||
use crate::project::collaboration::{publish_authored_file, FileSnapshot, FileWriteIntent};
|
use crate::project::collaboration::{publish_authored_file, FileSnapshot, FileWriteIntent};
|
||||||
|
use crate::project::samples::SampleCatalog;
|
||||||
use crate::scene::recovery::{
|
use crate::scene::recovery::{
|
||||||
default_state_root, discard_recovery_snapshots, latest_recovery_snapshot,
|
default_state_root, discard_recovery_snapshots, latest_recovery_snapshot,
|
||||||
write_recovery_snapshot,
|
write_recovery_snapshot,
|
||||||
@ -49,6 +50,7 @@ pub enum SceneIoRequest {
|
|||||||
SaveRecoveryCopyAs,
|
SaveRecoveryCopyAs,
|
||||||
DiscardRecovery,
|
DiscardRecovery,
|
||||||
OpenRecent(usize),
|
OpenRecent(usize),
|
||||||
|
OpenSample(String),
|
||||||
OpenPath(PathBuf),
|
OpenPath(PathBuf),
|
||||||
SwitchTab(usize),
|
SwitchTab(usize),
|
||||||
CloseTab(usize),
|
CloseTab(usize),
|
||||||
@ -313,6 +315,7 @@ fn process_scene_io_requests(world: &mut World) {
|
|||||||
SceneIoRequest::SaveRecoveryCopyAs => save_recovery_copy_with_dialog(world),
|
SceneIoRequest::SaveRecoveryCopyAs => save_recovery_copy_with_dialog(world),
|
||||||
SceneIoRequest::DiscardRecovery => discard_recovery(world),
|
SceneIoRequest::DiscardRecovery => discard_recovery(world),
|
||||||
SceneIoRequest::OpenRecent(index) => open_recent(world, index),
|
SceneIoRequest::OpenRecent(index) => open_recent(world, index),
|
||||||
|
SceneIoRequest::OpenSample(stable_id) => open_sample(world, &stable_id),
|
||||||
SceneIoRequest::OpenPath(path) => open_path(world, path),
|
SceneIoRequest::OpenPath(path) => open_path(world, path),
|
||||||
SceneIoRequest::SwitchTab(index) => switch_scene_tab(world, index),
|
SceneIoRequest::SwitchTab(index) => switch_scene_tab(world, index),
|
||||||
SceneIoRequest::CloseTab(index) => close_scene_tab(world, index),
|
SceneIoRequest::CloseTab(index) => close_scene_tab(world, index),
|
||||||
@ -377,13 +380,72 @@ fn open_recent(world: &mut World, index: usize) -> String {
|
|||||||
open_path(world, path)
|
open_path(world, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_sample(world: &mut World, stable_id: &str) -> String {
|
||||||
|
let catalog_state = {
|
||||||
|
let catalog = world.resource::<SampleCatalog>();
|
||||||
|
(
|
||||||
|
catalog.entry(stable_id).cloned(),
|
||||||
|
catalog.unavailable_reason().map(str::to_string),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
match catalog_state {
|
||||||
|
(Some(entry), _) => open_path(world, entry.scene_path),
|
||||||
|
(None, Some(reason)) => format!("Open sample failed: {reason}"),
|
||||||
|
(None, None) => format!("Open sample failed: unknown sample ID `{stable_id}`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comparison-only key. Stored paths remain unchanged because recovery and bookmarks use them.
|
||||||
|
fn scene_path_identity(project_root: &Path, path: &Path) -> PathBuf {
|
||||||
|
let candidate = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
project_root.join(path)
|
||||||
|
};
|
||||||
|
candidate.canonicalize().unwrap_or(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scene_paths_share_identity(project_root: &Path, first: &Path, second: &Path) -> bool {
|
||||||
|
scene_path_identity(project_root, first) == scene_path_identity(project_root, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remember_persisted_scene_path(
|
||||||
|
preferences: &mut crate::project_io::UserPreferences,
|
||||||
|
project_root: &Path,
|
||||||
|
path: &Path,
|
||||||
|
) {
|
||||||
|
preferences
|
||||||
|
.recent_levels
|
||||||
|
.retain(|recent| !scene_paths_share_identity(project_root, Path::new(recent), path));
|
||||||
|
preferences
|
||||||
|
.recent_levels
|
||||||
|
.insert(0, path.display().to_string());
|
||||||
|
preferences.recent_levels.truncate(8);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_path(world: &mut World, path: PathBuf) -> String {
|
fn open_path(world: &mut World, path: PathBuf) -> String {
|
||||||
if let Some(index) = world
|
let project_root = PathBuf::from(
|
||||||
.resource::<SceneIo>()
|
world
|
||||||
.tabs
|
.resource::<crate::project_io::ProjectWorkspace>()
|
||||||
.iter()
|
.root
|
||||||
.position(|tab| tab.path.as_ref() == Some(&path))
|
.clone(),
|
||||||
{
|
);
|
||||||
|
let Ok(project_root) = project_root.canonicalize() else {
|
||||||
|
return format!(
|
||||||
|
"Open failed: could not resolve project root {}",
|
||||||
|
project_root.display()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let matching_tab = world.resource::<SceneIo>().tabs.iter().position(|tab| {
|
||||||
|
tab.path
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|tab_path| scene_paths_share_identity(&project_root, tab_path, &path))
|
||||||
|
});
|
||||||
|
if let Some(index) = matching_tab {
|
||||||
|
let established_path = world.resource::<SceneIo>().tabs[index].path.clone();
|
||||||
|
if let Some(established_path) = established_path {
|
||||||
|
remember_path(world, established_path);
|
||||||
|
}
|
||||||
return switch_scene_tab(world, index);
|
return switch_scene_tab(world, index);
|
||||||
}
|
}
|
||||||
if let Err(error) = capture_active_tab(world) {
|
if let Err(error) = capture_active_tab(world) {
|
||||||
@ -2109,6 +2171,8 @@ fn load_composed_subscenes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn finalize_scene_load(world: &mut World) {
|
fn finalize_scene_load(world: &mut World) {
|
||||||
|
shared::initialize_level_object_visibility_hierarchy(world);
|
||||||
|
|
||||||
let rendering = world
|
let rendering = world
|
||||||
.resource::<settings::ProjectSettings>()
|
.resource::<settings::ProjectSettings>()
|
||||||
.rendering
|
.rendering
|
||||||
@ -2160,15 +2224,24 @@ fn backfill_missing_actor_kinds(world: &mut World) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remember_path(world: &mut World, path: PathBuf) {
|
fn remember_path(world: &mut World, path: PathBuf) {
|
||||||
let mut io = world.resource_mut::<SceneIo>();
|
let configured_root = PathBuf::from(
|
||||||
io.recent_paths.retain(|recent| recent != &path);
|
world
|
||||||
io.recent_paths.insert(0, path.clone());
|
.resource::<crate::project_io::ProjectWorkspace>()
|
||||||
io.recent_paths.truncate(8);
|
.root
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
let project_root = configured_root.canonicalize().unwrap_or(configured_root);
|
||||||
|
{
|
||||||
|
let mut io = world.resource_mut::<SceneIo>();
|
||||||
|
io.recent_paths
|
||||||
|
.retain(|recent| !scene_paths_share_identity(&project_root, recent, &path));
|
||||||
|
io.recent_paths.insert(0, path.clone());
|
||||||
|
io.recent_paths.truncate(8);
|
||||||
|
}
|
||||||
|
|
||||||
if world.contains_resource::<crate::project_io::UserPreferences>() {
|
if world.contains_resource::<crate::project_io::UserPreferences>() {
|
||||||
let path_label = path.display().to_string();
|
|
||||||
let mut prefs = world.resource_mut::<crate::project_io::UserPreferences>();
|
let mut prefs = world.resource_mut::<crate::project_io::UserPreferences>();
|
||||||
crate::project_io::push_recent(&mut prefs.recent_levels, path_label, 8);
|
remember_persisted_scene_path(&mut prefs, &project_root, &path);
|
||||||
if let Err(error) = crate::project_io::write_user_preferences(&prefs) {
|
if let Err(error) = crate::project_io::write_user_preferences(&prefs) {
|
||||||
warn!("Failed to save editor preferences: {error}");
|
warn!("Failed to save editor preferences: {error}");
|
||||||
}
|
}
|
||||||
@ -2419,12 +2492,20 @@ mod tests {
|
|||||||
.world_mut()
|
.world_mut()
|
||||||
.spawn((
|
.spawn((
|
||||||
LevelObject,
|
LevelObject,
|
||||||
|
EditorVisibility { visible: false },
|
||||||
BrushDesc::default(),
|
BrushDesc::default(),
|
||||||
ColliderDesc::static_cuboid(Vec3::ONE),
|
ColliderDesc::static_cuboid(Vec3::ONE),
|
||||||
))
|
))
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
finalize_scene_load(app.world_mut());
|
finalize_scene_load(app.world_mut());
|
||||||
|
assert_eq!(
|
||||||
|
app.world().get::<Visibility>(actor),
|
||||||
|
Some(&Visibility::Hidden),
|
||||||
|
"scene actors must receive authored visibility before Update hydration"
|
||||||
|
);
|
||||||
|
assert!(app.world().get::<InheritedVisibility>(actor).is_some());
|
||||||
|
assert!(app.world().get::<ViewVisibility>(actor).is_some());
|
||||||
assert!(
|
assert!(
|
||||||
app.world().get::<Children>(actor).is_none(),
|
app.world().get::<Children>(actor).is_none(),
|
||||||
"scene finalization must not create collider children ahead of Update"
|
"scene finalization must not create collider children ahead of Update"
|
||||||
@ -2440,6 +2521,7 @@ mod tests {
|
|||||||
.world()
|
.world()
|
||||||
.get::<avian3d::prelude::ColliderConstructor>(first_child)
|
.get::<avian3d::prelude::ColliderConstructor>(first_child)
|
||||||
.is_some());
|
.is_some());
|
||||||
|
assert!(app.world().get::<InheritedVisibility>(actor).is_some());
|
||||||
|
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -2451,6 +2533,57 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brush_sample_parents_own_visibility_before_generated_meshes_attach() {
|
||||||
|
let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../..")
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap();
|
||||||
|
let sample_path = project_root.join("assets/levels/samples/brush_blockout.scn.ron");
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins)
|
||||||
|
.add_plugins(AssetPlugin::default())
|
||||||
|
.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default())
|
||||||
|
.init_asset::<Mesh>()
|
||||||
|
.add_plugins(shared::SharedTypesPlugin);
|
||||||
|
app.world_mut()
|
||||||
|
.insert_resource(settings::ProjectSettings::default());
|
||||||
|
app.world_mut()
|
||||||
|
.insert_resource(crate::project_io::ProjectWorkspace {
|
||||||
|
root: project_root.display().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
app.world_mut()
|
||||||
|
.insert_resource(crate::ui::hierarchy_state::HierarchyPanelState::default());
|
||||||
|
|
||||||
|
load_level(app.world_mut(), &sample_path).unwrap();
|
||||||
|
|
||||||
|
let brush_actors: Vec<Entity> = app
|
||||||
|
.world_mut()
|
||||||
|
.query_filtered::<Entity, (With<LevelObject>, With<BrushDesc>)>()
|
||||||
|
.iter(app.world())
|
||||||
|
.collect();
|
||||||
|
assert!(!brush_actors.is_empty());
|
||||||
|
for actor in &brush_actors {
|
||||||
|
assert!(app.world().get::<InheritedVisibility>(*actor).is_some());
|
||||||
|
assert!(app.world().get::<ViewVisibility>(*actor).is_some());
|
||||||
|
assert!(app.world().get::<Children>(*actor).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
for actor in brush_actors {
|
||||||
|
let generated_meshes = app
|
||||||
|
.world()
|
||||||
|
.get::<Children>(actor)
|
||||||
|
.expect("brush hydration should attach generated mesh children");
|
||||||
|
assert!(generated_meshes
|
||||||
|
.iter()
|
||||||
|
.any(|child| app.world().get::<Mesh3d>(child).is_some()));
|
||||||
|
assert!(app.world().get::<InheritedVisibility>(actor).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scene_serialization_persists_composition_without_composed_members() {
|
fn scene_serialization_persists_composition_without_composed_members() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
@ -3168,6 +3301,258 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(root).unwrap();
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_sample_id_does_not_change_scene_state() {
|
||||||
|
let known = crate::project::samples::SampleCatalogEntry {
|
||||||
|
id: "known".into(),
|
||||||
|
label: "Known Sample".into(),
|
||||||
|
scene_path: PathBuf::from("assets/levels/samples/known.scn.ron"),
|
||||||
|
project_relative_scene: "assets/levels/samples/known.scn.ron".into(),
|
||||||
|
summary: "Known fixture".into(),
|
||||||
|
checks: vec!["opens".into()],
|
||||||
|
};
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<SceneIo>();
|
||||||
|
world.insert_resource(SampleCatalog::available_entries(vec![known]));
|
||||||
|
let original_tab_id = world.resource::<SceneIo>().tabs[0].id;
|
||||||
|
|
||||||
|
let status = open_sample(&mut world, "missing");
|
||||||
|
|
||||||
|
assert_eq!(status, "Open sample failed: unknown sample ID `missing`");
|
||||||
|
let io = world.resource::<SceneIo>();
|
||||||
|
assert_eq!(io.tabs.len(), 1);
|
||||||
|
assert_eq!(io.tabs[0].id, original_tab_id);
|
||||||
|
assert_eq!(io.active_path, None);
|
||||||
|
assert!(!io.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn in_project_absolute_and_relative_scene_paths_share_one_canonical_key() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-scene-path-identity-{}",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let scene = root.join("assets/levels/samples/alias.scn.ron");
|
||||||
|
std::fs::create_dir_all(scene.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(&scene, "(schema_version: 4, resources: {}, entities: {})").unwrap();
|
||||||
|
let root = root.canonicalize().unwrap();
|
||||||
|
|
||||||
|
let expected = scene.canonicalize().unwrap();
|
||||||
|
assert_eq!(scene_path_identity(&root, &scene), expected);
|
||||||
|
assert_eq!(
|
||||||
|
scene_path_identity(&root, Path::new("assets/levels/./samples/alias.scn.ron")),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_and_absolute_scene_aliases_do_not_create_a_second_tab() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-scene-tab-alias-{}",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let relative = PathBuf::from("assets/levels/samples/alias.scn.ron");
|
||||||
|
let absolute = root.join(&relative);
|
||||||
|
std::fs::create_dir_all(absolute.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
&absolute,
|
||||||
|
"(schema_version: 4, resources: {}, entities: {})",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<SceneIo>();
|
||||||
|
world.insert_resource(crate::project_io::ProjectWorkspace {
|
||||||
|
root: root.display().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut io = world.resource_mut::<SceneIo>();
|
||||||
|
io.active_path = Some(relative.clone());
|
||||||
|
io.tabs[0].path = Some(relative.clone());
|
||||||
|
io.recent_paths = vec![absolute.clone(), relative.clone()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = open_path(&mut world, absolute.clone());
|
||||||
|
|
||||||
|
assert_eq!(status, "Scene tab is already active");
|
||||||
|
let io = world.resource::<SceneIo>();
|
||||||
|
assert_eq!(io.tabs.len(), 1);
|
||||||
|
assert_eq!(io.tabs[0].path.as_ref(), Some(&relative));
|
||||||
|
assert_eq!(io.active_path.as_ref(), Some(&relative));
|
||||||
|
assert_eq!(io.recent_paths, vec![relative.clone()]);
|
||||||
|
let mut preferences = crate::project_io::UserPreferences {
|
||||||
|
recent_levels: vec![
|
||||||
|
absolute.display().to_string(),
|
||||||
|
relative.display().to_string(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
remember_persisted_scene_path(&mut preferences, &root, &relative);
|
||||||
|
assert_eq!(
|
||||||
|
preferences.recent_levels,
|
||||||
|
vec![relative.display().to_string()]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<SceneIo>();
|
||||||
|
world.insert_resource(crate::project_io::ProjectWorkspace {
|
||||||
|
root: root.display().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut io = world.resource_mut::<SceneIo>();
|
||||||
|
io.active_path = Some(absolute.clone());
|
||||||
|
io.tabs[0].path = Some(absolute.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = open_path(&mut world, relative.clone());
|
||||||
|
|
||||||
|
assert_eq!(status, "Scene tab is already active");
|
||||||
|
let io = world.resource::<SceneIo>();
|
||||||
|
assert_eq!(io.tabs.len(), 1);
|
||||||
|
assert_eq!(io.tabs[0].path.as_ref(), Some(&absolute));
|
||||||
|
assert_eq!(io.active_path.as_ref(), Some(&absolute));
|
||||||
|
let mut preferences = crate::project_io::UserPreferences {
|
||||||
|
recent_levels: vec![
|
||||||
|
relative.display().to_string(),
|
||||||
|
absolute.display().to_string(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
remember_persisted_scene_path(&mut preferences, &root, &absolute);
|
||||||
|
assert_eq!(
|
||||||
|
preferences.recent_levels,
|
||||||
|
vec![absolute.display().to_string()]
|
||||||
|
);
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alias_open_preserves_recovery_generation_and_camera_bookmark_keys() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-scene-alias-state-{}",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
let root = root.canonicalize().unwrap();
|
||||||
|
let relative = PathBuf::from("assets/levels/samples/stateful.scn.ron");
|
||||||
|
let absolute = root.join(&relative);
|
||||||
|
std::fs::create_dir_all(absolute.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
&absolute,
|
||||||
|
"(schema_version: 4, resources: {}, entities: {})",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let state_root = root.join("editor-state");
|
||||||
|
let first_recovery =
|
||||||
|
write_recovery_snapshot(&state_root, &root, &absolute, b"first", 5).unwrap();
|
||||||
|
let bookmark = Transform::from_xyz(7.0, 11.0, 13.0);
|
||||||
|
let bookmark_key = absolute.display().to_string();
|
||||||
|
let mut bookmarks = crate::viewport::CameraBookmarks::default();
|
||||||
|
crate::viewport::save_camera_bookmark(&mut bookmarks, &bookmark_key, bookmark);
|
||||||
|
|
||||||
|
let mut world = World::new();
|
||||||
|
world.init_resource::<SceneIo>();
|
||||||
|
world.insert_resource(crate::project_io::ProjectWorkspace {
|
||||||
|
root: root.display().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
world.insert_resource(bookmarks);
|
||||||
|
{
|
||||||
|
let mut io = world.resource_mut::<SceneIo>();
|
||||||
|
io.active_path = Some(absolute.clone());
|
||||||
|
io.recovery_snapshot = Some(first_recovery.clone());
|
||||||
|
io.tabs[0].path = Some(absolute.clone());
|
||||||
|
io.tabs[0].recovery_snapshot = Some(first_recovery.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = open_path(&mut world, relative);
|
||||||
|
|
||||||
|
assert_eq!(status, "Scene tab is already active");
|
||||||
|
let active_path = world
|
||||||
|
.resource::<SceneIo>()
|
||||||
|
.active_path
|
||||||
|
.clone()
|
||||||
|
.expect("active path must remain established");
|
||||||
|
assert_eq!(active_path, absolute);
|
||||||
|
assert_eq!(
|
||||||
|
world.resource::<SceneIo>().recovery_snapshot.as_ref(),
|
||||||
|
Some(&first_recovery)
|
||||||
|
);
|
||||||
|
let second_recovery =
|
||||||
|
write_recovery_snapshot(&state_root, &root, &active_path, b"second", 5).unwrap();
|
||||||
|
assert_eq!(first_recovery.parent(), second_recovery.parent());
|
||||||
|
let active_key = active_path.display().to_string();
|
||||||
|
assert_eq!(
|
||||||
|
crate::viewport::recall_camera_bookmark(
|
||||||
|
world.resource::<crate::viewport::CameraBookmarks>(),
|
||||||
|
&active_key,
|
||||||
|
),
|
||||||
|
Some(bookmark)
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outside_project_scene_keeps_its_absolute_identity() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-scene-path-root-{}",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let outside = std::env::temp_dir().join(format!(
|
||||||
|
"blacksite-outside-scene-{}.scn.ron",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
|
std::fs::write(&outside, "(schema_version: 4, resources: {}, entities: {})").unwrap();
|
||||||
|
let root = root.canonicalize().unwrap();
|
||||||
|
let outside = outside.canonicalize().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(scene_path_identity(&root, &outside), outside);
|
||||||
|
|
||||||
|
std::fs::remove_file(outside).unwrap();
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_manifest_sample_typed_deserializes_without_a_gpu() {
|
||||||
|
let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../..")
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap();
|
||||||
|
let manifest = scene::sample_pack::load_editor_sample_manifest(&project_root).unwrap();
|
||||||
|
assert!(!manifest.samples.is_empty());
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins)
|
||||||
|
.add_plugins(AssetPlugin::default())
|
||||||
|
.add_plugins(bevy::pbr::MaterialPlugin::<StandardMaterial>::default())
|
||||||
|
.init_asset::<Mesh>()
|
||||||
|
.add_plugins(shared::SharedTypesPlugin);
|
||||||
|
|
||||||
|
for sample in manifest.samples {
|
||||||
|
let path = project_root.join(&sample.scene);
|
||||||
|
let text = std::fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|error| panic!("could not read sample `{}`: {error}", sample.id));
|
||||||
|
let document = SceneDocument::from_ron_text(&text).unwrap_or_else(|error| {
|
||||||
|
panic!(
|
||||||
|
"sample `{}` is not a valid scene document: {error}",
|
||||||
|
sample.id
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let bevy_ron = strip_schema_version(document.normalized_ron()).unwrap();
|
||||||
|
deserialize_dynamic_scene(app.world_mut(), &bevy_ron).unwrap_or_else(|error| {
|
||||||
|
panic!(
|
||||||
|
"sample `{}` failed typed deserialization: {error}",
|
||||||
|
sample.id
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scene_io_event_log_is_bounded_and_classifies_failures() {
|
fn scene_io_event_log_is_bounded_and_classifies_failures() {
|
||||||
let mut io = SceneIo::default();
|
let mut io = SceneIo::default();
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
|||||||
use egui_dock::DockState;
|
use egui_dock::DockState;
|
||||||
|
|
||||||
use crate::history::{apply_command_redo, apply_command_undo, EditorHistory};
|
use crate::history::{apply_command_redo, apply_command_undo, EditorHistory};
|
||||||
|
use crate::project::samples::{SampleCatalog, SampleCatalogEntry};
|
||||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||||
use crate::settings_ui::{open_project_settings_panel, ProjectSettingsPanel};
|
use crate::settings_ui::{open_project_settings_panel, ProjectSettingsPanel};
|
||||||
use crate::state::{EditorMode, PlayPaused};
|
use crate::state::{EditorMode, PlayPaused};
|
||||||
@ -19,6 +20,19 @@ use super::layout::reset_and_save_layout;
|
|||||||
use super::widgets::menu_item;
|
use super::widgets::menu_item;
|
||||||
use super::EditorTab;
|
use super::EditorTab;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum SampleMenuModel {
|
||||||
|
Available(Vec<SampleCatalogEntry>),
|
||||||
|
Unavailable(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_menu_model(catalog: &SampleCatalog) -> SampleMenuModel {
|
||||||
|
match catalog.unavailable_reason() {
|
||||||
|
Some(reason) => SampleMenuModel::Unavailable(reason.to_string()),
|
||||||
|
None => SampleMenuModel::Available(catalog.entries().to_vec()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn top_menu_bar(
|
pub fn top_menu_bar(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
root_ui: &mut egui::Ui,
|
root_ui: &mut egui::Ui,
|
||||||
@ -37,6 +51,25 @@ pub fn top_menu_bar(
|
|||||||
world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::Open);
|
world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::Open);
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
|
let sample_menu = sample_menu_model(world.resource::<SampleCatalog>());
|
||||||
|
ui.menu_button("Open Sample", |ui| match sample_menu {
|
||||||
|
SampleMenuModel::Available(entries) => {
|
||||||
|
for entry in entries {
|
||||||
|
if menu_item(ui, &entry.label, None, true)
|
||||||
|
.on_hover_text(entry.hover_text())
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
world.resource_mut::<SceneIo>().request =
|
||||||
|
Some(SceneIoRequest::OpenSample(entry.id));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SampleMenuModel::Unavailable(reason) => {
|
||||||
|
menu_item(ui, "Sample pack unavailable", None, false)
|
||||||
|
.on_disabled_hover_text(reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
if menu_item(ui, "Save", Some("Ctrl+S"), true).clicked() {
|
if menu_item(ui, "Save", Some("Ctrl+S"), true).clicked() {
|
||||||
world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::Save);
|
world.resource_mut::<SceneIo>().request = Some(SceneIoRequest::Save);
|
||||||
ui.close();
|
ui.close();
|
||||||
@ -287,3 +320,50 @@ pub fn top_menu_bar(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn entry(id: &str, label: &str) -> SampleCatalogEntry {
|
||||||
|
SampleCatalogEntry {
|
||||||
|
id: id.into(),
|
||||||
|
label: label.into(),
|
||||||
|
scene_path: PathBuf::from(format!("assets/levels/samples/{id}.scn.ron")),
|
||||||
|
project_relative_scene: format!("assets/levels/samples/{id}.scn.ron"),
|
||||||
|
summary: format!("{label} summary"),
|
||||||
|
checks: vec![format!("{label} check")],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sample_menu_preserves_catalog_order_and_hover_details() {
|
||||||
|
let catalog = SampleCatalog::available_entries(vec![
|
||||||
|
entry("material", "Material Lab"),
|
||||||
|
entry("terrain", "Terrain Sculpt"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let SampleMenuModel::Available(rows) = sample_menu_model(&catalog) else {
|
||||||
|
panic!("valid catalog must produce actionable menu rows");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rows.iter().map(|row| row.id.as_str()).collect::<Vec<_>>(),
|
||||||
|
vec!["material", "terrain"]
|
||||||
|
);
|
||||||
|
assert!(rows[0].hover_text().contains("Material Lab summary"));
|
||||||
|
assert!(rows[0].hover_text().contains("Material Lab check"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unavailable_catalog_produces_one_disabled_explanation() {
|
||||||
|
let catalog = SampleCatalog::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sample_menu_model(&catalog),
|
||||||
|
SampleMenuModel::Unavailable("Sample catalog is loading".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ mod migrate;
|
|||||||
pub mod navigation;
|
pub mod navigation;
|
||||||
mod prefab;
|
mod prefab;
|
||||||
mod project_validation;
|
mod project_validation;
|
||||||
|
pub mod sample_pack;
|
||||||
mod upgrade;
|
mod upgrade;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use shared::{
|
|||||||
AudioSourceDesc, BrushDesc, ColliderDesc, ColliderShapeDesc, EditorAssetRef, MaterialAsset,
|
AudioSourceDesc, BrushDesc, ColliderDesc, ColliderShapeDesc, EditorAssetRef, MaterialAsset,
|
||||||
MaterialDesc, MaterialInstanceAsset, MaterialOverride, ModelRef, PostProcessEffectAsset,
|
MaterialDesc, MaterialInstanceAsset, MaterialOverride, ModelRef, PostProcessEffectAsset,
|
||||||
PostProcessVolumeDesc, PrefabInstance, PrefabRef, RendererMaterialSet, ShaderSchemaAsset,
|
PostProcessVolumeDesc, PrefabInstance, PrefabRef, RendererMaterialSet, ShaderSchemaAsset,
|
||||||
SkinnedMeshRenderer, StaticMeshRenderer, ANIMATION_MANIFEST_SCHEMA_VERSION,
|
SkinnedMeshRenderer, StaticMeshRenderer, TerrainDesc, ANIMATION_MANIFEST_SCHEMA_VERSION,
|
||||||
AUDIO_CLIP_SUB_ASSET_ID, COMPONENT_ANIMATION_CONTROLLER_DESC, COMPONENT_SKINNED_MESH_RENDERER,
|
AUDIO_CLIP_SUB_ASSET_ID, COMPONENT_ANIMATION_CONTROLLER_DESC, COMPONENT_SKINNED_MESH_RENDERER,
|
||||||
NAVIGATION_GENERATED_ARTIFACT_DIRECTORY,
|
NAVIGATION_GENERATED_ARTIFACT_DIRECTORY,
|
||||||
};
|
};
|
||||||
@ -25,6 +25,7 @@ const MATERIAL_DESC: &str = "shared::components::MaterialDesc";
|
|||||||
const MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride";
|
const MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride";
|
||||||
const STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer";
|
const STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer";
|
||||||
const BRUSH_DESC: &str = "shared::components::BrushDesc";
|
const BRUSH_DESC: &str = "shared::components::BrushDesc";
|
||||||
|
const TERRAIN_DESC: &str = "shared::components::TerrainDesc";
|
||||||
const COLLIDER_DESC: &str = "shared::components::ColliderDesc";
|
const COLLIDER_DESC: &str = "shared::components::ColliderDesc";
|
||||||
const POST_PROCESS_VOLUME: &str = "shared::components::PostProcessVolumeDesc";
|
const POST_PROCESS_VOLUME: &str = "shared::components::PostProcessVolumeDesc";
|
||||||
const PREFAB_INSTANCE: &str = "shared::components::PrefabInstance";
|
const PREFAB_INSTANCE: &str = "shared::components::PrefabInstance";
|
||||||
@ -1905,6 +1906,53 @@ fn collect_component_references(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TERRAIN_DESC => match ron::from_str::<TerrainDesc>(&component.ron) {
|
||||||
|
Ok(terrain) => {
|
||||||
|
if let Err(error) = terrain.validate() {
|
||||||
|
report.findings.push(ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: "terrain.invalid".into(),
|
||||||
|
source_path: source_path.to_string(),
|
||||||
|
owner_actor_id: actor_id.map(str::to_string),
|
||||||
|
reference: None,
|
||||||
|
message: error,
|
||||||
|
repair: "Repair the terrain dimensions, samples, layers, and chunk settings in the Inspector."
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(material) = &terrain.base_material {
|
||||||
|
collect_terrain_material_ref(
|
||||||
|
project_root,
|
||||||
|
report,
|
||||||
|
source_path,
|
||||||
|
actor_id,
|
||||||
|
"TerrainDesc.base_material",
|
||||||
|
material,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (index, layer) in terrain.material_layers.iter().enumerate() {
|
||||||
|
if let Some(material) = &layer.material {
|
||||||
|
collect_terrain_material_ref(
|
||||||
|
project_root,
|
||||||
|
report,
|
||||||
|
source_path,
|
||||||
|
actor_id,
|
||||||
|
&format!("TerrainDesc.material_layers[{index}].material"),
|
||||||
|
material,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => report.findings.push(ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: "terrain.malformed".into(),
|
||||||
|
source_path: source_path.to_string(),
|
||||||
|
owner_actor_id: actor_id.map(str::to_string),
|
||||||
|
reference: None,
|
||||||
|
message: format!("could not parse TerrainDesc: {error}"),
|
||||||
|
repair: "Remove and recreate the Terrain component with the current editor.".into(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
COLLIDER_DESC => validate_collider(
|
COLLIDER_DESC => validate_collider(
|
||||||
project_root,
|
project_root,
|
||||||
report,
|
report,
|
||||||
@ -2736,6 +2784,64 @@ fn collect_material(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_terrain_material_ref(
|
||||||
|
project_root: &Path,
|
||||||
|
report: &mut ProjectValidationReport,
|
||||||
|
source_path: &str,
|
||||||
|
actor_id: Option<&str>,
|
||||||
|
property: &str,
|
||||||
|
reference: &EditorAssetRef,
|
||||||
|
) {
|
||||||
|
let Some(path) = reference
|
||||||
|
.source_path
|
||||||
|
.as_deref()
|
||||||
|
.filter(|path| !path.trim().is_empty())
|
||||||
|
else {
|
||||||
|
report.findings.push(ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: "terrain.material_ref_unresolved".into(),
|
||||||
|
source_path: source_path.to_string(),
|
||||||
|
owner_actor_id: actor_id.map(str::to_string),
|
||||||
|
reference: Some(property.to_string()),
|
||||||
|
message: format!("{property} has no loadable project source path"),
|
||||||
|
repair:
|
||||||
|
"Assign an existing project Material or Material Instance in the Terrain inspector."
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let path_value = Path::new(path);
|
||||||
|
if path_value.is_absolute()
|
||||||
|
|| path_value == Path::new("assets")
|
||||||
|
|| !path_value.starts_with("assets")
|
||||||
|
|| !path_value
|
||||||
|
.components()
|
||||||
|
.all(|component| matches!(component, Component::Normal(_)))
|
||||||
|
{
|
||||||
|
report.findings.push(ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: "terrain.material_ref_invalid".into(),
|
||||||
|
source_path: source_path.to_string(),
|
||||||
|
owner_actor_id: actor_id.map(str::to_string),
|
||||||
|
reference: Some(path.to_string()),
|
||||||
|
message: format!(
|
||||||
|
"{property} source path must be a normalized project-relative path below assets/"
|
||||||
|
),
|
||||||
|
repair: "Reassign the terrain layer from a project Material or Material Instance."
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add_reference(
|
||||||
|
project_root,
|
||||||
|
report,
|
||||||
|
source_path,
|
||||||
|
actor_id,
|
||||||
|
"terrain_material",
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_asset_ref(
|
fn collect_asset_ref(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
report: &mut ProjectValidationReport,
|
report: &mut ProjectValidationReport,
|
||||||
@ -3888,6 +3994,132 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(root).unwrap();
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_terrain_scene(root: &Path, terrain: &TerrainDesc) {
|
||||||
|
let terrain = ron::to_string(terrain).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
root.join("assets/levels/main.scn.ron"),
|
||||||
|
format!(
|
||||||
|
"(schema_version:4,resources:{{}},entities:{{1:(components:{{\"shared::components::ActorId\":(\"terrain-owner\"),\"{TERRAIN_DESC}\":{terrain},}})}})"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terrain_material_references_are_actor_owned_dependencies() {
|
||||||
|
let root = fixture_root();
|
||||||
|
let mut terrain = TerrainDesc::flat(3);
|
||||||
|
terrain.base_material = Some(
|
||||||
|
EditorAssetRef::new("base-id", "material:instance", "Base")
|
||||||
|
.with_source_path("assets/materials/base.ron"),
|
||||||
|
);
|
||||||
|
terrain.material_layers = vec![shared::TerrainMaterialLayer {
|
||||||
|
material: Some(
|
||||||
|
EditorAssetRef::new("layer-id", "material:source", "Layer")
|
||||||
|
.with_source_path("assets/materials/layer.ron"),
|
||||||
|
),
|
||||||
|
uv_scale: 4.0,
|
||||||
|
}];
|
||||||
|
write_terrain_scene(&root, &terrain);
|
||||||
|
|
||||||
|
let report = validate_project(&root);
|
||||||
|
let dependencies = report
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.filter(|dependency| dependency.kind == "terrain_material")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(dependencies.len(), 2, "{:?}", report.dependencies);
|
||||||
|
assert!(dependencies
|
||||||
|
.iter()
|
||||||
|
.all(|dependency| dependency.owner_actor_id.as_deref() == Some("terrain-owner")));
|
||||||
|
assert_eq!(
|
||||||
|
report
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.filter(|finding| finding.code == "asset.missing")
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terrain_material_references_require_runtime_source_paths() {
|
||||||
|
let root = fixture_root();
|
||||||
|
let mut terrain = TerrainDesc::flat(3);
|
||||||
|
terrain.base_material = Some(
|
||||||
|
EditorAssetRef::new("empty-path-id", "material:source", "Empty")
|
||||||
|
.with_source_path(" "),
|
||||||
|
);
|
||||||
|
terrain.material_layers = vec![
|
||||||
|
shared::TerrainMaterialLayer {
|
||||||
|
material: Some(EditorAssetRef::new(
|
||||||
|
"source-less-id",
|
||||||
|
"material:source",
|
||||||
|
"Source-less",
|
||||||
|
)),
|
||||||
|
uv_scale: 4.0,
|
||||||
|
},
|
||||||
|
shared::TerrainMaterialLayer {
|
||||||
|
material: Some(
|
||||||
|
EditorAssetRef::new("missing-id", "material:source", "Missing")
|
||||||
|
.with_source_path("assets/materials/missing.ron"),
|
||||||
|
),
|
||||||
|
uv_scale: 8.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
write_terrain_scene(&root, &terrain);
|
||||||
|
|
||||||
|
let report = validate_project(&root);
|
||||||
|
let unresolved = report
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.filter(|finding| finding.code == "terrain.material_ref_unresolved")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(unresolved.len(), 2, "{:?}", report.findings);
|
||||||
|
assert!(unresolved
|
||||||
|
.iter()
|
||||||
|
.all(|finding| finding.owner_actor_id.as_deref() == Some("terrain-owner")));
|
||||||
|
assert!(report.findings.iter().any(|finding| {
|
||||||
|
finding.code == "asset.missing"
|
||||||
|
&& finding.owner_actor_id.as_deref() == Some("terrain-owner")
|
||||||
|
&& finding.reference.as_deref() == Some("assets/materials/missing.ron")
|
||||||
|
}));
|
||||||
|
assert!(report.dependencies.iter().any(|dependency| {
|
||||||
|
dependency.kind == "terrain_material"
|
||||||
|
&& dependency.reference == "assets/materials/missing.ron"
|
||||||
|
}));
|
||||||
|
assert!(!report.dependencies.iter().any(|dependency| {
|
||||||
|
dependency.kind == "generated_manifest"
|
||||||
|
&& (dependency.reference.contains("empty-path-id")
|
||||||
|
|| dependency.reference.contains("source-less-id"))
|
||||||
|
}));
|
||||||
|
assert!(!report.is_release_ready());
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_terrain_descriptor_blocks_project_validation() {
|
||||||
|
let root = fixture_root();
|
||||||
|
let mut terrain = TerrainDesc::flat(3);
|
||||||
|
terrain.heights.pop();
|
||||||
|
write_terrain_scene(&root, &terrain);
|
||||||
|
|
||||||
|
let report = validate_project(&root);
|
||||||
|
let finding = report
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.find(|finding| finding.code == "terrain.invalid")
|
||||||
|
.expect("invalid terrain should block release validation");
|
||||||
|
|
||||||
|
assert_eq!(finding.owner_actor_id.as_deref(), Some("terrain-owner"));
|
||||||
|
assert!(finding.message.contains("requires 9 height samples"));
|
||||||
|
assert!(!report.is_release_ready());
|
||||||
|
std::fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project_default_level_must_be_in_the_audited_levels_tree() {
|
fn project_default_level_must_be_in_the_audited_levels_tree() {
|
||||||
let root = fixture_root();
|
let root = fixture_root();
|
||||||
|
|||||||
1101
crates/scene/src/sample_pack.rs
Normal file
1101
crates/scene/src/sample_pack.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -40,16 +40,17 @@ use static_meshes::{
|
|||||||
};
|
};
|
||||||
pub use terrain::HydratedTerrainChunk;
|
pub use terrain::HydratedTerrainChunk;
|
||||||
use terrain::{cleanup_removed_terrain, hydrate_terrain};
|
use terrain::{cleanup_removed_terrain, hydrate_terrain};
|
||||||
|
pub use visibility::initialize_level_object_visibility_hierarchy;
|
||||||
use visibility::{
|
use visibility::{
|
||||||
ensure_level_object_visibility_hierarchy, init_editor_visibility_on_spawn,
|
ensure_level_object_visibility_hierarchy, init_editor_visibility_on_spawn,
|
||||||
sync_editor_visibility, visibility_from_editor,
|
sync_editor_visibility,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authoring_component_active, AuthoringComponentStates, BrushDesc, ColliderDesc,
|
authoring_component_active, AuthoringComponentStates, BrushDesc, ColliderDesc, InspectorOrder,
|
||||||
EditorVisibility, InspectorOrder, LevelObject, MaterialDesc, MaterialOverride, Primitive,
|
LevelObject, MaterialDesc, MaterialOverride, Primitive, StaticMeshRenderer,
|
||||||
StaticMeshRenderer, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC,
|
COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC,
|
||||||
COMPONENT_MATERIAL_DESC, COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER,
|
COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Registers hydration systems in deterministic order.
|
/// Registers hydration systems in deterministic order.
|
||||||
@ -115,6 +116,8 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
world.insert_resource(StaticMeshArtifactCache::default());
|
world.insert_resource(StaticMeshArtifactCache::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialize_level_object_visibility_hierarchy(world);
|
||||||
|
|
||||||
let primitives: Vec<(Entity, Primitive)> = world
|
let primitives: Vec<(Entity, Primitive)> = world
|
||||||
.query_filtered::<(
|
.query_filtered::<(
|
||||||
Entity,
|
Entity,
|
||||||
@ -250,12 +253,6 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibility_targets: Vec<(Entity, EditorVisibility)> = world
|
|
||||||
.query_filtered::<(Entity, &EditorVisibility), With<LevelObject>>()
|
|
||||||
.iter(world)
|
|
||||||
.map(|(entity, vis)| (entity, *vis))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rendering = world
|
let rendering = world
|
||||||
.get_resource::<settings::ProjectSettings>()
|
.get_resource::<settings::ProjectSettings>()
|
||||||
.map(|s| s.rendering.clone())
|
.map(|s| s.rendering.clone())
|
||||||
@ -348,25 +345,6 @@ pub fn flush_level_object_hydration(world: &mut World) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.apply(world);
|
state.apply(world);
|
||||||
|
|
||||||
for (entity, editor) in visibility_targets {
|
|
||||||
let visibility = visibility_from_editor(editor);
|
|
||||||
if let Some(mut vis) = world.get_mut::<Visibility>(entity) {
|
|
||||||
*vis = visibility;
|
|
||||||
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
|
||||||
entity_mut.insert(visibility);
|
|
||||||
}
|
|
||||||
if world.get::<InheritedVisibility>(entity).is_none() {
|
|
||||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
|
||||||
entity_mut.insert(InheritedVisibility::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if world.get::<ViewVisibility>(entity).is_none() {
|
|
||||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
|
||||||
entity_mut.insert(ViewVisibility::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -3,6 +3,24 @@
|
|||||||
use crate::{EditorVisibility, LevelObject};
|
use crate::{EditorVisibility, LevelObject};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Establishes the runtime visibility hierarchy synchronously for all authored actors.
|
||||||
|
///
|
||||||
|
/// Scene loading can occur between scheduled hydration sets, so callers that materialize a scene
|
||||||
|
/// must run this before generated render children can be attached in the same frame.
|
||||||
|
pub fn initialize_level_object_visibility_hierarchy(world: &mut World) {
|
||||||
|
let targets: Vec<(Entity, EditorVisibility)> = world
|
||||||
|
.query_filtered::<(Entity, Option<&EditorVisibility>), With<LevelObject>>()
|
||||||
|
.iter(world)
|
||||||
|
.map(|(entity, visibility)| (entity, visibility.copied().unwrap_or_default()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (entity, editor_visibility) in targets {
|
||||||
|
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||||
|
entity_mut.insert((editor_visibility, visibility_from_editor(editor_visibility)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::type_complexity,
|
clippy::type_complexity,
|
||||||
reason = "the query mirrors Bevy's three visibility hierarchy components"
|
reason = "the query mirrors Bevy's three visibility hierarchy components"
|
||||||
@ -81,7 +99,9 @@ pub fn visibility_from_editor(editor: EditorVisibility) -> Visibility {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::ensure_level_object_visibility_hierarchy;
|
use super::{
|
||||||
|
ensure_level_object_visibility_hierarchy, initialize_level_object_visibility_hierarchy,
|
||||||
|
};
|
||||||
use crate::{EditorVisibility, LevelObject};
|
use crate::{EditorVisibility, LevelObject};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@ -102,4 +122,34 @@ mod tests {
|
|||||||
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
||||||
assert!(world.get::<ViewVisibility>(entity).is_some());
|
assert!(world.get::<ViewVisibility>(entity).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn immediate_initializer_establishes_hierarchy_before_scheduled_hydration() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let entity = world
|
||||||
|
.spawn((LevelObject, EditorVisibility { visible: false }))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
initialize_level_object_visibility_hierarchy(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(world.get::<Visibility>(entity), Some(&Visibility::Hidden));
|
||||||
|
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
||||||
|
assert!(world.get::<ViewVisibility>(entity).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn immediate_initializer_migrates_missing_authored_visibility() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let entity = world.spawn(LevelObject).id();
|
||||||
|
|
||||||
|
initialize_level_object_visibility_hierarchy(&mut world);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
world.get::<EditorVisibility>(entity),
|
||||||
|
Some(&EditorVisibility::default())
|
||||||
|
);
|
||||||
|
assert_eq!(world.get::<Visibility>(entity), Some(&Visibility::Visible));
|
||||||
|
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
||||||
|
assert!(world.get::<ViewVisibility>(entity).is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,10 @@ pub use actor::{infer_actor_kind, validate_actor, ActorValidationError};
|
|||||||
pub use animation::*;
|
pub use animation::*;
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
pub use hydration::{
|
pub use hydration::{
|
||||||
cascade_config_from_rendering, flush_level_object_hydration, material_from_desc,
|
cascade_config_from_rendering, flush_level_object_hydration,
|
||||||
strip_hydrated, strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember,
|
initialize_level_object_visibility_hierarchy, material_from_desc, strip_hydrated,
|
||||||
HydratedPrefabReady, HydratedSkinnedMeshRoot, HydratedTerrainChunk, HydrationPlugin,
|
strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember, HydratedPrefabReady,
|
||||||
PrefabHydrationBlocked,
|
HydratedSkinnedMeshRoot, HydratedTerrainChunk, HydrationPlugin, PrefabHydrationBlocked,
|
||||||
};
|
};
|
||||||
pub use material_asset::{
|
pub use material_asset::{
|
||||||
load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset,
|
load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset,
|
||||||
|
|||||||
@ -85,6 +85,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [editor/terrain.md](editor/terrain.md) | Terrain schema, inspector workflow, chunk hydration, collision, and follow-on boundaries |
|
| [editor/terrain.md](editor/terrain.md) | Terrain schema, inspector workflow, chunk hydration, collision, and follow-on boundaries |
|
||||||
| [editor/physics-placement.md](editor/physics-placement.md) | Transactional gravity placement, diagnostics, isolation, and undo workflow |
|
| [editor/physics-placement.md](editor/physics-placement.md) | Transactional gravity placement, diagnostics, isolation, and undo workflow |
|
||||||
| [editor/collider-authoring.md](editor/collider-authoring.md) | Collider shape authoring, hydration diagnostics, semantic overlays, and placement prerequisites |
|
| [editor/collider-authoring.md](editor/collider-authoring.md) | Collider shape authoring, hydration diagnostics, semantic overlays, and placement prerequisites |
|
||||||
|
| [editor/sample-regression-pack.md](editor/sample-regression-pack.md) | Five-area sample catalog, editor workflow, validation gates, and visual QA contract |
|
||||||
| [editor/evaluations/material-renderer-foundation/](editor/evaluations/material-renderer-foundation/) | Live screenshots and acceptance results for the renderer/material/component foundation |
|
| [editor/evaluations/material-renderer-foundation/](editor/evaluations/material-renderer-foundation/) | Live screenshots and acceptance results for the renderer/material/component foundation |
|
||||||
| [editor/evaluations/terrain-sculpt-tools/](editor/evaluations/terrain-sculpt-tools/) | Live screenshot and acceptance results for modal terrain sculpt tools |
|
| [editor/evaluations/terrain-sculpt-tools/](editor/evaluations/terrain-sculpt-tools/) | Live screenshot and acceptance results for modal terrain sculpt tools |
|
||||||
| [editor/evaluations/terrain-material-layers/](editor/evaluations/terrain-material-layers/) | Live screenshot and acceptance results for terrain material assignment, blending, painting, and history |
|
| [editor/evaluations/terrain-material-layers/](editor/evaluations/terrain-material-layers/) | Live screenshot and acceptance results for terrain material assignment, blending, painting, and history |
|
||||||
@ -92,6 +93,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [editor/evaluations/collider-diagnostics/](editor/evaluations/collider-diagnostics/) | Live screenshot and acceptance results for collider overlays, diagnostics, shape history, and placement preflight |
|
| [editor/evaluations/collider-diagnostics/](editor/evaluations/collider-diagnostics/) | Live screenshot and acceptance results for collider overlays, diagnostics, shape history, and placement preflight |
|
||||||
| [editor/evaluations/navigation-authoring/](editor/evaluations/navigation-authoring/) | Live screenshots and acceptance results for navigation authoring and runtime parity |
|
| [editor/evaluations/navigation-authoring/](editor/evaluations/navigation-authoring/) | Live screenshots and acceptance results for navigation authoring and runtime parity |
|
||||||
| [editor/evaluations/operator-invariants/](editor/evaluations/operator-invariants/) | Source acceptance results for production operator lifecycle, rollback, cleanup, and undo/redo invariants |
|
| [editor/evaluations/operator-invariants/](editor/evaluations/operator-invariants/) | Source acceptance results for production operator lifecycle, rollback, cleanup, and undo/redo invariants |
|
||||||
|
| [editor/evaluations/sample-regression-pack/](editor/evaluations/sample-regression-pack/) | Exact-implementation source and native acceptance evidence for the editor sample regression pack |
|
||||||
| [editor/evaluations/production-readiness/](editor/evaluations/production-readiness/) | Current versioned production gate, evidence matrix, candidate commands, soak protocol, and independent sign-off requirements |
|
| [editor/evaluations/production-readiness/](editor/evaluations/production-readiness/) | Current versioned production gate, evidence matrix, candidate commands, soak protocol, and independent sign-off requirements |
|
||||||
|
|
||||||
## Working plans (not canonical long-term)
|
## Working plans (not canonical long-term)
|
||||||
@ -116,6 +118,7 @@ Detailed milestone and feature plans live in [`.cursor/plans/`](../.cursor/plans
|
|||||||
| `material_library_and_targeted_drop_*.plan.md` | Dedicated Material Library, exact viewport slot/primitive/brush targeting, hover preview, cancel, and grouped history |
|
| `material_library_and_targeted_drop_*.plan.md` | Dedicated Material Library, exact viewport slot/primitive/brush targeting, hover preview, cancel, and grouped history |
|
||||||
| `terrain_material_layers_*.plan.md` | Terrain shared-material layers, normalized weights, blended hydration, and modal painting |
|
| `terrain_material_layers_*.plan.md` | Terrain shared-material layers, normalized weights, blended hydration, and modal painting |
|
||||||
| `operator_invariants_completion_*.plan.md` | Production operator dispatch, interruption, rollback, cleanup, and undo/redo acceptance |
|
| `operator_invariants_completion_*.plan.md` | Production operator dispatch, interruption, rollback, cleanup, and undo/redo acceptance |
|
||||||
|
| `editor_sample_regression_pack_*.plan.md` | Five-area sample manifest, editor catalog, deterministic validation, and native regression acceptance |
|
||||||
|
|
||||||
## Crate responsibilities (quick reference)
|
## Crate responsibilities (quick reference)
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,14 @@ Both the editor Diagnostics window and the `cargo validate-levels` headless comm
|
|||||||
entry point. The command supports `--project <path>` and `--json`; the editor can select the live
|
entry point. The command supports `--project <path>` and `--json`; the editor can select the live
|
||||||
owning actor for an attributed finding. Blocking errors produce a nonzero headless exit status.
|
owning actor for an attributed finding. Blocking errors produce a nonzero headless exit status.
|
||||||
|
|
||||||
|
The optional editor regression pack has one conventional manifest at
|
||||||
|
`assets/samples/editor_samples.ron`. Schema v1 contains exactly one Brush, Material, Terrain,
|
||||||
|
Physics Placement, and Rendering sample with stable metadata, project-relative scene paths, and
|
||||||
|
explicit manual checks. `cargo validate-samples` validates that catalog and combines its findings
|
||||||
|
with the authoritative project report. Projects without the manifest remain valid ordinary
|
||||||
|
projects; Blacksite CI invokes the sample command to make the complete pack mandatory for this
|
||||||
|
repository.
|
||||||
|
|
||||||
Project validation covers the project manifest and asset roots, registry IDs/sources/dependencies,
|
Project validation covers the project manifest and asset roots, registry IDs/sources/dependencies,
|
||||||
generated static mesh manifests, material and shader documents, post effects, scene and prefab
|
generated static mesh manifests, material and shader documents, post effects, scene and prefab
|
||||||
graphs, authored model/material/texture/brush/collider references, rendering profiles, and
|
graphs, authored model/material/texture/brush/collider references, rendering profiles, and
|
||||||
@ -34,6 +42,10 @@ startup entrypoint. Unknown newer scene schemas are rejected rather than down-st
|
|||||||
requirements are explicit findings; for example, Solari projects record the required forward-path
|
requirements are explicit findings; for example, Solari projects record the required forward-path
|
||||||
fallback QA when ray tracing is unavailable.
|
fallback QA when ray tracing is unavailable.
|
||||||
|
|
||||||
|
Terrain descriptors participate in the same actor-owned dependency pass. Their authored dimensions,
|
||||||
|
height and weight grids, chunk settings, base material, and material layers are validated before
|
||||||
|
hydration so a sample or shipping scene cannot hide an invalid or unresolved terrain reference.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- Headless and editor validation share result semantics and finding ownership.
|
- Headless and editor validation share result semantics and finding ownership.
|
||||||
@ -42,3 +54,7 @@ fallback QA when ray tracing is unavailable.
|
|||||||
Gitea #44 acceptance verifies the shared gate against real development output.
|
Gitea #44 acceptance verifies the shared gate against real development output.
|
||||||
- Asset formats added later must register their dependencies with this validator and extend the
|
- Asset formats added later must register their dependencies with this validator and extend the
|
||||||
valid, missing, cyclic, and incompatible project-fixture matrix.
|
valid, missing, cyclic, and incompatible project-fixture matrix.
|
||||||
|
- The sample manifest is discovery and regression metadata, not a second project-settings schema;
|
||||||
|
editor UI and headless tooling consume the same scene ordering and validation result.
|
||||||
|
- CI must hydrate Git LFS before validation because a pointer file is intentionally treated as
|
||||||
|
missing runtime content.
|
||||||
|
|||||||
@ -22,7 +22,8 @@ weights through `Mesh::ATTRIBUTE_COLOR`.
|
|||||||
`blacksite_surface` owns the terrain-specific `ExtendedMaterial` and blends each resolved layer's
|
`blacksite_surface` owns the terrain-specific `ExtendedMaterial` and blends each resolved layer's
|
||||||
albedo, tangent-space normal, metallic, and roughness inputs. Invalid or unavailable references leave
|
albedo, tangent-space normal, metallic, and roughness inputs. Invalid or unavailable references leave
|
||||||
the existing visible terrain fallback in place and emit diagnostics. Terrain remains excluded from
|
the existing visible terrain fallback in place and emit diagnostics. Terrain remains excluded from
|
||||||
Solari geometry until a matching ray-tracing evaluator exists.
|
Solari geometry until a matching ray-tracing evaluator exists, and its opaque material is therefore
|
||||||
|
forced through Bevy's forward raster path even when the project default is deferred.
|
||||||
|
|
||||||
Editor paint strokes materialize the implicit default map only when needed. Each pointer stroke is a
|
Editor paint strokes materialize the implicit default map only when needed. Each pointer stroke is a
|
||||||
single reflected `TerrainDesc` transaction; cancel restores the exact original descriptor.
|
single reflected `TerrainDesc` transaction; cancel restores the exact original descriptor.
|
||||||
|
|||||||
@ -28,6 +28,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| [terrain.md](terrain.md) | Inline height-grid terrain, chunk hydration, collision, inspector workflow, and fixtures |
|
| [terrain.md](terrain.md) | Inline height-grid terrain, chunk hydration, collision, inspector workflow, and fixtures |
|
||||||
| [physics-placement.md](physics-placement.md) | Transactional gravity placement, prerequisites, isolation, commit/cancel, and undo |
|
| [physics-placement.md](physics-placement.md) | Transactional gravity placement, prerequisites, isolation, commit/cancel, and undo |
|
||||||
| [collider-authoring.md](collider-authoring.md) | Collider shape editing, shared health diagnostics, overlays, hydration status, and placement reuse |
|
| [collider-authoring.md](collider-authoring.md) | Collider shape editing, shared health diagnostics, overlays, hydration status, and placement reuse |
|
||||||
|
| [sample-regression-pack.md](sample-regression-pack.md) | Five-area sample manifest, native workflow, visual composition, validation, and maintenance contract |
|
||||||
| [evaluations/](evaluations/) | Acceptance evidence records and native Gitea attachment publishing policy |
|
| [evaluations/](evaluations/) | Acceptance evidence records and native Gitea attachment publishing policy |
|
||||||
| [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation |
|
| [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation |
|
||||||
| [evaluations/material-library-targeted-drop/](evaluations/material-library-targeted-drop/) | Live screenshot and verification record for the docked Material Library and exact reversible surface drops |
|
| [evaluations/material-library-targeted-drop/](evaluations/material-library-targeted-drop/) | Live screenshot and verification record for the docked Material Library and exact reversible surface drops |
|
||||||
@ -40,6 +41,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| [evaluations/collider-diagnostics/](evaluations/collider-diagnostics/) | Live screenshot and verification record for semantic collider overlays, health linking, shape switching, and placement reuse |
|
| [evaluations/collider-diagnostics/](evaluations/collider-diagnostics/) | Live screenshot and verification record for semantic collider overlays, health linking, shape switching, and placement reuse |
|
||||||
| [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity |
|
| [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity |
|
||||||
| [evaluations/operator-invariants/](evaluations/operator-invariants/) | Source acceptance record for production operator lifecycle, rollback, cleanup, and undo/redo invariants |
|
| [evaluations/operator-invariants/](evaluations/operator-invariants/) | Source acceptance record for production operator lifecycle, rollback, cleanup, and undo/redo invariants |
|
||||||
|
| [evaluations/sample-regression-pack/](evaluations/sample-regression-pack/) | Exact-implementation source and native acceptance evidence for the five-area editor regression pack |
|
||||||
| [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence |
|
| [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence |
|
||||||
|
|
||||||
## Subsystems (code → doc)
|
## Subsystems (code → doc)
|
||||||
@ -67,6 +69,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
| `project/session.rs` | Versioned XDG session document, clean marker, and safe resume | session-recovery.md, ADR 0024 |
|
| `project/session.rs` | Versioned XDG session document, clean marker, and safe resume | session-recovery.md, ADR 0024 |
|
||||||
| `project/diagnostics_bundle.rs` | Privacy-bounded transactional support report export | session-recovery.md, ADR 0024 |
|
| `project/diagnostics_bundle.rs` | Privacy-bounded transactional support report export | session-recovery.md, ADR 0024 |
|
||||||
| `project/launcher.rs` | Strict project inspection, CLI activation, recent filtering, and sandbox scaffolding | project-launcher.md, ADR 0025 |
|
| `project/launcher.rs` | Strict project inspection, CLI activation, recent filtering, and sandbox scaffolding | project-launcher.md, ADR 0025 |
|
||||||
|
| `project/samples.rs` / `scene::sample_pack` / `xtask/src/validate_samples.rs` | Cached editor sample catalog, canonical pack contract, and deterministic headless release gate | sample-regression-pack.md, ADR 0028 |
|
||||||
| `ext/` | Command palette, BRP, game panel adapters | ADR 0007 |
|
| `ext/` | Command palette, BRP, game panel adapters | ADR 0007 |
|
||||||
| `ui/mod.rs` | Dock layout, unified viewport, menus | README controls |
|
| `ui/mod.rs` | Dock layout, unified viewport, menus | README controls |
|
||||||
| `ui/scene_tabs.rs` | Main-toolbar scene tabs and composition controls | multi-scene-composition.md, ADR 0026 |
|
| `ui/scene_tabs.rs` | Main-toolbar scene tabs and composition controls | multi-scene-composition.md, ADR 0026 |
|
||||||
|
|||||||
@ -11,6 +11,7 @@ Living checklist for the production editor program ([ADR 0012](../adr/0012-zero-
|
|||||||
| Optional / inferred `ActorKind` as long-term save path | Done | Schema v2 + migration; save validates |
|
| Optional / inferred `ActorKind` as long-term save path | Done | Schema v2 + migration; save validates |
|
||||||
| Hydrated components in committed `.scn.ron` | CI | `scene::validate_scene_authoring_only` + `repo_editor_scene_has_no_hydrated_components` |
|
| Hydrated components in committed `.scn.ron` | CI | `scene::validate_scene_authoring_only` + `repo_editor_scene_has_no_hydrated_components` |
|
||||||
| Production mutation without commit/cancel/failure/undo invariants | Done | Shared operator harness plus typed history projections cover palette, asset, brush, terrain, physics, grouping, lighting, material-drop, and transform paths; see [evaluation](evaluations/operator-invariants/) |
|
| Production mutation without commit/cancel/failure/undo invariants | Done | Shared operator harness plus typed history projections cover palette, asset, brush, terrain, physics, grouping, lighting, material-drop, and transform paths; see [evaluation](evaluations/operator-invariants/) |
|
||||||
|
| Representative editor coverage can silently disappear | Done | Versioned five-area manifest, typed scene gate, `validate-samples`, and native protocol; see [sample pack](sample-regression-pack.md) |
|
||||||
| Dual FBX thumbnail ad-hoc path (parallel to unified pipeline) | Partial | Phase 5 `assets/thumbnails/` refactor |
|
| Dual FBX thumbnail ad-hoc path (parallel to unified pipeline) | Partial | Phase 5 `assets/thumbnails/` refactor |
|
||||||
| `failed_keys` thumbnail cache without retry API | Partial | `asset_thumbnails.rs`; Phase 5 `ThumbnailState` |
|
| `failed_keys` thumbnail cache without retry API | Partial | `asset_thumbnails.rs`; Phase 5 `ThumbnailState` |
|
||||||
|
|
||||||
@ -31,3 +32,4 @@ Living checklist for the production editor program ([ADR 0012](../adr/0012-zero-
|
|||||||
- [ ] Save/load roundtrip: hierarchy, lights, materials unchanged
|
- [ ] Save/load roundtrip: hierarchy, lights, materials unchanged
|
||||||
- [ ] PIE 60s stop: project lighting profile unchanged unless scene overrides edited
|
- [ ] PIE 60s stop: project lighting profile unchanged unless scene overrides edited
|
||||||
- [ ] Rerun the [operator invariant matrix](operator-regression-testing.md) from the nominated release-candidate commit
|
- [ ] Rerun the [operator invariant matrix](operator-regression-testing.md) from the nominated release-candidate commit
|
||||||
|
- [ ] Open all five [sample regression pack](sample-regression-pack.md) entries through the native File menu and record exact-candidate results
|
||||||
|
|||||||
@ -4,15 +4,17 @@ Date: 2026-07-12
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
The native Wayland editor is shown with the committed
|
The native Wayland editor is shown with the 2026-07-12 revision of
|
||||||
`assets/levels/physics_placement_showcase.scn.ron` fixture. Placement Prop A, B, and C were
|
`assets/levels/physics_placement_showcase.scn.ron`. At that revision, Placement Prop A, B, and C were
|
||||||
multi-selected and released from three different heights through the hand/down-arrow action in the
|
multi-selected and released from three different heights through the hand/down-arrow action in the
|
||||||
existing horizontal viewport toolbar.
|
existing horizontal viewport toolbar. The fixture now exposes those stable actor IDs as Placement
|
||||||
|
Cuboid, Placement Sphere, and Placement Capsule; its distinct-shape rerun is tracked by the
|
||||||
|
[sample-pack evaluation](../sample-regression-pack/).
|
||||||
|
|
||||||
## Acceptance
|
## Acceptance
|
||||||
|
|
||||||
- All three props used the hydrated Avian cuboid colliders and settled on Placement Floor after
|
- In the recorded revision, all three props used hydrated Avian cuboid colliders and settled on
|
||||||
1.5 simulated seconds.
|
Placement Floor after 1.5 simulated seconds.
|
||||||
- During preview the HUD reported selection count, simulated time, and Settled state; transform
|
- During preview the HUD reported selection count, simulated time, and Settled state; transform
|
||||||
gizmos and viewport picking did not compete with the modal tool.
|
gizmos and viewport picking did not compete with the modal tool.
|
||||||
- Escape restored every starting transform and left history empty.
|
- Escape restored every starting transform and left history empty.
|
||||||
@ -25,4 +27,3 @@ existing horizontal viewport toolbar.
|
|||||||
error after the stale-binding fix.
|
error after the stale-binding fix.
|
||||||
|
|
||||||
Packaged-runtime tests remain intentionally deferred by project-owner request.
|
Packaged-runtime tests remain intentionally deferred by project-owner request.
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# Production-Readiness Acceptance Matrix
|
# Production-Readiness Acceptance Matrix
|
||||||
|
|
||||||
**Matrix version:** 0.2
|
**Matrix version:** 0.4
|
||||||
|
|
||||||
**Last audit:** 2026-07-12
|
**Last audit:** 2026-07-13
|
||||||
|
|
||||||
**Release-candidate commit:** Not nominated
|
**Release-candidate commit:** Not nominated
|
||||||
|
|
||||||
@ -30,21 +30,21 @@ another commit, a dirty worktree, or an older package do not transfer to the can
|
|||||||
|
|
||||||
| ID | Requirement | State | Current evidence and gap |
|
| ID | Requirement | State | Current evidence and gap |
|
||||||
|----|-------------|-------|--------------------------|
|
|----|-------------|-------|--------------------------|
|
||||||
| G1 | Project create/open/resume, scene authoring, autosave/recovery, hierarchy, prefab, and asset integrity pass | Partial | Project/recovery/session/multi-scene/prefab implementations are documented in [project launcher](../../project-launcher.md), [session recovery](../../session-recovery.md), [multi-scene composition](../../multi-scene-composition.md), and [prefab authoring](../../prefab-authoring.md). Collaborative file safety `#49` and non-blocking native dialogs `#52` passed live acceptance. Candidate-specific end-to-end reruns remain. |
|
| G1 | Project create/open/resume, scene authoring, autosave/recovery, hierarchy, prefab, and asset integrity pass | Fail | Project/recovery/session/multi-scene/prefab implementations are documented in [project launcher](../../project-launcher.md), [session recovery](../../session-recovery.md), [multi-scene composition](../../multi-scene-composition.md), and [prefab authoring](../../prefab-authoring.md). Collaborative file safety `#49` and non-blocking native dialogs `#52` passed live acceptance, but `#55` records unguarded native dirty shutdown and a missing clean-history savepoint. Candidate-specific end-to-end reruns also remain. |
|
||||||
| G2 | Brush, material, terrain, physics placement, animation, audio, navigation, PIE, and build/package samples pass | Fail | Animation, audio, navigation, renderer/material foundation `#51`, Material Library, targeted material drops, terrain `#22`-`#24`, physics placement `#25`, and build foundations are implemented with source/live fixtures. Brush acceptance `#37` remains incomplete. Packaged testing is owner-deferred. |
|
| G2 | Brush, material, terrain, physics placement, animation, audio, navigation, PIE, and build/package samples pass | Partial | The [five-area sample pack](../sample-regression-pack/) passed source and native acceptance on exact implementation commit `d52cc2e`, including Brush, Material, Terrain, Physics Placement, and Rendering. No release candidate is nominated, brush acceptance `#37` remains open, and combined animation/audio/navigation/PIE/build evidence is incomplete. Packaged testing is owner-deferred. |
|
||||||
| G3 | Undo/redo/cancel invariants and helper cleanup cover every production mutation path | Partial | Source implementation and focused evidence are complete in [operator regression testing](../../operator-regression-testing.md) and the [operator-invariants evaluation](../operator-invariants/); Gitea `#33` is ready to close. A clean, exact-candidate rerun is still required for `Pass`. |
|
| G3 | Undo/redo/cancel invariants and helper cleanup cover every production mutation path | Partial | Source implementation and focused evidence are complete in [operator regression testing](../../operator-regression-testing.md) and the [operator-invariants evaluation](../operator-invariants/); Gitea `#33` closed at `c55f347`. A clean, exact-candidate rerun is still required for `Pass`. |
|
||||||
| G4 | Representative project completes an eight-hour soak without unbounded memory/target growth or unrecoverable failure | Missing | No candidate soak log, resource timeline, failure ledger, or target-growth measurement exists. |
|
| G4 | Representative project completes an eight-hour soak without unbounded memory/target growth or unrecoverable failure | Missing | No candidate soak log, resource timeline, failure ledger, or target-growth measurement exists. |
|
||||||
| G5 | Cold start, scene open/save, asset refresh, common manipulation, and package-build budgets are documented and measured | Missing | Gitea `#34` is open; no ratified budgets or candidate measurement record exists. |
|
| G5 | Cold start, scene open/save, asset refresh, common manipulation, and package-build budgets are documented and measured | Missing | Gitea `#34` is open; no ratified budgets or candidate measurement record exists. |
|
||||||
| G6 | Headless content validation and CI are green from a clean checkout | Missing | [CI configuration](../../../../.github/workflows/ci.yml) exists and local source/headless checks have passed during feature work, but no clean-checkout candidate run is linked. The current Gitea server does not expose an Actions run endpoint for this repository. |
|
| G6 | Headless content validation and CI are green from a clean checkout | Missing | [CI configuration](../../../../.github/workflows/ci.yml) hydrates LFS and includes both project and sample content gates. Local implementation checks have passed, but no clean-checkout candidate run is linked. The current Gitea server does not expose an Actions run endpoint for this repository. |
|
||||||
| G7 | First-hour UX and recovery QA are signed off by someone other than the implementer | External | No independent sign-off exists. Gitea `#36` remains open; historical H1-H6 implementation-pass notes do not count. |
|
| G7 | First-hour UX and recovery QA are signed off by someone other than the implementer | External | No independent sign-off exists. Gitea `#36` remains open; historical H1-H6 implementation-pass notes do not count. |
|
||||||
| G8 | Known limitations have severity/workaround and no P0 blocker remains | Fail | Limitations are distributed across feature docs rather than one candidate ledger. The remaining open P0 implementation blocker is `#32`; `#50` cannot close while it remains. Operator invariants `#33`, renderer foundation `#51`, roadmap audit `#35`, Material Library `#16`, targeted drops `#18`, collaborative safety `#49`, and dialog responsiveness `#52` are complete. Property-block application `#53` is P1; dynamic deformed Solari geometry `#54` is a documented P2 limitation with Forward/raster fallback. |
|
| G8 | Known limitations have severity/workaround and no P0 blocker remains | Fail | Sample-pack gap `#32` has exact-implementation source/native evidence at `d52cc2e`, but guarded native shutdown and clean history savepoints remain the open P0 `#55`. A consolidated candidate limitations ledger is also missing. Deterministic imported-asset fingerprints `#56` and property-block application `#53` are P1; dynamic deformed Solari geometry `#54` is a documented P2 limitation with Forward/raster fallback. |
|
||||||
|
|
||||||
## Deliverables
|
## Deliverables
|
||||||
|
|
||||||
| ID | Deliverable | State | Evidence or next action |
|
| ID | Deliverable | State | Evidence or next action |
|
||||||
|----|-------------|-------|-------------------------|
|
|----|-------------|-------|-------------------------|
|
||||||
| D1 | Versioned acceptance matrix under `docs/editor/evaluations/` | Pass | This file, version 0.2. |
|
| D1 | Versioned acceptance matrix under `docs/editor/evaluations/` | Pass | This file, version 0.4. |
|
||||||
| D2 | Release-candidate representative project and reproducible validation commands | Missing | Expand the committed project/regression pack through `#32`, including terrain and physics-placement samples, then nominate an exact commit. |
|
| D2 | Release-candidate representative project and reproducible validation commands | Partial | The committed [sample regression pack](../sample-regression-pack/), commands, and exact-implementation native evidence exist. Nominate a release candidate and rerun from a clean LFS-hydrated checkout. |
|
||||||
| D3 | Signed milestone comment linking evidence, limitations, and exact commit | Missing | Post only after G1-G8 pass; no candidate exists yet. |
|
| D3 | Signed milestone comment linking evidence, limitations, and exact commit | Missing | Post only after G1-G8 pass; no candidate exists yet. |
|
||||||
|
|
||||||
## Workflow Coverage
|
## Workflow Coverage
|
||||||
@ -55,11 +55,13 @@ another commit, a dirty worktree, or an older package do not transfer to the can
|
|||||||
| Scene save, autosave, recovery | Implemented | Not rerun |
|
| Scene save, autosave, recovery | Implemented | Not rerun |
|
||||||
| Hierarchy parenting and prefab structure | Implemented | Not rerun |
|
| Hierarchy parenting and prefab structure | Implemented | Not rerun |
|
||||||
| Multi-scene composition | Implemented | Not rerun |
|
| Multi-scene composition | Implemented | Not rerun |
|
||||||
|
| Five-area editor sample pack | Implemented with manifest, native menu, typed scene gate, and headless validation | Exact-implementation source/native acceptance passed at `d52cc2e`; not rerun as one release candidate |
|
||||||
| Brush blockout/edit/CSG | Implemented foundation; `#37` open | Not signed off |
|
| Brush blockout/edit/CSG | Implemented foundation; `#37` open | Not signed off |
|
||||||
| Material catalog and assignment | Renderer/material foundation `#51`, Material Library, and exact targeted drops accepted; optional property-block promotion is `#53` | Not rerun as one candidate |
|
| Material catalog and assignment | Renderer/material foundation `#51`, Material Library, and exact targeted drops accepted; optional property-block promotion is `#53` | Not rerun as one candidate |
|
||||||
| Terrain authoring | `#22`-`#24` source/live acceptance complete; M3 closed | Pass; packaged-runtime acceptance owner-deferred |
|
| Rendering volumes and look development | Deterministic visible rendering fixture is in the sample pack | Not rerun as one candidate |
|
||||||
| Physics placement | `#25` source/live acceptance complete | Pass; packaged-runtime acceptance owner-deferred |
|
| Terrain authoring | `#22`-`#24` source/live acceptance complete; M3 closed | Feature-level live acceptance exists; not rerun as one candidate. Packaged-runtime acceptance owner-deferred. |
|
||||||
| Collider authoring and diagnostics | `#26` source/live acceptance complete | Pass; packaged-runtime acceptance owner-deferred |
|
| Physics placement | `#25` source/live acceptance complete | Feature-level live acceptance exists; not rerun as one candidate. Packaged-runtime acceptance owner-deferred. |
|
||||||
|
| Collider authoring and diagnostics | `#26` source/live acceptance complete | Feature-level live acceptance exists; not rerun as one candidate. Packaged-runtime acceptance owner-deferred. |
|
||||||
| Animation | Implemented | Not rerun |
|
| Animation | Implemented | Not rerun |
|
||||||
| Audio | Implemented | Not rerun |
|
| Audio | Implemented | Not rerun |
|
||||||
| Navigation | Implemented | Not rerun |
|
| Navigation | Implemented | Not rerun |
|
||||||
@ -81,6 +83,7 @@ cargo clippy --workspace --all-targets -- -D warnings
|
|||||||
cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
cargo validate-levels --project .
|
cargo validate-levels --project .
|
||||||
|
cargo validate-samples --project .
|
||||||
cargo bake-navigation --project . --check
|
cargo bake-navigation --project . --check
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -92,8 +95,9 @@ package hash verification, and packaged-runtime smoke protocol used for the cand
|
|||||||
The final candidate must run for eight continuous hours against the representative project. The
|
The final candidate must run for eight continuous hours against the representative project. The
|
||||||
record must sample process RSS, GPU memory, target-directory size, recovery generations, background
|
record must sample process RSS, GPU memory, target-directory size, recovery generations, background
|
||||||
job counts, and error/warning totals at least every five minutes. Exercise scene switching, save and
|
job counts, and error/warning totals at least every five minutes. Exercise scene switching, save and
|
||||||
recovery, asset refresh, material editing, brush/terrain/physics tools, animation/audio/navigation,
|
recovery, asset refresh, all five sample-pack scenes, material editing, brush/terrain/physics tools,
|
||||||
PIE transitions, and build UI cancellation without replacing the candidate during the run.
|
animation/audio/navigation, PIE transitions, and build UI cancellation without replacing the
|
||||||
|
candidate during the run.
|
||||||
|
|
||||||
A pass requires no unrecoverable editor failure, no lost authored work, no unbounded upward trend in
|
A pass requires no unrecoverable editor failure, no lost authored work, no unbounded upward trend in
|
||||||
steady-state resource use, bounded recovery/cache behavior, and a triaged explanation for every
|
steady-state resource use, bounded recovery/cache behavior, and a triaged explanation for every
|
||||||
|
|||||||
71
docs/editor/evaluations/sample-regression-pack/README.md
Normal file
71
docs/editor/evaluations/sample-regression-pack/README.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Sample Regression Pack Evaluation
|
||||||
|
|
||||||
|
**Date:** 2026-07-13
|
||||||
|
|
||||||
|
**Issue:** [Gitea #32](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/32)
|
||||||
|
|
||||||
|
**Implementation commit:** `d52cc2e3b72c908d192ed9e9f49306c25367599b`
|
||||||
|
|
||||||
|
**Release-candidate commit:** Not nominated
|
||||||
|
|
||||||
|
**Implementation validation:** **Pass**
|
||||||
|
|
||||||
|
**Native visual acceptance:** **Pass**
|
||||||
|
|
||||||
|
This record separates source/fixture verification from release-candidate acceptance. The exact
|
||||||
|
implementation commit above passed the five-area native protocol on a clean tracked worktree. These
|
||||||
|
results do not transfer to a later release-candidate commit.
|
||||||
|
|
||||||
|
## Source Evidence
|
||||||
|
|
||||||
|
| Check | Result | Evidence |
|
||||||
|
|-------|--------|----------|
|
||||||
|
| Manifest contract | Pass | Schema v1 contains one ordered entry for Brush, Material, Terrain, Physics Placement, and Rendering, with stable IDs and nonempty checks. |
|
||||||
|
| Authored scene fixtures | Pass | All five scene paths exist; actors use stable IDs and each scene contains its required authored feature. |
|
||||||
|
| Project validation | Pass | `cargo validate-levels --project .` reported 81 dependencies, five known nonblocking import/platform findings, and zero blocking errors on 2026-07-13. |
|
||||||
|
| Typed no-GPU deserialization | Pass | `cargo test -p editor every_manifest_sample_typed_deserializes_without_a_gpu --lib`: one passed, zero failed, 257 filtered out. |
|
||||||
|
| Sample-pack validation | Pass | `cargo validate-samples --project .`: five ordered samples, 86 dependencies, five known nonblocking findings, zero blocking errors. |
|
||||||
|
| Clean-checkout CI | Pending | The exact candidate must hydrate LFS and pass both content gates. Current Gitea does not expose an Actions run endpoint for this repository. |
|
||||||
|
|
||||||
|
## Native Protocol
|
||||||
|
|
||||||
|
Open every row through **File > Open Sample**, not through a typed filesystem path. Capture native
|
||||||
|
screenshots only after confirming the viewport is nonblank and correctly framed.
|
||||||
|
|
||||||
|
| Sample | Required native interaction and visual result | Result |
|
||||||
|
|--------|-----------------------------------------------|--------|
|
||||||
|
| Brush Blockout | Foundation, material-face tower, and red subtractive marker are distinct; enter brush edit and cancel one edit or clip preview without leaving helpers | Pass - selected one tower face in Brush Clip, observed the magenta preview, then canceled with no history or helper leak. |
|
||||||
|
| Material Lab | Concrete cube, custom sphere, instance block, and emissive panel are visible; perform one exact primitive material drop and undo | Pass - dropped `surface_tint_instance` on `Concrete Reference / Surface`; one history entry was created and Ctrl+Z restored the exact prior material. |
|
||||||
|
| Terrain Authoring | Hill chunks and two material layers render; perform and cancel one sculpt stroke and one paint stroke | Pass - Concrete/Surface Tint rendered visibly under active Solari through the forward raster boundary; both stroke cancels restored data and left history clean. |
|
||||||
|
| Physics Placement | Cuboid, sphere, and capsule settle on the floor; cancel restores all three, then commit/undo behaves as one transaction | Pass - three props settled in 2.3 seconds; cancel restored the airborne transforms, and the second run committed one `Move Selection` entry that Ctrl+Z restored. |
|
||||||
|
| Rendering Lab | Red/cyan/amber anchors and emissive strip render; camera traversal visibly changes vignette, fog, and exposure ownership | Pass - HUD ownership reached Foggy courtyard 73%/2 overrides, Cave mouth 100%/2 overrides, and Vignette demo 68% with the expected visible changes. |
|
||||||
|
|
||||||
|
For each row, inspect the status strip, Diagnostics panel, and process log for missing references,
|
||||||
|
hydration errors, render errors, helper leaks, or operator failures. Attach native images to the Gitea
|
||||||
|
issue as ordinary uploaded attachments rather than embedding repository raw URLs. A committed
|
||||||
|
original may remain beside this record through Git LFS under the shared evaluation policy.
|
||||||
|
|
||||||
|
## Native Environment And Artifacts
|
||||||
|
|
||||||
|
- Arch Linux `7.1.3-arch1-1`, Hyprland `0.55.4`, native Wayland.
|
||||||
|
- NVIDIA GeForce RTX 3080 Ti, driver `610.43.03`.
|
||||||
|
- Bevy `0.19.0`; the viewport reported the effective `Solari` path throughout the pass.
|
||||||
|
- Exact binary: `target/debug/editor --project .`, built from the implementation commit above.
|
||||||
|
- Process log: no warnings or errors across all five menu opens and interactions.
|
||||||
|
- [Gitea issue comment #1057](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/32#issuecomment-1057)
|
||||||
|
embeds full-resolution 3426x1384 PNG uploads, not repository/LFS raw links:
|
||||||
|
`blacksite-brush-d52cc2e.png`
|
||||||
|
(`sha256:3853bb51a70320b95af2cc48a1609251defc22705fb5f27fd3b1ab434f71877f`),
|
||||||
|
`blacksite-terrain-solari-d52cc2e.png`
|
||||||
|
(`sha256:8ee4e5d5c40436a5719fbec34deaef10f5bcb79748b052268781c29468e1ef7b`), and
|
||||||
|
`blacksite-rendering-vignette-d52cc2e.png`
|
||||||
|
(`sha256:67a09743f86439adf98ad2cf9b9d91041f89954aea2b8f29c29bbf10ead8c000`).
|
||||||
|
|
||||||
|
## Release Use
|
||||||
|
|
||||||
|
A future candidate may mark this evaluation fully passed only when the exact nominated commit has:
|
||||||
|
|
||||||
|
1. Passed typed scene deserialization, `validate-levels`, and `validate-samples` from a clean,
|
||||||
|
LFS-hydrated checkout.
|
||||||
|
2. Opened all five entries through the native menu and completed the interactions above.
|
||||||
|
3. Recorded the commit, machine profile, commands, exit codes, logs, and native attachments.
|
||||||
@ -30,9 +30,10 @@ and placement status agree about missing, invalid, stale, disabled, or trigger c
|
|||||||
ten-second simulated-time limit stops unstable previews while
|
ten-second simulated-time limit stops unstable previews while
|
||||||
leaving explicit commit and cancel available.
|
leaving explicit commit and cancel available.
|
||||||
|
|
||||||
`assets/levels/physics_placement_showcase.scn.ron` is the committed acceptance fixture. Select
|
`assets/levels/physics_placement_showcase.scn.ron` is the Physics Placement entry in the committed
|
||||||
**Placement Prop A**, **B**, and **C** together, start physics placement, and verify that the three
|
[sample regression pack](sample-regression-pack.md). Select **Placement Cuboid**, **Placement
|
||||||
different shapes settle on **Placement Floor** as one commit/cancel transaction.
|
Sphere**, and **Placement Capsule** together, start physics placement, and verify that the three
|
||||||
|
distinct collider cases settle on **Placement Floor** as one commit/cancel transaction.
|
||||||
|
|
||||||
The clock and transaction boundary are specified by
|
The clock and transaction boundary are specified by
|
||||||
[ADR 0041](../adr/0041-transactional-editor-physics-placement.md).
|
[ADR 0041](../adr/0041-transactional-editor-physics-placement.md).
|
||||||
|
|||||||
@ -89,7 +89,10 @@ Shipped examples: `post_fx/vignette.ron`, `post_fx/chromatic_aberration.ron`, `r
|
|||||||
|
|
||||||
## Example level
|
## Example level
|
||||||
|
|
||||||
Open `assets/levels/rendering_showcase.scn.ron` for fog, exposure, and vignette volume examples.
|
Open **File > Open Sample > Rendering Lab** for the committed rendering fixture. Its red sphere,
|
||||||
|
cyan cube, amber block, and emissive horizon make the adjacent vignette, fog, and dark-exposure zones
|
||||||
|
visually comparable while the volume actors exercise stable ownership, profile, and fullscreen-effect
|
||||||
|
references. See the [sample regression pack](sample-regression-pack.md) for the native QA protocol.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -124,6 +124,7 @@ Phased work for the in-process editor (`crates/editor`). Status reflects the cur
|
|||||||
| Thumbnails | Done | Textures via asset load; glTF albedo fast-path; FBX/untextured models via offscreen studio |
|
| Thumbnails | Done | Textures via asset load; glTF albedo fast-path; FBX/untextured models via offscreen studio |
|
||||||
| Import settings per asset | Done | Scale/collider/LOD in registry + details pane |
|
| Import settings per asset | Done | Scale/collider/LOD in registry + details pane |
|
||||||
| Prefab workflow | Done | Shared stable nested property/component/structural overrides, recursive validation, committed variant fixtures, transactional source Apply, conflict recovery, and undoable outer unpack/recursive local conversion passed headless, packaged-runtime, and live editor acceptance in Gitea #43 |
|
| Prefab workflow | Done | Shared stable nested property/component/structural overrides, recursive validation, committed variant fixtures, transactional source Apply, conflict recovery, and undoable outer unpack/recursive local conversion passed headless, packaged-runtime, and live editor acceptance in Gitea #43 |
|
||||||
|
| Curated editor regression samples | Done | Versioned five-area manifest, dedicated brush/material labs, strengthened terrain/physics/rendering fixtures, **File > Open Sample**, typed scene coverage, and `validate-samples` release gate in Gitea #32 |
|
||||||
|
|
||||||
## Phase 6 — Extensibility
|
## Phase 6 — Extensibility
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ with implementation sequencing in
|
|||||||
| Milestone | Exit condition | Status |
|
| Milestone | Exit condition | Status |
|
||||||
|-----------|----------------|--------|
|
|-----------|----------------|--------|
|
||||||
| M6 Reliability, recovery, and project workflow | Transactional save/recovery, stable sessions, project launcher, hardened hierarchy/prefabs, multi-scene composition | Implementation complete; all six scoped issues are closed after prefab #43 passed workspace, headless, packaged-runtime, and live editor acceptance |
|
| M6 Reliability, recovery, and project workflow | Transactional save/recovery, stable sessions, project launcher, hardened hierarchy/prefabs, multi-scene composition | Implementation complete; all six scoped issues are closed after prefab #43 passed workspace, headless, packaged-runtime, and live editor acceptance |
|
||||||
| M7 Content production and shipping | Build/package profiles, content release gate, animation, audio, navigation, collaborative safety | Active; #44-#48 are complete. #49 implementation and source/headless acceptance are complete, with live visual acceptance pending; final readiness gate #50 remains. Packaged acceptance is deferred until requested by the project owner. |
|
| M7 Content production and shipping | Build/package profiles, content release gate, animation, audio, navigation, collaborative safety | Active; #44-#49 are complete after source, headless, and native acceptance. Final readiness gate #50 remains blocked by open P0 shutdown/savepoint work #55; deterministic imported-asset fingerprints #56 are P1 follow-up work. Packaged acceptance is deferred until requested by the project owner. |
|
||||||
|
|
||||||
Production readiness is not inferred from feature count. Gitea
|
Production readiness is not inferred from feature count. Gitea
|
||||||
[`#50`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/50)
|
[`#50`](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/50)
|
||||||
|
|||||||
57
docs/editor/sample-regression-pack.md
Normal file
57
docs/editor/sample-regression-pack.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Editor Sample Regression Pack
|
||||||
|
|
||||||
|
The editor sample regression pack is the committed, deterministic first stop for content and native
|
||||||
|
viewport QA. Its source of truth is `assets/samples/editor_samples.ron`; manifest order is also the
|
||||||
|
order shown by **File > Open Sample** and by headless reports.
|
||||||
|
|
||||||
|
## Open And Exercise A Sample
|
||||||
|
|
||||||
|
1. Open **File > Open Sample** and choose the relevant area.
|
||||||
|
2. Confirm the scene opens in a normal scene tab and the viewport contains the expected anchors.
|
||||||
|
3. Perform the checks listed in the manifest and the table below.
|
||||||
|
4. Watch the status strip and Diagnostics panel for missing references, hydration failures, or
|
||||||
|
operator errors.
|
||||||
|
5. Use **Save As** before destructive experimentation. The committed samples are regular project
|
||||||
|
scenes, not disposable or read-only copies.
|
||||||
|
|
||||||
|
An invalid or unavailable manifest leaves an actionable disabled item in the menu. It must not
|
||||||
|
silently fall back to another scene.
|
||||||
|
|
||||||
|
## Pack Contents
|
||||||
|
|
||||||
|
| Area | Scene and visual composition | Representative checks |
|
||||||
|
|------|------------------------------|-----------------------|
|
||||||
|
| Brush | `brush_blockout.scn.ron`: broad concrete foundation, tall additive brush with a contrasting shared-material top face, red rotated subtractive marker | Select object and face targets; enter brush edit; exercise draw/clip/CSG preview and cancel; verify shared face material resolution |
|
||||||
|
| Material | `material_lab.scn.ron`: neutral floor, concrete cube, blue custom-surface sphere, rotated Material Instance block, cyan emissive backdrop | Inspect shared Material/Instance paths; target a primitive with a Material drop; compare standard, custom, and emissive hydration |
|
||||||
|
| Terrain | `terrain_authoring_showcase.scn.ron`: asymmetric 5x5 hill, four deterministic chunks, two painted shared-material layers | Sculpt and paint preview/cancel; inspect normalized weights and layer refs; verify generated selection and collider state |
|
||||||
|
| Physics Placement | `physics_placement_showcase.scn.ron`: cuboid, sphere, and tall capsule above a static floor | Multi-select all three props; settle; cancel and compare exact starting transforms; repeat, commit, undo, and redo as one group |
|
||||||
|
| Rendering | `rendering_showcase.scn.ron`: red sphere, cyan cube, amber block, emissive horizon, and three adjacent vignette/fog/exposure volumes | Move the camera through all zones; inspect active-volume ownership; verify profile/fullscreen-effect refs and visible transitions |
|
||||||
|
|
||||||
|
All authored sample actors have stable `ActorId` values. Generated hydration children are deliberately
|
||||||
|
absent from the committed scene documents.
|
||||||
|
|
||||||
|
## Headless Validation
|
||||||
|
|
||||||
|
Run both gates from the project root with Git LFS content hydrated:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo validate-levels --project .
|
||||||
|
cargo validate-samples --project .
|
||||||
|
```
|
||||||
|
|
||||||
|
`validate-levels` remains the authoritative project dependency/finding report. `validate-samples`
|
||||||
|
adds the pack contract: schema and safe paths, exactly one entry per required area, stable actor IDs,
|
||||||
|
authoring-only scene data, required area components, and nonempty operator checks. Either command
|
||||||
|
must exit nonzero on a blocking finding. Use `--json` when attaching machine-readable evidence.
|
||||||
|
|
||||||
|
## Maintenance Contract
|
||||||
|
|
||||||
|
- Keep exactly the five areas `Brush`, `Material`, `Terrain`, `PhysicsPlacement`, and `Rendering`.
|
||||||
|
- Treat manifest IDs as persistent automation keys; rename labels instead of IDs.
|
||||||
|
- Keep scene paths project-relative, inside `assets/levels/`, and free of symlink or traversal escapes.
|
||||||
|
- When a sample changes, update its manifest summary/checks and this visual-composition table in the
|
||||||
|
same change.
|
||||||
|
- Run the typed no-GPU scene test plus both validation commands before native QA.
|
||||||
|
- Record exact-commit native results under
|
||||||
|
[evaluations/sample-regression-pack/](evaluations/sample-regression-pack/). Implementation checks
|
||||||
|
from a dirty worktree do not satisfy release-candidate acceptance.
|
||||||
@ -38,10 +38,12 @@ weight to the remaining channels. Every sample stays normalized to 255, the terr
|
|||||||
or amber footprint previews the brush, and release records one undo entry. Escape or right-click
|
or amber footprint previews the brush, and release records one undo entry. Escape or right-click
|
||||||
restores the exact pre-stroke weights or closes the idle tool.
|
restores the exact pre-stroke weights or closes the idle tool.
|
||||||
|
|
||||||
`assets/levels/terrain_authoring_showcase.scn.ron` is the deterministic foundation fixture. Its 5×5
|
`assets/levels/terrain_authoring_showcase.scn.ron` is the Terrain entry in the deterministic
|
||||||
|
[sample regression pack](sample-regression-pack.md). Its 5×5
|
||||||
grid forms an asymmetric hill split into four 2×2-quad chunks. It validates chunk boundaries,
|
grid forms an asymmetric hill split into four 2×2-quad chunks. It validates chunk boundaries,
|
||||||
normals, selection through generated children, collider generation, inspector state, and save
|
normals, selection through generated children, collider generation, inspector state, and save
|
||||||
stripping without requiring external assets.
|
stripping. Height geometry is inline, while its committed shared Material and Material Instance
|
||||||
|
references deliberately validate terrain dependency resolution and layer transport.
|
||||||
|
|
||||||
## Data And Hydration
|
## Data And Hydration
|
||||||
|
|
||||||
@ -57,9 +59,10 @@ stripping without requiring external assets.
|
|||||||
fallback rather than missing geometry.
|
fallback rather than missing geometry.
|
||||||
- Generated `HydratedTerrainChunk` children own mesh, optional trimesh collider, and shadow state.
|
- Generated `HydratedTerrainChunk` children own mesh, optional trimesh collider, and shadow state.
|
||||||
They are runtime-only and never serialized as authored actors.
|
They are runtime-only and never serialized as authored actors.
|
||||||
- Foundation chunks are explicitly excluded from Solari submission and remain raster-visible while
|
- Foundation chunks are explicitly excluded from Solari submission and force their layer blend
|
||||||
Auto/Solari is active. Raster material-layer parity is shipped; matching Solari terrain evaluation
|
through Bevy's forward opaque raster path while Auto/Solari is active. Raster material-layer
|
||||||
remains explicit future work, and the editor never substitutes a semantically different proxy.
|
parity is shipped; matching Solari terrain evaluation remains explicit future work, and the editor
|
||||||
|
never substitutes a semantically different proxy.
|
||||||
|
|
||||||
The terrain foundation is recorded in [ADR 0039](../adr/0039-inline-height-grid-terrain-foundation.md),
|
The terrain foundation is recorded in [ADR 0039](../adr/0039-inline-height-grid-terrain-foundation.md),
|
||||||
and layer weight persistence/render transport in
|
and layer weight persistence/render transport in
|
||||||
|
|||||||
@ -9,6 +9,11 @@ name = "validate-levels"
|
|||||||
path = "src/validate_levels.rs"
|
path = "src/validate_levels.rs"
|
||||||
required-features = ["validate-levels"]
|
required-features = ["validate-levels"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "validate-samples"
|
||||||
|
path = "src/validate_samples.rs"
|
||||||
|
required-features = ["validate-samples"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "clean-target"
|
name = "clean-target"
|
||||||
path = "src/clean_target.rs"
|
path = "src/clean_target.rs"
|
||||||
@ -44,3 +49,4 @@ libc = "0.2"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
validate-levels = ["dep:scene", "dep:settings", "dep:shared"]
|
validate-levels = ["dep:scene", "dep:settings", "dep:shared"]
|
||||||
|
validate-samples = ["validate-levels"]
|
||||||
|
|||||||
209
xtask/src/validate_samples.rs
Normal file
209
xtask/src/validate_samples.rs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
//! Validate the canonical editor sample pack and the complete project dependency graph.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use scene::{
|
||||||
|
sample_pack::validate_editor_sample_pack, ProjectValidationReport, ValidationSeverity,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ValidateSamplesOutput<'a> {
|
||||||
|
manifest_path: &'a str,
|
||||||
|
samples: &'a [scene::sample_pack::EditorSampleEntry],
|
||||||
|
dependencies: &'a [scene::ProjectDependency],
|
||||||
|
findings: &'a [scene::ProjectValidationFinding],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut project_root = PathBuf::from(".");
|
||||||
|
let mut json = false;
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(argument) = args.next() {
|
||||||
|
match argument.as_str() {
|
||||||
|
"--json" => json = true,
|
||||||
|
"--project" => {
|
||||||
|
let Some(path) = args.next() else {
|
||||||
|
eprintln!("validate-samples: --project requires a path");
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
project_root = PathBuf::from(path);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("validate-samples: unknown argument `{argument}`");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sample_validation = validate_editor_sample_pack(&project_root);
|
||||||
|
let mut report = scene::validate_project(&project_root);
|
||||||
|
merge_reports(&mut report, &sample_validation.report);
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let output = ValidateSamplesOutput {
|
||||||
|
manifest_path: &sample_validation.manifest_path,
|
||||||
|
samples: &sample_validation.samples,
|
||||||
|
dependencies: &report.dependencies,
|
||||||
|
findings: &report.findings,
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&output).expect("sample validation report must serialize")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for sample in &sample_validation.samples {
|
||||||
|
println!(
|
||||||
|
"sample: {} [{}] {} ({})",
|
||||||
|
sample.area, sample.id, sample.label, sample.scene
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for finding in &report.findings {
|
||||||
|
let actor = finding
|
||||||
|
.owner_actor_id
|
||||||
|
.as_deref()
|
||||||
|
.map(|actor| format!(" actor={actor}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
eprintln!(
|
||||||
|
"{:?} [{}] {}{}: {}",
|
||||||
|
finding.severity, finding.code, finding.source_path, actor, finding.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"validate-samples: {} samples, {} dependencies, {} findings, {} blocking errors",
|
||||||
|
sample_validation.samples.len(),
|
||||||
|
report.dependencies.len(),
|
||||||
|
report.findings.len(),
|
||||||
|
report.blocking_error_count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.is_release_ready() {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_reports(target: &mut ProjectValidationReport, source: &ProjectValidationReport) {
|
||||||
|
target
|
||||||
|
.dependencies
|
||||||
|
.extend(source.dependencies.iter().cloned());
|
||||||
|
target.findings.extend(source.findings.iter().cloned());
|
||||||
|
target.dependencies.sort_by(|left, right| {
|
||||||
|
(
|
||||||
|
&left.owner_path,
|
||||||
|
&left.owner_actor_id,
|
||||||
|
&left.kind,
|
||||||
|
&left.reference,
|
||||||
|
)
|
||||||
|
.cmp(&(
|
||||||
|
&right.owner_path,
|
||||||
|
&right.owner_actor_id,
|
||||||
|
&right.kind,
|
||||||
|
&right.reference,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
target.dependencies.dedup();
|
||||||
|
target.findings.sort_by(|left, right| {
|
||||||
|
(
|
||||||
|
&left.source_path,
|
||||||
|
&left.owner_actor_id,
|
||||||
|
&left.code,
|
||||||
|
&left.reference,
|
||||||
|
&left.message,
|
||||||
|
severity_rank(left.severity),
|
||||||
|
&left.repair,
|
||||||
|
)
|
||||||
|
.cmp(&(
|
||||||
|
&right.source_path,
|
||||||
|
&right.owner_actor_id,
|
||||||
|
&right.code,
|
||||||
|
&right.reference,
|
||||||
|
&right.message,
|
||||||
|
severity_rank(right.severity),
|
||||||
|
&right.repair,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
target.findings.dedup();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn severity_rank(severity: ValidationSeverity) -> u8 {
|
||||||
|
match severity {
|
||||||
|
ValidationSeverity::Error => 0,
|
||||||
|
ValidationSeverity::Warning => 1,
|
||||||
|
ValidationSeverity::Info => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use scene::{ProjectDependency, ProjectValidationFinding};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merged_report_is_sorted_and_deduplicated() {
|
||||||
|
let dependency = ProjectDependency {
|
||||||
|
owner_path: "assets/samples/editor_samples.ron".into(),
|
||||||
|
owner_actor_id: None,
|
||||||
|
kind: "editor_sample_scene".into(),
|
||||||
|
reference: "assets/levels/samples/brush.scn.ron".into(),
|
||||||
|
};
|
||||||
|
let finding = ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: "sample_pack.scene_missing".into(),
|
||||||
|
source_path: "assets/samples/editor_samples.ron".into(),
|
||||||
|
owner_actor_id: None,
|
||||||
|
reference: Some("assets/levels/samples/brush.scn.ron".into()),
|
||||||
|
message: "missing".into(),
|
||||||
|
repair: "restore".into(),
|
||||||
|
};
|
||||||
|
let source = ProjectValidationReport {
|
||||||
|
dependencies: vec![dependency.clone()],
|
||||||
|
findings: vec![finding.clone()],
|
||||||
|
};
|
||||||
|
let mut target = source.clone();
|
||||||
|
|
||||||
|
merge_reports(&mut target, &source);
|
||||||
|
|
||||||
|
assert_eq!(target.dependencies, vec![dependency]);
|
||||||
|
assert_eq!(target.findings, vec![finding]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merged_json_is_deterministic_across_input_order() {
|
||||||
|
let dependency = |reference: &str| ProjectDependency {
|
||||||
|
owner_path: "assets/samples/editor_samples.ron".into(),
|
||||||
|
owner_actor_id: None,
|
||||||
|
kind: "editor_sample_scene".into(),
|
||||||
|
reference: reference.into(),
|
||||||
|
};
|
||||||
|
let finding = |code: &str, repair: &str| ProjectValidationFinding {
|
||||||
|
severity: ValidationSeverity::Error,
|
||||||
|
code: code.into(),
|
||||||
|
source_path: "assets/samples/editor_samples.ron".into(),
|
||||||
|
owner_actor_id: None,
|
||||||
|
reference: Some(code.into()),
|
||||||
|
message: code.into(),
|
||||||
|
repair: repair.into(),
|
||||||
|
};
|
||||||
|
let first = ProjectValidationReport {
|
||||||
|
dependencies: vec![dependency("assets/levels/z.scn.ron")],
|
||||||
|
findings: vec![finding("sample_pack.same", "repair-z")],
|
||||||
|
};
|
||||||
|
let second = ProjectValidationReport {
|
||||||
|
dependencies: vec![dependency("assets/levels/a.scn.ron")],
|
||||||
|
findings: vec![finding("sample_pack.same", "repair-a")],
|
||||||
|
};
|
||||||
|
let mut left = first.clone();
|
||||||
|
merge_reports(&mut left, &second);
|
||||||
|
let mut right = second;
|
||||||
|
merge_reports(&mut right, &first);
|
||||||
|
|
||||||
|
let left_json = serde_json::to_string(&left).unwrap();
|
||||||
|
let right_json = serde_json::to_string(&right).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(left, right);
|
||||||
|
assert_eq!(left_json, right_json);
|
||||||
|
assert_eq!(left.findings[0].repair, "repair-a");
|
||||||
|
assert_eq!(left.findings[1].repair, "repair-z");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user