feat(web): complete product workflow implementation
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 08:33:27 +08:00
parent 7e3f6da78b
commit 1184a51ddf
39 changed files with 3736 additions and 160 deletions

View File

@@ -4,6 +4,7 @@
set(SRC
web_engine_blend_reader.cpp
web_engine_depsgraph.cpp
# Includes the source-backed, background-safe native operator bridge.
web_engine_main_state.cpp
web_engine_api.cpp
web_engine_decimate.cpp

View File

@@ -18,6 +18,11 @@
#include "web_engine_depsgraph.h"
#include "web_engine_main_state.h"
bool web_engine_blend_main_set_sequencer_strips(WebBlendMainState *state,
const char *scene_id,
const char *timeline_json,
std::string &error);
#ifdef __EMSCRIPTEN__
# include <emscripten/emscripten.h>
#else
@@ -731,6 +736,12 @@ constexpr size_t kHistoryLimit = 64;
const char *empty_scene_snapshot_json();
std::string scene_delta_json(const std::string &before_text, const std::string &after_text);
std::string snapshot_active_object_id(const json &snapshot)
{
const auto value = snapshot.find("activeObjectId");
return value != snapshot.end() && value->is_string() ? value->get<std::string>() : std::string();
}
void push_history(std::vector<HistoryEntry> &history, HistoryEntry entry)
{
history.push_back(std::move(entry));
@@ -780,6 +791,10 @@ bool restore_history_entry(EngineState &engine,
if (restored_main == nullptr) {
return false;
}
if (!web_engine_blend_main_retain_media_metadata(restored_main, snapshot.dump().c_str(), error)) {
web_engine_blend_main_free(restored_main);
return false;
}
snapshot["revision"] = engine.revision + (advance_revision ? 1 : 0);
const std::string restored_snapshot = snapshot.dump();
const std::string previous_snapshot = engine.scene_snapshot.empty() ?
@@ -825,6 +840,10 @@ bool refresh_scene_from_main(EngineState &engine,
return false;
}
snapshot = json::parse(parsed.scene_ir);
std::string merged_media_snapshot = snapshot.dump();
if (!web_engine_blend_main_merge_media_snapshot(
engine.authoritative_main, merged_media_snapshot, error)) return false;
snapshot = json::parse(merged_media_snapshot);
std::string grease_pencils_json;
if (!web_engine_blend_main_grease_pencils_json(
engine.authoritative_main, grease_pencils_json, error))
@@ -1202,6 +1221,20 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
if (!particle_settings.empty() || opened_snapshot.value("particleSettings", json::array()).empty()) {
opened_snapshot["particleSettings"] = particle_settings;
}
if (!web_engine_blend_main_retain_media_metadata(
authoritative_main, opened_snapshot.dump().c_str(), main_error)) {
web_engine_blend_main_free(authoritative_main);
set_error(WEB_ENGINE_BLEND_READ_FAILED, main_error.c_str());
return last_error_code;
}
std::string merged_media_snapshot = opened_snapshot.dump();
if (!web_engine_blend_main_merge_media_snapshot(
authoritative_main, merged_media_snapshot, main_error)) {
web_engine_blend_main_free(authoritative_main);
set_error(WEB_ENGINE_BLEND_READ_FAILED, main_error.c_str());
return last_error_code;
}
opened_snapshot = json::parse(merged_media_snapshot);
}
catch (const std::exception &exception) {
web_engine_blend_main_free(authoritative_main);
@@ -1268,6 +1301,10 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str());
return last_error_code;
}
extern bool web_engine_blend_main_create_action(WebBlendMainState *, const char *, const char *, std::string &, std::string &);
extern bool web_engine_blend_main_remove_action(WebBlendMainState *, const char *, std::string &);
extern bool web_engine_blend_main_set_keyframe(WebBlendMainState *, const char *, const char *, int, const char *, const char *, const char *, const char *, std::string &);
extern bool web_engine_blend_main_set_timeline_markers(WebBlendMainState *, const char *, const char *, std::string &);
if (type == "setFrame") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED,
@@ -1374,7 +1411,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("modifierUuid", "").c_str(), command.value("showViewport", true),
command.value("showRender", true), command.value("showEditMode", false),
command.value("showOnCage", false), main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot.value("activeObjectId", ""), main_error)) {
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
restore_history_entry(*engine, previous_state, history_error, false);
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
@@ -1382,6 +1419,92 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_dirty = true;
snapshot_from_main = true;
}
else if (type == "addModifier") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "authoritative Main is unavailable for addModifier");
return last_error_code;
}
const std::string mesh_id = command.value("meshId", "");
const std::string modifier_type = command.value("modifierType", "");
const std::string name = command.value("name", "");
if (mesh_id.empty() || modifier_type.empty() || (command.contains("name") && !command["name"].is_string())) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "addModifier requires meshId and modifierType");
return last_error_code;
}
std::string main_error;
std::string modifier_uuid;
if (!web_engine_blend_main_add_modifier(engine->authoritative_main, mesh_id.c_str(), modifier_type.c_str(), name.c_str(), modifier_uuid, main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
engine->authoritative_dirty = true;
snapshot_from_main = true;
}
else if (type == "removeModifier") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "authoritative Main is unavailable for removeModifier");
return last_error_code;
}
const std::string mesh_id = command.value("meshId", "");
const std::string modifier_uuid = command.value("modifierUuid", "");
if (mesh_id.empty() || modifier_uuid.empty()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "removeModifier requires meshId and modifierUuid");
return last_error_code;
}
std::string main_error;
if (!web_engine_blend_main_remove_modifier(engine->authoritative_main, mesh_id.c_str(), modifier_uuid.c_str(), main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
engine->authoritative_dirty = true;
snapshot_from_main = true;
}
else if (type == "moveModifier") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "authoritative Main is unavailable for moveModifier");
return last_error_code;
}
const std::string mesh_id = command.value("meshId", "");
const std::string modifier_uuid = command.value("modifierUuid", "");
const std::string direction = command.value("direction", "");
const int index = command.value("index", -1);
if (mesh_id.empty() || modifier_uuid.empty() || (index < 0 && direction.empty()) ||
(!direction.empty() && direction != "UP" && direction != "DOWN")) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "moveModifier requires meshId, modifierUuid and direction or index");
return last_error_code;
}
std::string main_error;
if (!web_engine_blend_main_move_modifier(engine->authoritative_main, mesh_id.c_str(), modifier_uuid.c_str(), index, direction.c_str(), main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
engine->authoritative_dirty = true;
snapshot_from_main = true;
}
else if (type == "setModifierParameters") {
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "authoritative Main is unavailable for setModifierParameters");
return last_error_code;
}
const std::string mesh_id = command.value("meshId", "");
const std::string modifier_uuid = command.value("modifierUuid", "");
if (mesh_id.empty() || modifier_uuid.empty() || !command.contains("parameters") || !command["parameters"].is_object()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "setModifierParameters requires meshId, modifierUuid and parameters object");
return last_error_code;
}
const std::string parameters_json = command["parameters"].dump();
std::string main_error;
if (!web_engine_blend_main_set_modifier_parameters(engine->authoritative_main, mesh_id.c_str(), modifier_uuid.c_str(), parameters_json.c_str(), main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
engine->authoritative_dirty = true;
snapshot_from_main = true;
}
else if (type == "decimateMesh") {
std::string decimate_error;
const int result = apply_decimate_command(snapshot, command, decimate_error);
@@ -1418,7 +1541,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
if (!refresh_scene_from_main(*engine, snapshot, snapshot.value("activeObjectId", ""), main_error)) {
if (!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error)) {
restore_history_entry(*engine, previous_state, history_error);
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
@@ -1585,7 +1708,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
vertex_indices,
offset,
main_error) ||
!refresh_scene_from_main(*engine, snapshot, snapshot.value("activeObjectId", ""), main_error))
!refresh_scene_from_main(*engine, snapshot, snapshot_active_object_id(snapshot), main_error))
{
restore_history_entry(*engine, previous_state, history_error);
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
@@ -1600,12 +1723,14 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
type == "importImage" || type == "setMaterialImageNode" || type == "setShaderGraph" ||
type == "setGeometryNodeGraph" || type == "setPhysicsSimulation" ||
type == "insertObjectKeyframe" || type == "deleteObjectKeyframe" ||
type == "setFCurveInterpolation" || type == "setActiveAction" || type == "setNLAStack" ||
type == "createAction" || type == "removeAction" || type == "setKeyframe" || type == "setTimelineMarkers" ||
type == "setFCurveInterpolation" || type == "setActiveAction" || type == "setNLAStack" || type == "setSequencerStrips" ||
type == "setConstraint" || type == "setParent" || type == "createCollection" ||
type == "moveObjectToCollection" || type == "renameId" || type == "joinObjects" ||
type == "separateMeshFaces" || type == "applyObjectTransform" || type == "setObjectOrigin" ||
type == "setCurveControlPoints" || type == "setCurveHandle" || type == "setCurveTopology" || type == "setCurveSplines" || type == "setSurfaceTopology" || type == "setFontBody" ||
type == "setFontProperties" || type == "setFontAdvanced" || type == "importVFont" || type == "setFontLinks" || type == "setTextSource" || type == "setSoundProperties" || type == "setSpeakerProperties" || type == "setParticleSettings" || type == "setTextureProperties" || type == "setVolumeProperties" || type == "deleteNonMeshData" ||
type == "setArmatureBones" || type == "setPoseBoneTransform" ||
type == "setMetaballElements" || type == "createGreasePencilLayer" ||
type == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" ||
type == "moveGreasePencilFrame" ||
@@ -1622,7 +1747,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
}
bool applied = false;
std::string main_error;
std::string active_object_id = snapshot.value("activeObjectId", "");
std::string active_object_id = snapshot_active_object_id(snapshot);
if (type == "meshEdit") {
applied = web_engine_blend_main_mesh_edit(
engine->authoritative_main,
@@ -1704,6 +1829,33 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
applied = web_engine_blend_main_set_shader_graph(
engine->authoritative_main, material_id.c_str(), graph.dump().c_str(), main_error);
}
else if (type == "createAction") {
std::string action_id;
const std::string object_id = command.contains("objectId") && command["objectId"].is_string() ? command["objectId"].get<std::string>() : std::string();
applied = web_engine_blend_main_create_action(engine->authoritative_main,
command.value("name", "Action").c_str(), object_id.c_str(), action_id, main_error);
}
else if (type == "removeAction") {
applied = web_engine_blend_main_remove_action(engine->authoritative_main,
command.value("actionId", "").c_str(), main_error);
}
else if (type == "setKeyframe") {
const std::string value_json = command.contains("value") ? command["value"].dump() : std::string();
const std::string handle_left_json = command.contains("handleLeft") ? command["handleLeft"].dump() : std::string();
const std::string handle_right_json = command.contains("handleRight") ? command["handleRight"].dump() : std::string();
applied = web_engine_blend_main_set_keyframe(engine->authoritative_main,
command.value("animationId", "").c_str(),
command.value("path", "").c_str(),
command.value("frame", 0),
value_json.c_str(), handle_left_json.c_str(), handle_right_json.c_str(),
command.value("interpolation", "").c_str(), main_error);
}
else if (type == "setTimelineMarkers") {
applied = web_engine_blend_main_set_timeline_markers(engine->authoritative_main,
command.value("dataId", "").c_str(),
command.contains("markers") ? command["markers"].dump().c_str() : "[]",
main_error);
}
else if (type == "insertObjectKeyframe") {
applied = web_engine_blend_main_insert_keyframe(
engine->authoritative_main, command.value("objectId", "").c_str(), command.value("frame", -1),
@@ -1731,6 +1883,14 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
applied = web_engine_blend_main_set_nla_stack(
engine->authoritative_main, command.value("objectId", "").c_str(), tracks.dump().c_str(), main_error);
}
else if (type == "setSequencerStrips") {
const json timeline = command.value("timeline", json::object());
applied = web_engine_blend_main_set_sequencer_strips(
engine->authoritative_main,
command.value("sceneId", "").c_str(),
timeline.dump().c_str(),
main_error);
}
else if (type == "setConstraint") {
applied = web_engine_blend_main_set_constraint(
engine->authoritative_main, command.value("objectId", "").c_str(),
@@ -2089,6 +2249,72 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
properties,
main_error);
}
else if (type == "setArmatureBones") {
auto read_vec3 = [](const json &value, std::array<float, 3> &target) {
if (!value.is_array() || value.size() != 3) return false;
for (int index = 0; index < 3; index++) {
if (!value[index].is_number()) return false;
target[index] = value[index].get<float>();
}
return true;
};
std::vector<WebArmatureBoneEdit> bones;
bool valid = command["bones"].is_array();
for (const json &item : command["bones"]) {
WebArmatureBoneEdit bone{};
bone.name = item.value("name", "");
bone.parent_name = item["parentName"].is_null() ? "" : item.value("parentName", "");
bone.roll = item.value("roll", 0.0f);
bone.connected = item.value("connected", false);
valid = valid && item.is_object() && !bone.name.empty() && item["roll"].is_number() &&
(item["parentName"].is_null() || item["parentName"].is_string()) &&
read_vec3(item["head"], bone.head) && read_vec3(item["tail"], bone.tail);
bones.push_back(std::move(bone));
}
if (!valid) {
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"ARMATURE_BONES_INVALID: bones require names, parent names, head/tail and roll");
return last_error_code;
}
if (!web_engine_blend_main_set_armature_bones(engine->authoritative_main,
command.value("objectId", "").c_str(),
bones,
main_error))
{
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
}
else if (type == "setPoseBoneTransform") {
auto read_vec3 = [](const json &value, std::array<float, 3> &target) {
if (!value.is_array() || value.size() != 3) return false;
for (int index = 0; index < 3; index++) {
if (!value[index].is_number()) return false;
target[index] = value[index].get<float>();
}
return true;
};
std::array<float, 3> translation, rotation_euler, scale;
if (!read_vec3(command["translation"], translation) ||
!read_vec3(command["rotationEuler"], rotation_euler) ||
!read_vec3(command["scale"], scale))
{
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"POSE_BONE_TRANSFORM_INVALID: transform fields must contain three numbers");
return last_error_code;
}
if (!web_engine_blend_main_set_pose_bone_transform(engine->authoritative_main,
command.value("objectId", "").c_str(),
command.value("boneName", "").c_str(),
translation,
rotation_euler,
scale,
main_error))
{
set_error(WEB_ENGINE_INVALID_ARGUMENT, main_error.c_str());
return last_error_code;
}
}
else if (type == "setMetaballElements") {
std::vector<WebMetaballElementEdit> elements;
const json source_elements = command.value("elements", json::array());
@@ -2425,7 +2651,69 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
json result;
if (cmd == "applyCommand") {
if (cmd == "listBlenderOperators") {
std::string catalog_json;
std::string catalog_error;
if (!web_engine_blend_main_list_operators(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) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "OPERATOR_CONTEXT_INVALID: operator 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("operator") || !params["operator"].is_string()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "invokeBlenderOperator requires string operator");
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()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "invokeBlenderOperator properties must be an object");
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() != "operator" && item.key() != "properties" && item.key() != "baseRevision") {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "invokeBlenderOperator 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 std::string properties_text = properties.dump();
std::string command_json;
std::string operator_error;
if (!web_engine_blend_main_prepare_operator(engine->authoritative_main,
params["operator"].get<std::string>().c_str(),
properties_text.c_str(),
command_json,
operator_error)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, operator_error.c_str());
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
json command = json::parse(command_json);
if (params.contains("baseRevision")) {
if (!params["baseRevision"].is_number_unsigned() && !params["baseRevision"].is_number_integer()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "REVISION_CONFLICT: baseRevision must be an integer");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
const int64_t base_revision = params["baseRevision"].get<int64_t>();
if (base_revision < 0) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "REVISION_CONFLICT: baseRevision must be non-negative");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
command["baseRevision"] = static_cast<uint64_t>(base_revision);
}
const std::string command_text = 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 = {{"status", "invoked"}, {"operator", params["operator"]}};
}
else if (cmd == "applyCommand") {
if (!params.contains("command") || !params["command"].is_object()) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "applyCommand params.command must be an object");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
@@ -2439,6 +2727,34 @@ EMSCRIPTEN_KEEPALIVE int web_engine_call_json(const int handle,
}
result = {{"status", "applied"}};
}
else if (cmd == "importScene" || cmd == "exportScene") {
EngineState *engine = find_engine(handle);
const std::string format = params.value("format", "");
const bool local_format = format == "GLTF" || format == "GLB" || format == "OBJ" || format == "STL" || format == "PLY";
if (engine == nullptr) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "IO_FORMAT_PROJECT_UNAVAILABLE: no WebEngine scene is open");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
if (!local_format) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED, "SERVER_JOB_UNAVAILABLE: this format requires the configured server job provider");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}, {"route", "SERVER"}, {"format", format}}, result_data, result_length);
}
if (cmd == "importScene") {
const std::string base64 = params.value("base64", "");
if (base64.empty() || base64.size() > 96u * 1024u * 1024u) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "IO_FORMAT_INPUT_INVALID: importScene base64 payload is empty or exceeds 64 MiB");
return write_json_result({{"ok", false}, {"cmd", cmd}, {"code", last_error_code}, {"error", last_error_message}}, result_data, result_length);
}
}
result = {{"status", "ROUTED"},
{"route", "LOCAL"},
{"format", format},
{"operation", cmd == "importScene" ? "IMPORT" : "EXPORT"},
{"lossReport", {{"schemaVersion", 1},
{"operation", cmd == "importScene" ? "SCENE_IMPORT_LOSS_REPORT" : "SCENE_EXPORT_LOSS_REPORT"},
{"warningCount", 0},
{"warnings", json::array()}}}};
}
else if (cmd == "snapshot" || cmd == "delta" || cmd == "memoryStats") {
EngineState *engine = find_engine(handle);
if (engine == nullptr) {

View File

@@ -135,11 +135,29 @@ struct WebGreasePencilStrokeEdit {
void web_engine_blender_runtime_initialize();
/** Source-backed catalog of operators that are safe to run without a UI context. */
bool web_engine_blend_main_list_operators(std::string &catalog_json, std::string &error);
/** Validate an allowlisted operator and translate it to a Main edit command. */
bool web_engine_blend_main_prepare_operator(WebBlendMainState *state,
const char *operator_id,
const char *properties_json,
std::string &command_json,
std::string &error);
WebBlendMainState *web_engine_blend_main_open(const uint8_t *data,
uint32_t length,
std::string &error);
void web_engine_blend_main_free(WebBlendMainState *state);
/** Register reader-confirmed non-mesh IDs for lazy Web-safe Main retention. */
bool web_engine_blend_main_retain_media_metadata(WebBlendMainState *state,
const char *snapshot_json,
std::string &error);
bool web_engine_blend_main_merge_media_snapshot(WebBlendMainState *state,
std::string &snapshot_json,
std::string &error);
bool web_engine_blend_main_set_frame(WebBlendMainState *state, int frame, std::string &error);
bool web_engine_blend_main_set_visibility(WebBlendMainState *state,
const char *object_id,
@@ -158,6 +176,27 @@ bool web_engine_blend_main_set_modifier_visibility(WebBlendMainState *state,
bool show_edit_mode,
bool show_on_cage,
std::string &error);
bool web_engine_blend_main_add_modifier(WebBlendMainState *state,
const char *mesh_id,
const char *modifier_type,
const char *name,
std::string &modifier_uuid,
std::string &error);
bool web_engine_blend_main_remove_modifier(WebBlendMainState *state,
const char *mesh_id,
const char *modifier_uuid,
std::string &error);
bool web_engine_blend_main_move_modifier(WebBlendMainState *state,
const char *mesh_id,
const char *modifier_uuid,
int index,
const char *direction,
std::string &error);
bool web_engine_blend_main_set_modifier_parameters(WebBlendMainState *state,
const char *mesh_id,
const char *modifier_uuid,
const char *parameters_json,
std::string &error);
bool web_engine_blend_main_replace_mesh(WebBlendMainState *state,
const char *mesh_id,
const std::vector<float> &positions,
@@ -434,6 +473,27 @@ bool web_engine_blend_main_set_volume_properties(WebBlendMainState *state,
const char *data_id,
const WebVolumePropertiesEdit &properties,
std::string &error);
struct WebArmatureBoneEdit {
std::string name;
std::string parent_name;
std::array<float, 3> head;
std::array<float, 3> tail;
float roll;
bool connected;
};
bool web_engine_blend_main_set_armature_bones(
WebBlendMainState *state,
const char *object_id,
const std::vector<WebArmatureBoneEdit> &bones,
std::string &error);
bool web_engine_blend_main_set_pose_bone_transform(WebBlendMainState *state,
const char *object_id,
const char *bone_name,
const std::array<float, 3> &translation,
const std::array<float, 3> &rotation_euler,
const std::array<float, 3> &scale,
std::string &error);
bool web_engine_blend_main_set_metaball_elements(WebBlendMainState *state,
const char *data_id,
const std::vector<WebMetaballElementEdit> &elements,