Files
Faset_Engine/tests/editor_blender_reimport_probe.cpp

145 lines
7.5 KiB
C++

#include <chrono>
#include <faset/core/io.hpp>
#include <faset/editor/editor_ui.hpp>
#include <iostream>
#include <thread>
using namespace faset;
namespace fs = std::filesystem;
namespace {
void check(bool value, const char* message) {
if (!value)
throw std::runtime_error(message);
}
Json geometry(const render::DrawItem& draw) {
Json positions = Json::array();
for (const auto& vertex : draw.mesh->vertices)
positions.push_back(vertex.position);
return positions;
}
int run(int argc, char** argv) {
try {
check(argc == 3, "Usage: faset_blender_editor_probe FIXTURE_DIRECTORY NEW_PROJECT");
const auto fixture = path_from_utf8(argv[1]), project = path_from_utf8(argv[2]);
check(!fs::exists(project), "Use a new project directory for the Blender UI probe");
const auto source = project / "Assets/door";
fs::create_directories(source);
editor::Session session({project, path_from_utf8(FASET_TEST_ENGINE), project});
render::Renderer renderer({.width = 1280,
.height = 900,
.title = "Blender reimport",
.headless = true,
.validation = true});
editor::EditorUI ui(session, renderer,
path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf",
path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json");
auto import = [&](const char* stage) {
fs::copy(fixture / stage, source,
fs::copy_options::recursive | fs::copy_options::overwrite_existing);
const auto job = session.commands()
.call("faset_import", {{"path", "Assets/door/manifest.json"}})
.at("job");
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (std::chrono::steady_clock::now() < deadline) {
ui.frame({});
auto result = session.commands().call("faset_job", {{"id", job}});
if (result.at("state") != "queued" && result.at("state") != "running")
return result;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("Blender import timed out");
};
const auto initial = import("initial");
check(initial.at("state") == "succeeded", "Initial Blender import failed");
const auto id = initial.at("result").at("asset_id");
const auto document = ui.current_document();
auto current = session.authoring().query(document);
Json operations = Json::array();
for (int index = 0; index < 2; ++index) {
auto entity = authoring::make_entity(session.authoring().schemas(), "Placed door");
entity["components"][0]["fields"]["position"] = {index ? 3 : -3, 0, 0};
entity["components"].push_back(
{{"id", new_id()},
{"type", "faset.mesh"},
{"version", 1},
{"fields",
{{"asset", id},
{"primitive", "asset"},
{"color", index ? Json{0.5, 0.8, 0.3, 1} : Json{0.3, 0.5, 0.9, 1}}}}});
// Missing gameplay packages stay opaque; reimport must not remove them.
entity["components"].push_back({{"id", new_id()},
{"type", "project.door"},
{"version", 2},
{"fields", {{"locked", index == 0}, {"speed", 2.5}}}});
auto body = session.authoring().schemas().default_fields("faset.rigid_body_3d");
body["body_type"] = "static";
body["friction"] = index ? 0.75 : 0.25;
entity["components"].push_back({{"id", new_id()},
{"type", "faset.rigid_body_3d"},
{"version", 1},
{"fields", body}});
operations.push_back({{"op", "entity.create"}, {"entity", entity}});
}
session.authoring().transact(document, current.at("revision"), operations);
current = session.authoring().query(document);
ui.frame({});
check(ui.snapshot().draws.size() >= 2, "Both placed Blender meshes must render");
const auto first = ui.snapshot().draws[0], second = ui.snapshot().draws[1];
check(first.mesh == second.mesh && first.model != second.model,
"Two independent placements must share the imported mesh");
renderer.render(ui.snapshot());
renderer.capture(project / "initial.ppm");
const auto renamed = import("renamed");
check(renamed.at("state") == "succeeded", "Renamed Blender import failed");
// Exercise the real editor's generation watcher, without manually clearing SceneView.
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
ui.frame({});
const auto first_after = ui.snapshot().draws[0], second_after = ui.snapshot().draws[1];
check(geometry(first) != geometry(first_after) &&
geometry(second) != geometry(second_after),
"Both live Editor instances must receive changed Blender geometry");
check(first_after.mesh == second_after.mesh, "New instances share one cooked mesh");
check(first.model == first_after.model && second.model == second_after.model &&
first.color == first_after.color && second.color == second_after.color,
"Reimport must preserve each instance's placement and tint");
check(session.authoring().query(document) == current,
"Reimport must preserve authoring IDs, gameplay fields and revision");
renderer.render(ui.snapshot());
renderer.capture(project / "renamed.ppm");
const auto removed = import("removed");
check(removed.at("state") == "conflict", "Removed Blender output must conflict");
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
ui.frame({});
check(geometry(ui.snapshot().draws[0]) == geometry(first_after) &&
geometry(ui.snapshot().draws[1]) == geometry(second_after),
"Conflict must keep both last-good visible meshes");
check(session.authoring().query(document) == current,
"Import conflict must preserve authoring");
check(renderer.stats().validation_errors == 0, "Vulkan validation failed");
atomic_write_json(project / "probe.json",
{{"status", "passed"},
{"instances", 2},
{"live_geometry_updated", true},
{"placement_and_tint_preserved", true},
{"opaque_gameplay_preserved", true},
{"physics_fields_preserved", true},
{"conflict_keeps_visible_generation", true},
{"validation_enabled", renderer.stats().validation_enabled},
{"validation_errors", renderer.stats().validation_errors}});
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
} // namespace
#ifdef _WIN32
int wmain(int argc, wchar_t** argv) {
return run_utf8_main(argc, argv, run);
}
#else
int main(int argc, char** argv) {
return run(argc, argv);
}
#endif