Govern task context and advance execution pointer
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-08-20 06:02:43 -04:00
parent 380cbed4ff
commit 10640aeb3c
984 changed files with 543475 additions and 327 deletions

View File

@@ -843,6 +843,16 @@ bool refresh_scene_from_main(EngineState &engine,
return false;
}
snapshot["physicsSimulation"] = json::parse(physics_json);
std::string particle_settings_json;
if (!web_engine_blend_main_particle_settings_json(
engine.authoritative_main, particle_settings_json, error))
{
return false;
}
const json particle_settings = json::parse(particle_settings_json)["settings"];
if (!particle_settings.empty() || snapshot.value("particleSettings", json::array()).empty()) {
snapshot["particleSettings"] = particle_settings;
}
if (!active_object_id.empty()) snapshot["activeObjectId"] = active_object_id;
engine.blend_bytes.assign(reinterpret_cast<const char *>(bytes.data()), bytes.size());
engine.packed_assets = parsed.packed_assets;
@@ -860,7 +870,7 @@ const char *empty_scene_snapshot_json()
"\"source\":{\"kind\":\"blend\"},"
"\"coordinateSystem\":{\"upAxis\":\"Z\",\"forwardAxis\":\"-Y\",\"handedness\":\"RIGHT\",\"unitSystem\":0,\"unitScale\":1},"
"\"nodes\":[],\"meshes\":[],\"materials\":[],\"cameras\":[],\"lights\":[],\"worlds\":[],\"images\":[],\"animations\":[],"
"\"collections\":[],\"scenes\":[],\"activeObjectId\":null,"
"\"collections\":[],\"scenes\":[],\"particleSettings\":[],\"sounds\":[],\"activeObjectId\":null,"
"\"frame\":{\"current\":1,\"start\":1,\"end\":250}}";
}
@@ -1157,6 +1167,18 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
return last_error_code;
}
opened_snapshot["physicsSimulation"] = json::parse(physics_json);
std::string particle_settings_json;
if (!web_engine_blend_main_particle_settings_json(
authoritative_main, particle_settings_json, main_error))
{
web_engine_blend_main_free(authoritative_main);
set_error(WEB_ENGINE_BLEND_READ_FAILED, main_error.c_str());
return last_error_code;
}
const json particle_settings = json::parse(particle_settings_json)["settings"];
if (!particle_settings.empty() || opened_snapshot.value("particleSettings", json::array()).empty()) {
opened_snapshot["particleSettings"] = particle_settings;
}
}
catch (const std::exception &exception) {
web_engine_blend_main_free(authoritative_main);

View File

@@ -32,6 +32,7 @@
#include "DNA_genfile.h"
#include "DNA_curve_enums.h"
#include "DNA_lattice_types.h"
#include "DNA_mask_types.h"
#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
@@ -48,6 +49,7 @@ using namespace blender;
constexpr uint64_t MAX_BLEND_BLOCK_BYTES = UINT64_C(1024) * 1024 * 1024;
constexpr size_t MAX_NON_MESH_POINTS = 1000000;
constexpr size_t MAX_LATTICE_POINTS = 64 * 64 * 64;
constexpr size_t MAX_SCRIPT_SOURCE_BYTES = 1024 * 1024;
uint32_t rotate_right(const uint32_t value, const uint32_t bits)
@@ -539,6 +541,13 @@ std::string id_prefix(const std::string &type_name)
if (type_name == "Library") return "library";
if (type_name == "WorkSpace") return "workspace";
if (type_name == "bScreen") return "screen";
if (type_name == "Brush") return "brush";
if (type_name == "FreestyleLineStyle") return "line-style";
if (type_name == "Lattice") return "lattice";
if (type_name == "ParticleSettings") return "particle-settings";
if (type_name == "bSound") return "sound";
if (type_name == "Speaker") return "speaker";
if (type_name == "Tex") return "texture";
return "datablock";
}
@@ -552,7 +561,9 @@ bool is_exported_id_type(const std::string &type_name)
type_name == "MetaBall" || type_name == "PointCloud" || type_name == "Curves" ||
type_name == "Volume" || type_name == "GreasePencil" || type_name == "Text" ||
type_name == "VFont" || type_name == "Mask" || type_name == "Library" ||
type_name == "WorkSpace" || type_name == "bScreen";
type_name == "WorkSpace" || type_name == "bScreen" || type_name == "Brush" ||
type_name == "FreestyleLineStyle" || type_name == "Lattice" ||
type_name == "ParticleSettings" || type_name == "bSound" || type_name == "Speaker" || type_name == "Tex";
}
std::string unique_id(const std::string &prefix,
@@ -613,6 +624,17 @@ std::string object_type_name(const int64_t type)
}
}
const char *lattice_interpolation_name(const int64_t type)
{
switch (type) {
case KEY_LINEAR: return "KEY_LINEAR";
case KEY_CARDINAL: return "KEY_CARDINAL";
case KEY_BSPLINE: return "KEY_BSPLINE";
case KEY_CATMULL_ROM: return "KEY_CATMULL_ROM";
default: return "UNKNOWN";
}
}
std::array<float, 16> trs_matrix(const std::vector<float> &location,
const std::vector<float> &rotation,
const std::vector<float> &scale)
@@ -1452,8 +1474,8 @@ std::string modifier_type_name(const int64_t type)
"GREASE_PENCIL_OPACITY", "GREASE_PENCIL_SUBDIV", "GREASE_PENCIL_COLOR", "GREASE_PENCIL_TINT",
"GREASE_PENCIL_SMOOTH", "GREASE_PENCIL_OFFSET", "GREASE_PENCIL_NOISE", "GREASE_PENCIL_MIRROR",
"GREASE_PENCIL_THICKNESS", "GREASE_PENCIL_LATTICE", "GREASE_PENCIL_DASH", "GREASE_PENCIL_MULTIPLY",
"GREASE_PENCIL_LENGTH", "GREASE_PENCIL_WEIGHT_ANGLE", "GREASE_PENCIL_ARRAY",
"GREASE_PENCIL_WEIGHT_PROXIMITY", "GREASE_PENCIL_HOOK", "GREASE_PENCIL_LINEART",
"GREASE_PENCIL_LENGTH", "GREASE_PENCIL_VERTEX_WEIGHT_ANGLE", "GREASE_PENCIL_ARRAY",
"GREASE_PENCIL_VERTEX_WEIGHT_PROXIMITY", "GREASE_PENCIL_HOOK", "GREASE_PENCIL_LINEART",
"GREASE_PENCIL_ARMATURE", "GREASE_PENCIL_TIME", "GREASE_PENCIL_ENVELOPE",
"GREASE_PENCIL_OUTLINE", "GREASE_PENCIL_SHRINKWRAP", "GREASE_PENCIL_BUILD",
"GREASE_PENCIL_SIMPLIFY", "GREASE_PENCIL_TEXTURE"};
@@ -1501,7 +1523,8 @@ json modifier_parameters(const ParsedBlend &blend,
case 4:
parameters["start"] = read_float(*blend.sdna, modifier, "start").value_or(1.0f);
parameters["length"] = read_float(*blend.sdna, modifier, "length").value_or(100.0f);
parameters["randomize"] = read_float(*blend.sdna, modifier, "randomize").value_or(0.0f);
parameters["randomize"] = read_integer(*blend.sdna, modifier, "randomize").value_or(0);
parameters["seed"] = read_integer(*blend.sdna, modifier, "seed").value_or(0);
break;
case 5:
parameters["flag"] = read_integer(*blend.sdna, modifier, "flag").value_or(
@@ -3124,6 +3147,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
json lights = json::array();
json worlds = json::array();
json images = json::array();
json brushes = json::array();
json line_styles = json::array();
json lattices = json::array();
json particle_settings = json::array();
json sounds = json::array();
json speakers = json::array();
json texts = json::array();
json textures = json::array();
json workspace_resources = json::array();
json non_mesh_data = json::array();
json vfonts = json::array();
json libraries = json::array();
@@ -3289,10 +3321,12 @@ json scene_ir_from_blend(const ParsedBlend &blend,
if (data_pointer) {
const auto data_id = ids_by_pointer.find(*data_pointer);
node["dataId"] = data_id != ids_by_pointer.end() ? json(data_id->second) : json(nullptr);
const json object_modifier_stack = modifier_stack_from_object(blend, element, record.id, ids_by_pointer);
if (!object_modifier_stack.empty()) node["modifierStack"] = object_modifier_stack;
if (data_id != ids_by_pointer.end()) {
if (const std::optional<ElementRef> data = element_for_pointer(blend, *data_pointer)) {
if (data->type_name == "Mesh") {
const json stack = modifier_stack_from_object(blend, element, record.id, ids_by_pointer);
const json stack = object_modifier_stack;
if (!stack.empty()) modifier_stacks_by_mesh_id.emplace(data_id->second, stack);
}
}
@@ -3527,6 +3561,155 @@ json scene_ir_from_blend(const ParsedBlend &blend,
if (!warnings.empty()) material["warnings"] = warnings;
materials.push_back(std::move(material));
}
else if (record.type_name == "Brush") {
json brush = {
{"id", record.id},
{"name", record.name},
{"size", std::clamp<int64_t>(read_integer(*blend.sdna, element, "size").value_or(0), 0, 100000)},
{"alpha", std::clamp<float>(read_float(*blend.sdna, element, "alpha").value_or(1.0f), 0.0f, 1.0f)},
{"hardness", std::clamp<float>(read_float(*blend.sdna, element, "hardness").value_or(0.0f), 0.0f, 1.0f)},
{"spacing", std::clamp<int64_t>(read_integer(*blend.sdna, element, "spacing").value_or(0), 1, 1000)},
{"jitter", std::clamp<float>(read_float(*blend.sdna, element, "jitter").value_or(0.0f), 0.0f, 1.0f)},
{"sculptTool", read_integer(*blend.sdna, element, "sculpt_brush_type").value_or(0)},
};
brushes.push_back(std::move(brush));
}
else if (record.type_name == "FreestyleLineStyle") {
const std::vector<float> color = {read_float(*blend.sdna, element, "r").value_or(0.0f),
read_float(*blend.sdna, element, "g").value_or(0.0f),
read_float(*blend.sdna, element, "b").value_or(0.0f)};
const int64_t flag = read_integer(*blend.sdna, element, "flag").value_or(0);
line_styles.push_back({
{"id", record.id},
{"name", record.name},
{"color", float_array_or(color, {0.0f, 0.0f, 0.0f})},
{"alpha", std::clamp(read_float(*blend.sdna, element, "alpha").value_or(1.0f), 0.0f, 1.0f)},
{"thickness", std::clamp(read_float(*blend.sdna, element, "thickness").value_or(1.0f), 0.0f, 10000.0f)},
{"chaining", read_integer(*blend.sdna, element, "chaining").value_or(0)},
{"caps", read_integer(*blend.sdna, element, "caps").value_or(0)},
{"dashed", (flag & (1 << 2)) != 0},
{"modifierCounts", {
{"color", linked_list_elements(blend, element, "color_modifiers").size()},
{"alpha", linked_list_elements(blend, element, "alpha_modifiers").size()},
{"thickness", linked_list_elements(blend, element, "thickness_modifiers").size()},
{"geometry", linked_list_elements(blend, element, "geometry_modifiers").size()}}}
});
}
else if (record.type_name == "Lattice") {
const int64_t points_u = read_integer(*blend.sdna, element, "pntsu").value_or(0);
const int64_t points_v = read_integer(*blend.sdna, element, "pntsv").value_or(0);
const int64_t points_w = read_integer(*blend.sdna, element, "pntsw").value_or(0);
const bool dimensions_valid = points_u >= 1 && points_u <= 64 &&
points_v >= 1 && points_v <= 64 &&
points_w >= 1 && points_w <= 64;
const size_t point_count = dimensions_valid ?
size_t(points_u) * size_t(points_v) * size_t(points_w) :
0;
json points = json::array();
bool points_complete = point_count > 0 && point_count <= MAX_LATTICE_POINTS;
if (const std::optional<uint64_t> points_pointer = read_pointer(*blend.sdna, element, "def");
points_complete && points_pointer)
{
for (size_t index = 0; index < point_count; index++) {
const std::optional<ElementRef> point = raw_array_element(
blend, *points_pointer, "BPoint", index);
if (!point) {
points_complete = false;
break;
}
const std::vector<float> coordinate = read_float_array(
*blend.sdna, *point, "vec", 4);
if (coordinate.size() != 4) {
points_complete = false;
break;
}
points.push_back({
{"coDeform", {coordinate[0], coordinate[1], coordinate[2]}},
{"weight", std::clamp(read_float(*blend.sdna, *point, "weight").value_or(0.0f),
0.01f,
100.0f)},
{"selected", (read_integer(*blend.sdna, *point, "f1").value_or(0) & 1) != 0},
});
}
}
else {
points_complete = false;
}
const int64_t flag = read_integer(*blend.sdna, element, "flag").value_or(0);
json lattice = {
{"id", record.id},
{"name", record.name},
{"dimensions", {points_u, points_v, points_w}},
{"pointCount", point_count},
{"interpolation", {
lattice_interpolation_name(read_integer(*blend.sdna, element, "typeu").value_or(-1)),
lattice_interpolation_name(read_integer(*blend.sdna, element, "typev").value_or(-1)),
lattice_interpolation_name(read_integer(*blend.sdna, element, "typew").value_or(-1))}},
{"useOutside", (flag & LT_OUTSIDE) != 0},
{"activePoint", read_integer(*blend.sdna, element, "actbp").value_or(LT_ACTBP_NONE)},
{"vertexGroup", read_string(*blend.sdna, element, "vgroup")},
{"status", points_complete ? "AVAILABLE" : "BLOCKED"},
};
if (points_complete) lattice["points"] = std::move(points);
else lattice["errorCode"] = dimensions_valid ? "LATTICE_POINTS_UNREADABLE" :
"LATTICE_DIMENSIONS_INVALID";
lattices.push_back(std::move(lattice));
}
else if (record.type_name == "ParticleSettings") {
const int64_t particle_type = read_integer(*blend.sdna, element, "type").value_or(0);
const int64_t particle_from = read_integer(*blend.sdna, element, "from").value_or(0);
const int64_t particle_physics = read_integer(*blend.sdna, element, "phystype").value_or(0);
particle_settings.push_back({
{"id", record.id},
{"name", record.name},
{"type", particle_type == 2 ? 1 : 0},
{"from", particle_from == 1 ? 0 : particle_from == 0 ? 1 : 2},
{"distribution", std::max<int64_t>(0, read_integer(*blend.sdna, element, "distr").value_or(0))},
{"physicsType", particle_physics == 1 ? 0 : particle_physics == 2 ? 1 : particle_physics == 3 ? 2 : 3},
{"totalParticles", std::max<int64_t>(0, read_integer(*blend.sdna, element, "totpart").value_or(0))},
{"start", std::max(0.0f, read_float(*blend.sdna, element, "sta").value_or(0.0f))},
{"end", std::max(0.0f, read_float(*blend.sdna, element, "end").value_or(0.0f))},
{"lifetime", std::max(0.0f, read_float(*blend.sdna, element, "lifetime").value_or(0.0f))},
{"size", std::max(0.0f, read_float(*blend.sdna, element, "size").value_or(0.0f))},
{"drawSize", std::max(0.0f, read_float(*blend.sdna, element, "draw_size").value_or(0.0f))},
});
}
else if (record.type_name == "bSound") {
const std::string filepath = read_string(*blend.sdna, element, "filepath");
const std::optional<uint64_t> packed_pointer = read_pointer(*blend.sdna, element, "packedfile");
const std::vector<uint8_t> packed_bytes = packed_pointer ? packed_file_bytes(blend, *packed_pointer) : std::vector<uint8_t>();
json sound = {{"id", record.id},
{"name", record.name},
{"sourcePath", filepath},
{"packed", !packed_bytes.empty()},
{"volume", std::max(0.0f, read_float(*blend.sdna, element, "volume").value_or(1.0f))},
{"pitch", std::max(0.0f, read_float(*blend.sdna, element, "pitch").value_or(1.0f))},
{"audioChannels", std::max<int64_t>(0, read_integer(*blend.sdna, element, "audio_channels").value_or(0))},
{"sampleRate", std::max<int64_t>(0, read_integer(*blend.sdna, element, "samplerate").value_or(0))}};
if (!packed_bytes.empty()) sound["packedByteLength"] = packed_bytes.size();
sounds.push_back(std::move(sound));
}
else if (record.type_name == "Speaker") {
json speaker = {
{"id", record.id},
{"name", record.name},
{"volumeMax", std::max(0.0f, read_float(*blend.sdna, element, "volume_max").value_or(1.0f))},
{"volumeMin", std::max(0.0f, read_float(*blend.sdna, element, "volume_min").value_or(0.0f))},
{"distanceMax", std::max(0.0f, read_float(*blend.sdna, element, "distance_max").value_or(0.0f))},
{"distanceReference", std::max(0.0f, read_float(*blend.sdna, element, "distance_reference").value_or(1.0f))},
{"attenuation", std::max(0.0f, read_float(*blend.sdna, element, "attenuation").value_or(1.0f))},
{"coneAngleOuter", std::clamp(read_float(*blend.sdna, element, "cone_angle_outer").value_or(360.0f), 0.0f, 360.0f)},
{"coneAngleInner", std::clamp(read_float(*blend.sdna, element, "cone_angle_inner").value_or(360.0f), 0.0f, 360.0f)},
{"coneVolumeOuter", std::clamp(read_float(*blend.sdna, element, "cone_volume_outer").value_or(1.0f), 0.0f, 1.0f)},
{"volume", std::max(0.0f, read_float(*blend.sdna, element, "volume").value_or(1.0f))},
{"pitch", std::max(0.0f, read_float(*blend.sdna, element, "pitch").value_or(1.0f))},
};
if (const std::optional<uint64_t> sound_pointer = read_pointer(*blend.sdna, element, "sound")) {
const auto sound_id = ids_by_pointer.find(*sound_pointer);
if (sound_id != ids_by_pointer.end()) speaker["soundId"] = sound_id->second;
}
speakers.push_back(std::move(speaker));
}
else if (record.type_name == "Image") {
std::string filepath = read_string(*blend.sdna, element, "filepath");
const std::vector<ElementRef> image_views = linked_list_elements(blend, element, "views");
@@ -3624,6 +3807,18 @@ json scene_ir_from_blend(const ParsedBlend &blend,
non_mesh_data.push_back(non_mesh_data_from_record(blend, element, record, ids_by_pointer));
}
else if (record.type_name == "Text") {
if (const std::optional<json> text = script_source_from_record(blend, element, record)) {
json resource = {{"id", record.id}, {"name", record.name}};
resource["source"] = text->value("source", std::string());
resource["sourceSha256"] = text->value("sourceSha256", std::string());
resource["byteLength"] = text->value("byteLength", size_t(0));
resource["lineCount"] = text->value("lineCount", size_t(0));
resource["internal"] = text->value("internal", true);
resource["isDirty"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 0)) != 0;
resource["useModule"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 4)) != 0;
if (text->contains("sourcePath")) resource["sourcePath"] = text->at("sourcePath");
texts.push_back(std::move(resource));
}
if (script_sources.size() >= 1024) {
script_sources_blocked = true;
}
@@ -3643,6 +3838,21 @@ json scene_ir_from_blend(const ParsedBlend &blend,
script_sources_blocked = true;
}
}
else if (record.type_name == "Tex") {
textures.push_back({
{"id", record.id},
{"name", record.name},
{"type", read_integer(*blend.sdna, element, "type").value_or(0)},
{"noiseScale", std::max(0.0f, read_float(*blend.sdna, element, "noisesize").value_or(0.25f))},
{"noiseDepth", std::clamp<int64_t>(read_integer(*blend.sdna, element, "noisedepth").value_or(2), 0, 30)},
{"intensity", std::max(0.0f, read_float(*blend.sdna, element, "bright").value_or(1.0f))},
{"contrast", std::max(0.0f, read_float(*blend.sdna, element, "contrast").value_or(1.0f))},
{"saturation", std::max(0.0f, read_float(*blend.sdna, element, "saturation").value_or(1.0f))},
});
}
else if (record.type_name == "WorkSpace") {
workspace_resources.push_back({{"id", record.id}, {"name", record.name}, {"screenCount", linked_list_elements(blend, element, "layouts").size()}});
}
else if (record.type_name == "VFont") {
const std::string filepath = read_string(*blend.sdna, element, "filepath");
const std::optional<uint64_t> packed_file = read_pointer(*blend.sdna, element, "packedfile");
@@ -3947,6 +4157,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
sort_by_id(lights);
sort_by_id(worlds);
sort_by_id(images);
sort_by_id(brushes);
sort_by_id(line_styles);
sort_by_id(lattices);
sort_by_id(particle_settings);
sort_by_id(sounds);
sort_by_id(speakers);
sort_by_id(texts);
sort_by_id(textures);
sort_by_id(workspace_resources);
sort_by_id(non_mesh_data);
sort_by_id(vfonts);
sort_by_id(libraries);
@@ -3985,6 +4204,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"lights", std::move(lights)},
{"worlds", std::move(worlds)},
{"images", std::move(images)},
{"brushes", std::move(brushes)},
{"lineStyles", std::move(line_styles)},
{"lattices", std::move(lattices)},
{"particleSettings", std::move(particle_settings)},
{"sounds", std::move(sounds)},
{"speakers", std::move(speakers)},
{"texts", std::move(texts)},
{"textures", std::move(textures)},
{"workspaces", std::move(workspace_resources)},
{"nonMeshData", std::move(non_mesh_data)},
{"vfonts", std::move(vfonts)},
{"libraryStatus", libraries_blocked ? "BLOCKED" : "AVAILABLE"},
@@ -3998,6 +4226,7 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"scenes", std::move(scenes)},
{"activeObjectId", active_object_id.empty() ? json(nullptr) : json(active_object_id)},
{"frame", {{"current", frame_current}, {"start", frame_start}, {"end", frame_end}}}};
snapshot["windowManager"] = {{"id", "window-manager:WinMan"}, {"name", "WinMan"}, {"presetName", "New Preset"}, {"windowCount", 1}, {"interfaceLocked", false}};
if (!masks_blocked) {
snapshot["trackingMasks"] = {{"schemaVersion", 1},
{"revision", revision},

View File

@@ -4396,6 +4396,41 @@ bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
return true;
}
bool web_engine_blend_main_particle_settings_json(WebBlendMainState *state,
std::string &particle_settings_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr) {
error = "PARTICLE_SETTINGS_MANIFEST_INVALID: authoritative Main is not open";
return false;
}
constexpr size_t max_settings = 4096;
auto canonical_number = [](const float value) -> json {
return std::isfinite(value) && std::trunc(value) == value ? json(int64_t(value)) : json(value);
};
json settings = json::array();
for (const ParticleSettings &part : state->main->particles) {
if (settings.size() >= max_settings) {
error = "PARTICLE_SETTINGS_BUDGET_EXCEEDED: ParticleSettings count exceeds the 4096 data-block budget";
return false;
}
settings.push_back({{"id", "particle-settings:" + id_name(part.id)},
{"name", id_name(part.id)},
{"type", int(part.type)},
{"from", int(part.from)},
{"distribution", int(part.distr)},
{"physicsType", int(part.phystype)},
{"totalParticles", part.totpart},
{"start", canonical_number(part.sta)},
{"end", canonical_number(part.end)},
{"lifetime", canonical_number(part.lifetime)},
{"size", canonical_number(part.size)},
{"drawSize", canonical_number(part.draw_size)}});
}
particle_settings_json = json({{"schemaVersion", 1}, {"settings", std::move(settings)}}).dump();
return true;
}
bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state,
const char *data_id,
const char *name,

View File

@@ -383,6 +383,9 @@ bool web_engine_blend_main_geometry_node_graphs_json(WebBlendMainState *state,
bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
std::string &physics_json,
std::string &error);
bool web_engine_blend_main_particle_settings_json(WebBlendMainState *state,
std::string &particle_settings_json,
std::string &error);
bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state,
const char *data_id,
const char *name,