Remodelating system

This commit is contained in:
Chewico 2026-01-06 14:20:38 +01:00
parent ba42be46ba
commit 47871fa1dc
21 changed files with 240 additions and 71 deletions

View File

@ -6,15 +6,15 @@ namespace Deer {
std::shared_ptr<spdlog::logger> Log::scriptLogger; std::shared_ptr<spdlog::logger> Log::scriptLogger;
std::shared_ptr<spdlog::logger> Log::EditorEngineLogger; std::shared_ptr<spdlog::logger> Log::EditorEngineLogger;
void Log::init() void Log::init() {
{ // spdlog::set_pattern("%^[%T] %n: %v%$");
spdlog::set_pattern("%^[%T] %n: %v%$"); spdlog::set_pattern("%n: %v%$");
coreLogger = spdlog::stdout_color_mt("Core"); coreLogger = spdlog::stdout_color_mt("Core");
clientLogger = spdlog::stdout_color_mt("Client"); clientLogger = spdlog::stdout_color_mt("Client");
scriptLogger = spdlog::stdout_color_mt("Script"); scriptLogger = spdlog::stdout_color_mt("Script");
EditorEngineLogger = spdlog::stdout_color_mt("UI Engine"); EditorEngineLogger = spdlog::stdout_color_mt("UI Engine");
coreLogger->set_level(spdlog::level::level_enum::trace); coreLogger->set_level(spdlog::level::level_enum::trace);
clientLogger->set_level(spdlog::level::level_enum::trace); clientLogger->set_level(spdlog::level::level_enum::trace);
scriptLogger->set_level(spdlog::level::level_enum::trace); scriptLogger->set_level(spdlog::level::level_enum::trace);
@ -30,8 +30,7 @@ namespace Deer {
spdlog::drop_all(); spdlog::drop_all();
} }
void Log::coreTrace(const char* msg) void Log::coreTrace(const char* msg) {
{ // coreLogger->trace(msg);
//coreLogger->trace(msg);
} }
} } // namespace Deer

View File

@ -61,9 +61,6 @@ namespace Deer {
voxelData.name.c_str()); voxelData.name.c_str());
continue; continue;
} }
// DEER_CORE_TRACE(" {0} - {1}",
// voxel.filename().generic_string().c_str(),
// voxelData.name);
uint32_t id = voxelsInfo.size(); uint32_t id = voxelsInfo.size();

View File

@ -56,10 +56,6 @@ namespace Deer {
continue; continue;
} }
// DEER_CORE_TRACE(" {0} - {1}",
// voxelAspectPath.filename().generic_string().c_str(),
// aspectDefinition.voxelName.c_str());
voxelsAspect[voxelID].definition = aspectDefinition; voxelsAspect[voxelID].definition = aspectDefinition;
} }
@ -92,9 +88,6 @@ namespace Deer {
texturesIDs[faceTextureString] = textureID; texturesIDs[faceTextureString] = textureID;
voxelAspect.textureFacesIDs[i] = textureID; voxelAspect.textureFacesIDs[i] = textureID;
// DEER_CORE_TRACE(" texture {0} - id: {1}",
// faceTextureString.c_str(), textureID);
} }
} }
} }

View File

@ -209,8 +209,6 @@ namespace Deer {
// Either of them. Don't leak shaders. // Either of them. Don't leak shaders.
glDeleteShader(vertexShader); glDeleteShader(vertexShader);
DEER_CORE_TRACE(source);
// Use the infoLog as you see fit. // Use the infoLog as you see fit.
DEER_CORE_ERROR("Error compiling fragment shader. \n{0}", infoLog); DEER_CORE_ERROR("Error compiling fragment shader. \n{0}", infoLog);
delete[] infoLog; delete[] infoLog;

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <vector>
#include "DeerRender/Tools/Path.h" #include "DeerRender/Tools/Path.h"
#include <string>
#include <unordered_map>
#include <vector>
class asIScriptEngine; class asIScriptEngine;
class asIScriptModule; class asIScriptModule;
@ -24,6 +26,7 @@ namespace Deer {
~Service(); ~Service();
Service(Service&) = delete; Service(Service&) = delete;
Service& operator=(Service&) = delete;
Service(Service&&) = default; Service(Service&&) = default;
Service& operator=(Service&&) = default; Service& operator=(Service&&) = default;
@ -32,11 +35,11 @@ namespace Deer {
void shutdown(); void shutdown();
protected: protected:
asITypeInfo* type= nullptr; asITypeInfo* type = nullptr;
asIScriptObject* object = nullptr; asIScriptObject* object = nullptr;
asIScriptFunction* updateFunction= nullptr; asIScriptFunction* updateFunction = nullptr;
asIScriptFunction* initFunction= nullptr; asIScriptFunction* initFunction = nullptr;
asIScriptFunction* shutdownFunction= nullptr; asIScriptFunction* shutdownFunction = nullptr;
}; };
struct Panel : public Service { struct Panel : public Service {
@ -55,12 +58,33 @@ namespace Deer {
asIScriptFunction* menuBarFunction; asIScriptFunction* menuBarFunction;
}; };
struct ModuleDescription {
public:
int patchVersion = -1;
std::string moduleName;
std::string modulePath;
std::vector<std::string> module_requires;
};
enum class ModuleState {
NotBuilt = 0,
Building = 1,
Built = 2,
DependencyFailed = 3,
CompilationFailed = 5,
ExecutionError = 6
};
struct Module { struct Module {
public: public:
Module(asIScriptModule*); ModuleDescription moduleInfo;
ModuleState state = ModuleState::NotBuilt;
Module(const ModuleDescription&);
~Module(); ~Module();
Module(const Module&) = delete; Module(const Module&) = delete;
Module& operator=(Module&) = delete;
Module(Module&&) = default; Module(Module&&) = default;
Module& operator=(Module&&) = default; Module& operator=(Module&&) = default;
@ -69,19 +93,22 @@ namespace Deer {
void render(); void render();
void shutdown(); void shutdown();
inline void invalidate() { invalidated = true; } void extractTypes();
inline void invalidate() { state = ModuleState::ExecutionError; }
private: private:
asIScriptModule* angelscriptModule;
std::vector<Panel> panels; std::vector<Panel> panels;
std::vector<Service> services; std::vector<Service> services;
asIScriptModule* angelscriptModule;
bool invalidated = false;
}; };
extern asIScriptEngine* scriptEngine; extern asIScriptEngine* scriptEngine;
extern asIScriptContext* scriptContext; extern asIScriptContext* scriptContext;
extern CScriptBuilder scriptBuilder; extern CScriptBuilder scriptBuilder;
extern std::vector<Module> modules; extern std::vector<Module> modules;
extern std::unordered_map<std::string, int> module_id;
extern asITypeInfo* panelBaseType; extern asITypeInfo* panelBaseType;
extern asITypeInfo* serviceBaseType; extern asITypeInfo* serviceBaseType;

View File

@ -21,6 +21,7 @@ namespace Deer {
CScriptBuilder scriptBuilder; CScriptBuilder scriptBuilder;
std::vector<Module> modules; std::vector<Module> modules;
std::unordered_map<std::string, int> module_id;
Module* executingModule; Module* executingModule;
asITypeInfo* panelBaseType; asITypeInfo* panelBaseType;
@ -82,6 +83,7 @@ namespace Deer {
} }
modules.clear(); modules.clear();
module_id.clear();
if (scriptContext) if (scriptContext)
scriptContext->Release(); scriptContext->Release();

View File

@ -1,12 +1,17 @@
#include "DeerStudio/AngelScriptEngine.h"
#include "angelscript.h"
#include "DeerRender/Log.h" #include "DeerRender/Log.h"
#include "DeerStudio/AngelScriptEngine.h"
#include "DeerStudio/AngelScriptEngine/ErrorHandle.h" #include "DeerStudio/AngelScriptEngine/ErrorHandle.h"
#include "angelscript.h"
namespace Deer { namespace Deer {
namespace AngelScriptEngine { namespace AngelScriptEngine {
Module::Module(asIScriptModule* _angelscriptModule) { Module::Module(const ModuleDescription& _mi) : moduleInfo(_mi) {}
angelscriptModule = _angelscriptModule;
void Module::extractTypes() {
if (state != ModuleState::Built)
return;
angelscriptModule = AngelScriptEngine::scriptEngine->GetModule(moduleInfo.moduleName.c_str());
uint32_t typeCount = angelscriptModule->GetObjectTypeCount(); uint32_t typeCount = angelscriptModule->GetObjectTypeCount();
for (uint32_t typeId = 0; typeId < typeCount; typeId++) { for (uint32_t typeId = 0; typeId < typeCount; typeId++) {
@ -21,10 +26,9 @@ namespace Deer {
} }
void Module::init() { void Module::init() {
angelscriptModule->BindAllImportedFunctions(); if (state != ModuleState::Built)
if (invalidated)
return; return;
angelscriptModule->BindAllImportedFunctions();
for (Service& service : services) for (Service& service : services)
service.init(); service.init();
@ -34,7 +38,7 @@ namespace Deer {
} }
void Module::update() { void Module::update() {
if (invalidated) if (state != ModuleState::Built)
return; return;
for (Service& service : services) for (Service& service : services)
@ -45,7 +49,7 @@ namespace Deer {
} }
void Module::render() { void Module::render() {
if (invalidated) if (state != ModuleState::Built)
return; return;
for (Panel& panel : panels) for (Panel& panel : panels)
@ -53,14 +57,14 @@ namespace Deer {
} }
void Module::shutdown() { void Module::shutdown() {
if (state != ModuleState::Built)
return;
for (Service& service : services) for (Service& service : services)
service.shutdown(); service.shutdown();
for (Panel& panel : panels) for (Panel& panel : panels)
panel.shutdown(); panel.shutdown();
if (invalidated)
return;
} }
Module::~Module() { Module::~Module() {

View File

@ -0,0 +1,17 @@
#pragma once
#include "DeerStudio/AngelScriptEngine.h"
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
namespace Deer {
namespace AngelScriptEngine {
template <class Archive>
void serialize(Archive& archive,
ModuleDescription& m) {
archive(cereal::make_nvp("name", m.moduleName));
archive(cereal::make_nvp("patch", m.patchVersion));
archive(cereal::make_nvp("requires", m.module_requires));
}
} // namespace AngelScriptEngine
} // namespace Deer

View File

@ -1,20 +1,28 @@
#include "DeerRender/Tools/Path.h" #include "DeerRender/Tools/Path.h"
#include "DeerStudio/AngelScriptEngine.h" #include "DeerStudio/AngelScriptEngine.h"
#include "DeerStudio/AngelScriptEngine/ErrorHandle.h" #include "DeerStudio/AngelScriptEngine/ErrorHandle.h"
#include "DeerStudio/AngelScriptEngine/ModuleDescriptorSerializer.h"
#include "angelscript.h" #include "angelscript.h"
#include "cereal/archives/json.hpp"
#include "scriptbuilder.h" #include "scriptbuilder.h"
#include "DeerRender/Log.h" #include "DeerRender/Log.h"
#include <filesystem> #include <filesystem>
#include <fstream>
#include <iostream> #include <iostream>
#include <stdexcept>
#include <string> #include <string>
#include <unordered_map>
#include <vector>
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace Deer { namespace Deer {
namespace AngelScriptEngine { namespace AngelScriptEngine {
void loadModule(const Path& path); bool loadModule(Module& module);
void loadModuleInfo(const Path& path);
} // namespace AngelScriptEngine } // namespace AngelScriptEngine
asIScriptFunction* AngelScriptEngine::getFactory(asITypeInfo* type) { asIScriptFunction* AngelScriptEngine::getFactory(asITypeInfo* type) {
@ -35,36 +43,116 @@ namespace Deer {
return type->GetFactoryByDecl(callString.c_str()); return type->GetFactoryByDecl(callString.c_str());
} }
void AngelScriptEngine::loadModule(const Path& path) { bool AngelScriptEngine::loadModule(Module& module) {
int err; int err;
module.state = ModuleState::Building;
scriptBuilder.StartNewModule(scriptEngine, path.stem().string().c_str()); DEER_CORE_TRACE("Loading module {}", module.moduleInfo.moduleName);
if (err < 0) { for (const std::string& dependency : module.moduleInfo.module_requires) {
DEER_EDITOR_ENGINE_ERROR("Failed to init module for {0}", path.string().c_str()); if (!module_id.contains(dependency)) {
return; DEER_CORE_ERROR("Failed to find dependency {} for module {}",
dependency.c_str(), module.moduleInfo.moduleName);
module.state = ModuleState::DependencyFailed;
return false;
}
Module& dependencyModule = modules[module_id[dependency]];
if (dependencyModule.state == ModuleState::DependencyFailed ||
dependencyModule.state == ModuleState::CompilationFailed) {
DEER_CORE_ERROR("Failed to load dependency {} for module {}, the dependency did not load correctly and caused a chain reaction",
dependency.c_str(), module.moduleInfo.moduleName.c_str());
module.state = ModuleState::DependencyFailed;
return false;
}
if (dependencyModule.state == ModuleState::Building) {
DEER_CORE_ERROR("Failed to load dependency {} for module {} due to loop",
dependency.c_str(), module.moduleInfo.moduleName.c_str());
module.state = ModuleState::DependencyFailed;
return false;
}
if (dependencyModule.state == ModuleState::NotBuilt) {
if (!loadModule(dependencyModule)) {
module.state = ModuleState::DependencyFailed;
return false;
}
}
} }
for (const auto& entry : fs::recursive_directory_iterator(path)) { scriptBuilder.StartNewModule(scriptEngine, module.moduleInfo.moduleName.c_str());
if (err < 0) {
DEER_EDITOR_ENGINE_ERROR("Failed to init module for {0}", module.moduleInfo.moduleName.c_str());
module.state = ModuleState::CompilationFailed;
return false;
}
for (const auto& entry : fs::recursive_directory_iterator(module.moduleInfo.modulePath)) {
if (!entry.is_regular_file() || entry.path().extension() != ".as") if (!entry.is_regular_file() || entry.path().extension() != ".as")
continue; continue;
DEER_CORE_TRACE("{}", entry.path().filename().string().c_str());
err = scriptBuilder.AddSectionFromFile(entry.path().string().c_str()); err = scriptBuilder.AddSectionFromFile(entry.path().string().c_str());
if (err < 0) { if (err < 0) {
DEER_EDITOR_ENGINE_ERROR("Failed loading script for module {0}\nscript: {1}", path.string().c_str(), entry.path().string().c_str()); DEER_EDITOR_ENGINE_ERROR("Failed loading script for module {0}\nscript: {1}", module.moduleInfo.moduleName.c_str(), entry.path().string().c_str());
module.state = ModuleState::CompilationFailed;
return false;
} }
} }
err = scriptBuilder.BuildModule(); err = scriptBuilder.BuildModule();
asIScriptModule* module = scriptBuilder.GetModule(); asIScriptModule* as_module = scriptBuilder.GetModule();
if (err < 0) { if (err < 0) {
DEER_EDITOR_ENGINE_ERROR("Failed compiling module {}\nerror: {}", path.stem().string().c_str(), DEER_EDITOR_ENGINE_ERROR("Failed compiling module {}\nerror: {}", module.moduleInfo.moduleName.c_str(), Deer::AngelScriptEngine::getAngelScriptReturnCodeString(err));
Deer::AngelScriptEngine::getAngelScriptReturnCodeString(err)); module.state = ModuleState::CompilationFailed;
return false;
}
module.state = ModuleState::Built;
return true;
}
void AngelScriptEngine::loadModuleInfo(const Path& path) {
ModuleDescription desc;
std::ifstream is((path / "module.json").string());
if (!is.is_open()) {
DEER_CORE_ERROR("Cannot open module description: {}", (path / "module.json").string().c_str());
ModuleDescription desc;
desc.moduleName = path.stem().string();
desc.patchVersion = 1;
std::ofstream os((path / "module.json").string());
{
cereal::JSONOutputArchive archive(os);
serialize(archive, desc);
}
return; return;
} }
modules.push_back({module}); std::string contents((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
std::stringstream ss(contents);
try {
cereal::JSONInputArchive archive(ss);
serialize(archive, desc);
if (desc.moduleName.empty()) {
DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str());
return;
}
if (desc.patchVersion < 0) {
DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str());
return;
}
} catch (const std::exception& e) {
DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str());
return;
}
desc.modulePath = path.string();
module_id[desc.moduleName] = modules.size();
modules.push_back({desc});
} }
void AngelScriptEngine::loadModules() { void AngelScriptEngine::loadModules() {
@ -77,14 +165,22 @@ namespace Deer {
} }
for (const auto& module : fs::directory_iterator(path)) { for (const auto& module : fs::directory_iterator(path)) {
// saveAngelscriptPredefined(_dir);
if (module.path().extension() == ".disabled") { if (module.path().extension() == ".disabled") {
DEER_CORE_TRACE("Ignoring {}", module.path().stem().string().c_str()); DEER_CORE_WARN("Ignoring {}", module.path().stem().string().c_str());
continue; continue;
} }
DEER_CORE_TRACE("=== Loading module {} ===", module.path().stem().string().c_str()); loadModuleInfo(module.path());
saveAngelscriptPredefined(module.path()); }
loadModule(module.path());
for (Module& module : modules) {
saveAngelscriptPredefined(module.moduleInfo.modulePath);
if (module.state == ModuleState::NotBuilt) {
loadModule(module);
}
}
for (Module& module : modules) {
module.extractTypes();
} }
} }
} // namespace Deer } // namespace Deer

View File

@ -29,7 +29,7 @@ namespace Deer {
if (!initFunction) if (!initFunction)
return; return;
AS_CHECK(scriptContext->Prepare(initFunction)); AS_CHECK(scriptContext->Prepare(initFunction));
AS_CHECK(scriptContext->SetObject(object)); AS_CHECK(scriptContext->SetObject(object));
AS_CHECK(scriptContext->Execute()); AS_CHECK(scriptContext->Execute());
@ -52,7 +52,7 @@ namespace Deer {
if (!shutdownFunction) { if (!shutdownFunction) {
if (object) if (object)
object->Release(); object->Release();
AS_CHECK(scriptContext->Unprepare()); AS_CHECK(scriptContext->Unprepare());
return; return;
} }
@ -62,7 +62,7 @@ namespace Deer {
AS_CHECK(scriptContext->Execute()); AS_CHECK(scriptContext->Execute());
if (object) if (object)
object->Release(); object->Release();
AS_CHECK(scriptContext->Unprepare()); AS_CHECK(scriptContext->Unprepare());
} }
@ -75,6 +75,6 @@ namespace Deer {
shutdownFunction = type->GetMethodByDecl("void shutdown()"); shutdownFunction = type->GetMethodByDecl("void shutdown()");
} }
Service::~Service() { } Service::~Service() {}
} // namespace AngelScriptEngine } // namespace AngelScriptEngine
} // namespace Deer } // namespace Deer

View File

@ -17,7 +17,6 @@ namespace Deer {
Scope<MeshData> StorageData::deserialize<EditorDataImporter, MeshData>() { Scope<MeshData> StorageData::deserialize<EditorDataImporter, MeshData>() {
Assimp::Importer importer; Assimp::Importer importer;
DEER_CORE_TRACE("Size {}", size);
std::string extension; std::string extension;
const char* extension_char = nullptr; const char* extension_char = nullptr;
if (getMetadata().contains("dataId")) { if (getMetadata().contains("dataId")) {

View File

@ -1,5 +1,5 @@
#include "DeerRender/Log.h"
#include "DeerStudio/EditorDataSource.h" #include "DeerStudio/EditorDataSource.h"
#include "DeerRender/Log.h"
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@ -8,6 +8,11 @@
namespace Deer { namespace Deer {
StorageData StorageBackend<EditorDataSource>::loadData(const std::string& location) { StorageData StorageBackend<EditorDataSource>::loadData(const std::string& location) {
Path path = Path("Editor") / location; Path path = Path("Editor") / location;
if (!std::filesystem::exists(path)) {
return StorageData();
}
std::ifstream file(path, std::ios::binary | std::ios::ate); std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) { if (!file.is_open()) {
DEER_CORE_ERROR("Failed to open file '{}'", location.c_str()); DEER_CORE_ERROR("Failed to open file '{}'", location.c_str());

View File

@ -1,5 +1,5 @@
#include "DeerRender/Log.h"
#include "DeerStudio/ResourceDataSource.h" #include "DeerStudio/ResourceDataSource.h"
#include "DeerRender/Log.h"
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@ -55,6 +55,10 @@ namespace Deer {
Path path = Path("Resources") / (location + ".meta"); Path path = Path("Resources") / (location + ".meta");
StorageMetadata metadata; StorageMetadata metadata;
if (!std::filesystem::exists(path)) {
return metadata;
}
std::ifstream file(path); std::ifstream file(path);
if (!file.is_open()) { if (!file.is_open()) {
DEER_CORE_TRACE("Failed to open metadata file '{}' for reading", path.string().c_str()); DEER_CORE_TRACE("Failed to open metadata file '{}' for reading", path.string().c_str());

View File

@ -9,7 +9,7 @@
#include <vector> #include <vector>
#define GET_ENV(env) ((env < 0)? Scene::environment : Deer::Resource<Environment>::unsafeFromId(environmentId).getData()) #define GET_ENV(env) ((env < 0) ? Scene::environment : Deer::Resource<Environment>::unsafeFromId(environmentId).getData())
#define GET_ENTITY(env, id) GET_ENV(env).getEntity(id) #define GET_ENTITY(env, id) GET_ENV(env).getEntity(id)
#define GET_MESH_COMPONENT(env, id) \ #define GET_MESH_COMPONENT(env, id) \
@ -47,7 +47,7 @@ namespace Deer {
EntityStruct::EntityStruct(uint16_t _entId, int32_t _envId) : EntityHandleStruct(_entId, _envId) {} EntityStruct::EntityStruct(uint16_t _entId, int32_t _envId) : EntityHandleStruct(_entId, _envId) {}
Environment* EntityHandleStruct::getEntityEnvironment() { Environment* EntityHandleStruct::getEntityEnvironment() {
if (environmentId < 0) if (environmentId < 0)
return &Scene::environment; return &Scene::environment;
return &Resource<Environment>::unsafeFromId(environmentId).getData(); return &Resource<Environment>::unsafeFromId(environmentId).getData();
} }
@ -241,9 +241,8 @@ namespace Deer {
EntityHandleStruct EntityStruct::createChild(std::string& name) { EntityHandleStruct EntityStruct::createChild(std::string& name) {
ASSERT_ENTITY("createChild()", return *this); ASSERT_ENTITY("createChild()", return *this);
DEER_CORE_INFO("Creating child on env {}", environmentId);
Environment* entityEnv = getEntityEnvironment(); Environment* entityEnv = getEntityEnvironment();
Entity& newEnt = entityEnv->createEntity(name); Entity& newEnt = entityEnv->createEntity(name);
Entity& me = GET_ENTITY(environmentId, entityId); Entity& me = GET_ENTITY(environmentId, entityId);

View File

@ -0,0 +1,5 @@
{
"patch": 1,
"name": "ActiveEntity",
"requires": []
}

View File

@ -0,0 +1,5 @@
{
"name": "EntityManipulation",
"patch": 1,
"requires": ["ActiveEntity"]
}

View File

@ -22,7 +22,6 @@ class RenderService : Service {
shaderC.shader = Resource::loadShader("shader.glsl"); shaderC.shader = Resource::loadShader("shader.glsl");
Engine::print("CAM : " + sceneCamera.transform.position.x + " " + sceneCamera.transform.scale.x + " : " + sceneCamera.camera.aspect);
sceneCamera.transform.position.z = -3; sceneCamera.transform.position.z = -3;
sceneCamera.transform.position.y = 1; sceneCamera.transform.position.y = 1;

View File

@ -0,0 +1,5 @@
{
"patch": 1,
"name": "Previewer",
"requires": []
}

View File

@ -0,0 +1,5 @@
{
"patch": 1,
"name": "ResourceExplorer",
"requires": ["Previewer"]
}

View File

@ -0,0 +1,5 @@
{
"patch": 1,
"name": "TreeExplorer",
"requires": ["ActiveEntity"]
}

View File

@ -0,0 +1,5 @@
{
"patch": 1,
"name": "Viewport",
"requires": ["Previewer"]
}