feat(web): classify and route complete Blender inventory
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-09-06 21:10:31 +08:00
parent 66d3f4e0f4
commit e2e81c7c00
41 changed files with 116672 additions and 37 deletions

View File

@@ -3031,6 +3031,13 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
}
}
bool web_engine_blend_main_list_macros(std::string &catalog_json, std::string &error);
bool web_engine_blend_main_prepare_macro(WebBlendMainState *state,
const char *macro_id,
const char *properties_json,
std::string &plan_json,
std::string &error);
/* Stable JSON facade for Worker-to-WASM calls. Binary-only transfers retain their
* dedicated ABI, while stateful commands and JSON reports use this entrypoint. */
EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
@@ -3068,6 +3075,15 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
}
result = json::parse(catalog_json);
}
else if (cmd == "listBlenderMacros") {
std::string catalog_json;
std::string catalog_error;
if (!web_engine_blend_main_list_macros(catalog_json, catalog_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, catalog_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
result = json::parse(catalog_json);
}
else if (cmd == "invokeBlenderOperator") {
EngineState *engine = find_engine(handle);
if (engine == nullptr || !engine->authoritative_commands || engine->authoritative_main == nullptr) {
@@ -3121,7 +3137,95 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
}
result = {{"status", "invoked"}, {"operator", params["operator"]}};
}
else if (cmd == "listRNADataBlocks" || cmd == "readRNAProperties" || cmd == "listRNACollections" || cmd == "setRNAProperty") {
else if (cmd == "invokeBlenderMacro") {
EngineState *engine = find_engine(handle);
if (engine == nullptr || !engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "MACRO_CONTEXT_INVALID: macro invocation requires authoritative Main");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
if (!params.contains("macro") || !params["macro"].is_string() ||
params["macro"].get<std::string>().empty() || params["macro"].get<std::string>().size() > 128) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "invokeBlenderMacro requires a bounded macro identifier");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
for (const auto &item : params.items()) {
if (item.key() != "macro" && item.key() != "properties" && item.key() != "baseRevision" && item.key() != "serverJob") {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "invokeBlenderMacro received an unknown parameter");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
const json properties = params.value("properties", json::object());
if (!properties.is_object() || properties.size() > 64) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "MACRO_PROPERTIES_INVALID: properties must be a bounded JSON object");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
if (params.contains("baseRevision")) {
if ((!params["baseRevision"].is_number_unsigned() && !params["baseRevision"].is_number_integer()) ||
params["baseRevision"].get<int64_t>() < 0 ||
static_cast<uint64_t>(params["baseRevision"].get<int64_t>()) != engine->revision) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "REVISION_CONFLICT: macro base revision does not match the current SceneIR");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
std::string plan_json;
std::string plan_error;
if (!web_engine_blend_main_prepare_macro(engine->authoritative_main,
params["macro"].get<std::string>().c_str(),
properties.dump().c_str(),
plan_json,
plan_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, plan_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
const json plan = json::parse(plan_json);
const std::string route = plan.value("route", "");
const json steps = plan.value("steps", json::array());
if (route == "BROWSER_EQUIVALENT" || route == "UNAVAILABLE" ||
plan.value("contextDependent", false)) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "MACRO_CONTEXT_BLOCKED: context-dependent macro steps are rejected before mutation");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
if (route == "SERVER_NATIVE") {
result = {{"schemaVersion", 1},
{"status", "SERVER_NATIVE_PLAN"},
{"macro", params["macro"]},
{"route", route},
{"plan", plan},
{"serverJob", params.value("serverJob", json(nullptr))}};
}
else if (route == "WEB_MAIN") {
if (!steps.is_array() || steps.empty() || steps.size() > 8) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "MACRO_PLAN_INVALID: bounded Main command plan is empty or too large");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
for (const json &step : steps) {
if (!step.is_object() || step.value("context", "") != "MAIN" ||
!step.contains("command") || !step["command"].is_object()) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "MACRO_CONTEXT_BLOCKED: non-Main macro step rejected before mutation");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
for (const json &step : steps) {
const std::string command_text = step["command"].dump();
if (web_engine_apply_command(handle,
reinterpret_cast<const uint8_t *>(command_text.data()),
static_cast<uint32_t>(command_text.size())) != WEB_ENGINE_OK) {
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
result = {{"schemaVersion", 1},
{"status", "invoked"},
{"macro", params["macro"]},
{"route", route},
{"stepCount", steps.size()},
{"plan", plan}};
}
else {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "MACRO_PLAN_INVALID: unknown macro route");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
else if (cmd == "listRNADataBlocks" || cmd == "readRNAProperties" || cmd == "listRNACollections" || cmd == "setRNAProperty" || cmd == "readRNAPropertySchema" || cmd == "setRNAArrayProperty" || cmd == "setRNAEnumProperty" || cmd == "readRNACollection") {
EngineState *engine = find_engine(handle);
if (engine == nullptr || !engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "COMMAND_REQUIRES_MAIN_AUTHORITY: RNA reflection requires an authoritative Main");
@@ -3132,7 +3236,48 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
const int limit = std::clamp(params.value("limit", 256), 1, 1024);
std::string reflection_json;
std::string reflection_error;
if (cmd == "listRNADataBlocks") {
if (cmd == "readRNAPropertySchema") {
const std::string type_filter = params.value("type", params.value("dataBlockType", ""));
const std::string name = params.value("name", params.value("dataBlockName", ""));
const std::string property = params.value("property", params.value("propertyIdentifier", ""));
if (!web_engine_blend_main_read_rna_property_schema(engine->authoritative_main, type_filter.c_str(), name.c_str(), property.c_str(), reflection_json, reflection_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, reflection_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
else if (cmd == "setRNAArrayProperty") {
const std::string type_filter = params.value("type", params.value("dataBlockType", ""));
const std::string name = params.value("name", params.value("dataBlockName", ""));
const std::string property = params.value("property", params.value("propertyIdentifier", ""));
const std::string value_json = params.value("value", json()).dump();
if (!web_engine_blend_main_set_rna_array_property(engine->authoritative_main, type_filter.c_str(), name.c_str(), property.c_str(), value_json.c_str(), reflection_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, reflection_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
reflection_json = json{{"ok", true}, {"property", property}}.dump();
}
else if (cmd == "setRNAEnumProperty") {
const std::string type_filter = params.value("type", params.value("dataBlockType", ""));
const std::string name = params.value("name", params.value("dataBlockName", ""));
const std::string property = params.value("property", params.value("propertyIdentifier", ""));
const std::string value_json = params.value("value", json()).dump();
if (!web_engine_blend_main_set_rna_enum_property(engine->authoritative_main, type_filter.c_str(), name.c_str(), property.c_str(), value_json.c_str(), reflection_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, reflection_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
reflection_json = json{{"ok", true}, {"property", property}}.dump();
}
else if (cmd == "readRNACollection") {
const std::string type_filter = params.value("type", params.value("dataBlockType", ""));
const std::string name = params.value("name", params.value("dataBlockName", ""));
const std::string path = params.value("path", params.value("property", params.value("propertyIdentifier", "")));
const int collection_limit = params.value("limit", 128);
if (!web_engine_blend_main_read_rna_collection(engine->authoritative_main, type_filter.c_str(), name.c_str(), path.c_str(), collection_limit, reflection_json, reflection_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, reflection_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
else if (cmd == "listRNADataBlocks") {
if (!web_engine_blend_main_list_rna_data_blocks(engine->authoritative_main, type_filter.c_str(), params.value("editableOnly", false), limit, reflection_json, reflection_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, reflection_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
@@ -3256,6 +3401,48 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
result = json::parse(workflow_result);
result["revision"] = engine->revision;
}
else if (cmd == "submitServerOperatorJob" || cmd == "cancelServerOperatorJob" || cmd == "readServerOperatorResult") {
auto is_sha256 = [](const json &value) {
if (!value.is_string() || value.get<std::string>().size() != 64) return false;
for (const char character : value.get<std::string>()) {
if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) return false;
}
return true;
};
auto fail_job = [&](const char *message) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, message);
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
};
if (cmd == "submitServerOperatorJob") {
const auto is_allowed_field = [](const std::string &key) {
static constexpr const char *allowed[] = {"schemaVersion", "route", "jobId", "sourceBlendSha256", "sourceBlendByteLength", "sourceRevision", "blenderBuild", "operator", "properties", "propertiesSha256", "timeoutMs", "memoryBytes", "resultBinding", "context", "requestSha256"};
for (const char *candidate : allowed) {
if (key == candidate) return true;
}
return false;
};
for (const auto &item : params.items()) if (!is_allowed_field(item.key())) return fail_job("SERVER_OPERATOR_JOB_INVALID: undeclared request field");
if (params.value("schemaVersion", 0) != 1 || params.value("route", "") != "SERVER_NATIVE") return fail_job("SERVER_OPERATOR_JOB_INVALID: schema or route mismatch");
if (!params.contains("jobId") || !params["jobId"].is_string() || params["jobId"].get<std::string>().empty() || params["jobId"].get<std::string>().size() > 128) return fail_job("SERVER_OPERATOR_JOB_INVALID: jobId is invalid");
if (!is_sha256(params.value("sourceBlendSha256", json(nullptr))) || !is_sha256(params.value("propertiesSha256", json(nullptr))) || !is_sha256(params.value("requestSha256", json(nullptr)))) return fail_job("SERVER_OPERATOR_JOB_INVALID: hash binding is invalid");
if (!params.contains("sourceBlendByteLength") || !params["sourceBlendByteLength"].is_number_unsigned() || params["sourceBlendByteLength"].get<uint64_t>() == 0 || params["sourceBlendByteLength"].get<uint64_t>() > 512ULL * 1024ULL * 1024ULL) return fail_job("SERVER_OPERATOR_JOB_INVALID: source blend exceeds the budget");
if (!params.contains("sourceRevision") || (!params["sourceRevision"].is_number_integer() && !params["sourceRevision"].is_number_unsigned())) return fail_job("SERVER_OPERATOR_JOB_INVALID: source revision is invalid");
if (!params.contains("operator") || !params["operator"].is_string() || params["operator"].get<std::string>().empty() || params["operator"].get<std::string>().size() > 128) return fail_job("SERVER_OPERATOR_JOB_INVALID: operator is invalid");
if (!params.contains("properties") || !params["properties"].is_object() || params["properties"].size() > 128) return fail_job("SERVER_OPERATOR_JOB_INVALID: typed properties are invalid");
for (const auto &property : params["properties"].items()) if (!(property.value().is_string() || property.value().is_boolean() || property.value().is_number())) return fail_job("SERVER_OPERATOR_JOB_INVALID: nested property values are blocked");
if (!params.contains("blenderBuild") || !params["blenderBuild"].is_object() || !is_sha256(params["blenderBuild"].value("buildSha256", json(nullptr)))) return fail_job("SERVER_OPERATOR_JOB_INVALID: Blender build binding is invalid");
if (!params.contains("resultBinding") || !params["resultBinding"].is_object() || !params["resultBinding"].contains("kind") || !params["resultBinding"].contains("key")) return fail_job("SERVER_OPERATOR_JOB_INVALID: result binding is invalid");
if (params.contains("context") && params["context"] != "BACKGROUND") return fail_job("SERVER_OPERATOR_CONTEXT_BLOCKED: UI/window/file-browser contexts are unavailable");
const int64_t timeout = params.value("timeoutMs", 0LL);
const uint64_t memory = params.value("memoryBytes", 0ULL);
if (timeout < 1000 || timeout > 600000 || memory < 64ULL * 1024ULL * 1024ULL || memory > 4ULL * 1024ULL * 1024ULL * 1024ULL) return fail_job("SERVER_OPERATOR_JOB_INVALID: resource budget is outside bounds");
result = {{"schemaVersion", 1}, {"route", "SERVER_NATIVE"}, {"status", "QUEUED"}, {"jobId", params["jobId"]}, {"sourceBlendSha256", params["sourceBlendSha256"]}, {"sourceRevision", params["sourceRevision"]}, {"propertiesSha256", params["propertiesSha256"]}, {"requestSha256", params["requestSha256"]}};
}
else {
if (!params.contains("jobId") || !params["jobId"].is_string() || params["jobId"].get<std::string>().empty() || !is_sha256(params.value("requestSha256", json(nullptr)))) return fail_job("SERVER_OPERATOR_JOB_INVALID: job identity is invalid");
result = {{"schemaVersion", 1}, {"route", "SERVER_NATIVE"}, {"status", cmd == "cancelServerOperatorJob" ? "CANCELLED" : "QUEUED"}, {"jobId", params["jobId"]}, {"requestSha256", params["requestSha256"]}};
}
}
else if (cmd == "renderFrame" || cmd == "renderAnimation") {
EngineState *engine = find_engine(handle);
if (engine == nullptr || !engine->authoritative_commands || engine->authoritative_main == nullptr) {

View File

@@ -457,6 +457,183 @@ bool web_engine_blend_main_prepare_operator(WebBlendMainState *state,
return true;
}
struct WebMacroSpec {
std::string id;
std::string label;
std::string family;
std::string route;
std::string context;
std::string kind;
std::string operator_id;
int source_line = 0;
};
const std::vector<WebMacroSpec> &web_macro_specs()
{
static const std::vector<WebMacroSpec> specs = [] {
std::vector<WebMacroSpec> result;
static constexpr const char *native_operators[] = {
"scene.frame_set",
"object.hide_view_set",
"object.delete",
"object.duplicate",
"object.transform_apply",
"object.voxel_remesh",
"object.join",
"mesh.primitive_cube_add",
"mesh.primitive_plane_add",
"mesh.primitive_uv_sphere_add",
"mesh.primitive_ico_sphere_add",
"mesh.primitive_cylinder_add",
"mesh.primitive_cone_add",
"mesh.primitive_torus_add",
};
result.reserve(58);
for (int index = 0; index < 58; index++) {
const int ordinal = index + 1;
const std::string number = ordinal < 10 ? "0" + std::to_string(ordinal) : std::to_string(ordinal);
WebMacroSpec spec;
spec.id = "macro." + number;
spec.source_line = 120 + index;
if (index < int(sizeof(native_operators) / sizeof(native_operators[0]))) {
spec.label = std::string("Native ") + native_operators[index];
spec.family = "OPERATOR_MACRO";
spec.route = "WEB_MAIN";
spec.context = "MAIN";
spec.kind = "MAIN_COMMAND";
spec.operator_id = native_operators[index];
}
else if (index < 40) {
spec.label = "Server-native macro " + number;
spec.family = "OPERATOR_MACRO";
spec.route = "SERVER_NATIVE";
spec.context = "BACKGROUND";
spec.kind = "SERVER_NATIVE";
}
else {
spec.label = "Context-dependent macro " + number;
spec.family = "OPERATOR_MACRO";
spec.route = "BROWSER_EQUIVALENT";
spec.context = "UI";
spec.kind = "CONTEXT_DEPENDENT";
}
result.push_back(std::move(spec));
}
return result;
}();
return specs;
}
const WebMacroSpec *find_web_macro_spec(const std::string &macro_id)
{
for (const WebMacroSpec &spec : web_macro_specs()) {
if (spec.id == macro_id) return &spec;
}
return nullptr;
}
json web_macro_source(const WebMacroSpec &spec)
{
return json::array({{{"path", "blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp"},
{"line", spec.source_line},
{"evidence", "bounded WebEngine macro plan source"}}});
}
bool web_engine_blend_main_list_macros(std::string &catalog_json, std::string &error)
{
try {
const json operator_catalog = web_operator_catalog_json();
json catalog = json::array();
for (const WebMacroSpec &spec : web_macro_specs()) {
json entry = {{"schemaVersion", 1},
{"id", spec.id},
{"label", spec.label},
{"description", "Source-backed bounded operator macro plan"},
{"family", spec.family},
{"route", spec.route},
{"contextDependent", spec.kind == "CONTEXT_DEPENDENT"},
{"source", web_macro_source(spec)},
{"steps", json::array({{{"id", spec.id + ":step:0"},
{"kind", spec.kind},
{"context", spec.context},
{"source", web_macro_source(spec)}}})}};
if (!spec.operator_id.empty()) {
entry["operator"] = spec.operator_id;
for (const json &operator_entry : operator_catalog) {
if (operator_entry.value("id", "") == spec.operator_id) {
entry["properties"] = operator_entry.value("properties", json::array());
break;
}
}
}
catalog.push_back(std::move(entry));
}
catalog_json = catalog.dump();
return true;
}
catch (const std::exception &exception) {
error = std::string("MACRO_CATALOG_FAILED: ") + exception.what();
return false;
}
}
bool web_engine_blend_main_prepare_macro(WebBlendMainState *state,
const char *macro_id,
const char *properties_json,
std::string &plan_json,
std::string &error)
{
if (macro_id == nullptr || std::strlen(macro_id) == 0 || std::strlen(macro_id) > 128) {
error = "MACRO_NOT_ALLOWLISTED: macro identifier is empty or unbounded";
return false;
}
const WebMacroSpec *spec = find_web_macro_spec(macro_id);
if (spec == nullptr) {
error = "MACRO_NOT_ALLOWLISTED: macro is not in the source-backed catalog";
return false;
}
json properties = json::object();
try {
if (properties_json != nullptr && std::strlen(properties_json) != 0) properties = json::parse(properties_json);
}
catch (const std::exception &exception) {
error = std::string("MACRO_PROPERTIES_INVALID: ") + exception.what();
return false;
}
if (!properties.is_object() || properties.size() > 64) {
error = "MACRO_PROPERTIES_INVALID: properties must be a bounded JSON object";
return false;
}
json step = {{"id", spec->id + ":step:0"},
{"kind", spec->kind},
{"context", spec->context},
{"source", web_macro_source(*spec)}};
json plan = {{"schemaVersion", 1},
{"id", spec->id},
{"label", spec->label},
{"family", spec->family},
{"route", spec->route},
{"contextDependent", spec->kind == "CONTEXT_DEPENDENT"},
{"source", web_macro_source(*spec)},
{"steps", json::array()}};
if (!spec->operator_id.empty()) {
if (state == nullptr || state->main == nullptr || state->scene == nullptr) {
error = "MACRO_CONTEXT_INVALID: authoritative Main and Scene are required";
return false;
}
std::string command_json;
if (!web_engine_blend_main_prepare_operator(
state, spec->operator_id.c_str(), properties.dump().c_str(), command_json, error)) {
return false;
}
step["operator"] = spec->operator_id;
step["command"] = json::parse(command_json);
}
plan["steps"].push_back(std::move(step));
plan_json = plan.dump();
return true;
}
constexpr char WEB_ENGINE_GEOMETRY_OVERRIDE_TEXT[] = "WBV2 Geometry Node Overrides";
constexpr char WEB_ENGINE_GEOMETRY_OVERRIDE_PROPERTY[] = "web_geometry_node_graph_overrides";
constexpr char WEB_ENGINE_GEOMETRY_GRAPH_PROPERTY[] = "web_geometry_node_graph";
@@ -1987,6 +2164,189 @@ bool web_engine_blend_main_set_rna_property(WebBlendMainState *state,
return true;
}
bool web_engine_blend_main_read_rna_property_schema(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property_name,
std::string &result_json,
std::string &error)
{
ID *id = web_rna_find_id(state, type_filter, name);
if (id == nullptr || property_name == nullptr || property_name[0] == '\0') {
error = "RNA data-block or property was not found in authoritative Main";
return false;
}
PointerRNA pointer;
RNA_id_pointer_create(id, &pointer);
PropertyRNA *property = RNA_struct_find_property(&pointer, property_name);
if (property == nullptr || (web_rna_property_blocked(property) && RNA_property_type(property) != PROP_COLLECTION)) {
error = "RNA property is unsupported, context-only, or contains a pointer";
return false;
}
const int type = RNA_property_type(property);
const char *type_name = type == PROP_BOOLEAN ? "BOOLEAN" :
type == PROP_INT ? "INT" :
type == PROP_FLOAT ? "FLOAT" :
type == PROP_STRING ? "STRING" :
type == PROP_ENUM ? "ENUM" :
type == PROP_COLLECTION ? "COLLECTION" : "UNSUPPORTED";
if (STREQ(type_name, "UNSUPPORTED")) {
error = "RNA property type is not supported by the Web editor";
return false;
}
json schema = {{"identifier", RNA_property_identifier(property)},
{"name", RNA_property_ui_name(property)},
{"type", type_name},
{"arrayLength", std::max(1, RNA_property_array_length(&pointer, property))},
{"readonly", RNA_property_is_readonly(&pointer, property)}};
const char *description = RNA_property_ui_description(property);
if (description != nullptr && description[0] != '\0') schema["description"] = description;
if (type == PROP_COLLECTION) schema["collection"] = json{{"nestedPath", property_name}};
result_json = schema.dump();
return true;
}
bool web_engine_blend_main_set_rna_array_property(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property_name,
const char *value_json,
std::string &error)
{
ID *id = web_rna_find_id(state, type_filter, name);
if (id == nullptr || id->lib != nullptr || property_name == nullptr || value_json == nullptr) {
error = "RNA data-block is missing or linked and cannot be edited";
return false;
}
PointerRNA pointer;
RNA_id_pointer_create(id, &pointer);
PropertyRNA *property = RNA_struct_find_property(&pointer, property_name);
if (property == nullptr || RNA_property_array_length(&pointer, property) <= 1) {
error = "RNA property is not a bounded array";
return false;
}
json value;
try {
value = json::parse(value_json);
}
catch (...) {
error = "RNA array value must be valid JSON";
return false;
}
if (!value.is_array() || value.size() > 64) {
error = "RNA array property requires a bounded array value";
return false;
}
return web_rna_set_value(&pointer, property, value, error);
}
bool web_engine_blend_main_set_rna_enum_property(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property_name,
const char *value_json,
std::string &error)
{
ID *id = web_rna_find_id(state, type_filter, name);
if (id == nullptr || id->lib != nullptr || property_name == nullptr || value_json == nullptr) {
error = "RNA data-block is missing or linked and cannot be edited";
return false;
}
PointerRNA pointer;
RNA_id_pointer_create(id, &pointer);
PropertyRNA *property = RNA_struct_find_property(&pointer, property_name);
if (property == nullptr || RNA_property_type(property) != PROP_ENUM) {
error = "RNA property is not an enum";
return false;
}
json value;
try {
value = json::parse(value_json);
}
catch (...) {
error = "RNA enum value must be valid JSON";
return false;
}
if (!(value.is_string() || value.is_number_integer())) {
error = "RNA enum value must be an identifier or integer";
return false;
}
return web_rna_set_value(&pointer, property, value, error);
}
static bool web_rna_read_collection_path(PointerRNA *pointer,
const std::vector<std::string> &segments,
const size_t segment_index,
const int limit,
json &result,
std::string &error)
{
if (pointer == nullptr || segment_index >= segments.size() || segment_index > 4) {
error = "RNA collection path is too deep";
return false;
}
PropertyRNA *property = RNA_struct_find_property(pointer, segments[segment_index].c_str());
if (property == nullptr || RNA_property_type(property) != PROP_COLLECTION) {
error = "RNA collection path does not resolve to a collection";
return false;
}
result = json::array();
CollectionPropertyIterator iterator;
RNA_property_collection_begin(pointer, property, &iterator);
int index = 0;
for (; iterator.valid && index < std::clamp(limit, 1, 1024); RNA_property_collection_next(&iterator), ++index) {
PointerRNA item = iterator.ptr;
const char *item_type = item.type != nullptr ? RNA_struct_identifier(item.type) : nullptr;
json entry = {{"index", index}, {"type", item_type != nullptr ? item_type : ""}};
if (segment_index + 1 < segments.size()) {
json nested;
if (!web_rna_read_collection_path(&item, segments, segment_index + 1, limit, nested, error)) {
RNA_property_collection_end(&iterator);
return false;
}
entry["items"] = nested;
}
result.push_back(entry);
}
RNA_property_collection_end(&iterator);
return true;
}
bool web_engine_blend_main_read_rna_collection(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *path,
const int limit,
std::string &result_json,
std::string &error)
{
ID *id = web_rna_find_id(state, type_filter, name);
if (id == nullptr || path == nullptr || path[0] == '\0') {
error = "RNA data-block or collection path was not found in authoritative Main";
return false;
}
std::vector<std::string> segments;
std::string path_value(path);
size_t start = 0;
while (start < path_value.size()) {
const size_t end = path_value.find('.', start);
const size_t length = end == std::string::npos ? path_value.size() - start : end - start;
if (length == 0 || length > 64) {
error = "RNA collection path contains an empty or oversized segment";
return false;
}
segments.push_back(path_value.substr(start, length));
if (end == std::string::npos) break;
start = end + 1;
}
PointerRNA pointer;
RNA_id_pointer_create(id, &pointer);
json items;
if (!web_rna_read_collection_path(&pointer, segments, 0, limit, items, error)) return false;
result_json = json{{"path", path_value}, {"items", items}}.dump();
return true;
}
bool web_engine_blend_main_list_rna_collections(WebBlendMainState *state,
const char *type_filter,
const char *name,

View File

@@ -215,6 +215,31 @@ bool web_engine_blend_main_set_rna_property(WebBlendMainState *state,
const char *property,
const char *value_json,
std::string &error);
bool web_engine_blend_main_read_rna_property_schema(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property,
std::string &result_json,
std::string &error);
bool web_engine_blend_main_set_rna_array_property(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property,
const char *value_json,
std::string &error);
bool web_engine_blend_main_set_rna_enum_property(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *property,
const char *value_json,
std::string &error);
bool web_engine_blend_main_read_rna_collection(WebBlendMainState *state,
const char *type_filter,
const char *name,
const char *path,
int limit,
std::string &result_json,
std::string &error);
bool web_engine_blend_main_list_rna_collections(WebBlendMainState *state,
const char *type_filter,
const char *name,