feat(web): complete product workflow implementation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user