Advance M8-M11 parity workflows
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-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -829,6 +829,13 @@ bool refresh_scene_from_main(EngineState &engine,
return false;
}
snapshot["greasePencils"] = json::parse(grease_pencils_json);
std::string geometry_node_graphs_json;
if (!web_engine_blend_main_geometry_node_graphs_json(
engine.authoritative_main, geometry_node_graphs_json, error))
{
return false;
}
snapshot["geometryNodeGraphs"] = json::parse(geometry_node_graphs_json);
std::string physics_json;
if (!web_engine_blend_main_physics_simulation_json(
engine.authoritative_main, physics_json, error))
@@ -1132,6 +1139,15 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
return last_error_code;
}
opened_snapshot["greasePencils"] = json::parse(grease_pencils_json);
std::string geometry_node_graphs_json;
if (!web_engine_blend_main_geometry_node_graphs_json(
authoritative_main, geometry_node_graphs_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;
}
opened_snapshot["geometryNodeGraphs"] = json::parse(geometry_node_graphs_json);
std::string physics_json;
if (!web_engine_blend_main_physics_simulation_json(
authoritative_main, physics_json, main_error))
@@ -1543,9 +1559,10 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
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 == "setFontLinks" || type == "setVolumeProperties" || type == "deleteNonMeshData" ||
type == "setFontProperties" || type == "setFontAdvanced" || type == "importVFont" || type == "setFontLinks" || type == "setVolumeProperties" || type == "deleteNonMeshData" ||
type == "setMetaballElements" || type == "createGreasePencilLayer" ||
type == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" ||
type == "moveGreasePencilFrame" ||
type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" ||
type == "setGreasePencilStrokes" || type == "setVertexColors" ||
type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" ||
@@ -1919,6 +1936,13 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(), characters,
text_boxes, command.value("activeTextBox", -1), main_error);
}
else if (type == "importVFont") {
std::string font_id;
applied = web_engine_blend_main_import_vfont(
engine->authoritative_main, command.value("name", "").c_str(),
command.value("sourcePath", "").c_str(), command.value("base64", "").c_str(),
font_id, main_error);
}
else if (type == "setFontLinks") {
const json links = command.value("links", json::object());
const std::array<const char *, 4> fields = {"regular", "bold", "italic", "boldItalic"};
@@ -1984,6 +2008,12 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(),
command.value("layerId", "").c_str(), command.value("direction", "UP").c_str(), main_error);
}
else if (type == "moveGreasePencilFrame") {
applied = web_engine_blend_main_move_grease_pencil_frame(
engine->authoritative_main, command.value("dataId", "").c_str(),
command.value("layerId", "").c_str(), command.value("frame", -1),
command.value("targetFrame", -1), command.value("drawingId", "").c_str(), main_error);
}
else if (type == "insertGreasePencilFrame") {
applied = web_engine_blend_main_insert_grease_pencil_frame(
engine->authoritative_main, command.value("dataId", "").c_str(),
@@ -2066,7 +2096,8 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("vertexGroup", "").c_str(),
command.value("indices", std::vector<uint32_t>()),
command.value("values", std::vector<float>()),
command.value("normalize", false), command.value("mirror", false), main_error);
command.value("normalize", false), command.value("limit", 0u), command.value("mirror", false),
command.value("mirrorAxis", 0), command.value("mirrorTolerance", 1e-4f), main_error);
}
else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties")
{

View File

@@ -3513,7 +3513,16 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"emissionStrength", std::clamp(emission_strength, 0.0f, 1000000.0f)} };
if (!normal_image_id.empty()) material["normalImageId"] = normal_image_id;
if (!image_ids.empty()) material["imageIds"] = image_ids;
if (!material_nodes.empty()) material["nodes"] = std::move(material_nodes);
if (!material_nodes.empty()) {
/* The browser compiler binds its report to this exact reader graph. */
material["shaderGraphHash"] = sha256_hex(json({
{"schemaVersion", 1},
{"materialId", record.id},
{"nodes", material_nodes},
{"links", material_links},
}).dump());
material["nodes"] = std::move(material_nodes);
}
if (!material_links.empty()) material["links"] = std::move(material_links);
if (!warnings.empty()) material["warnings"] = warnings;
materials.push_back(std::move(material));
@@ -3636,9 +3645,20 @@ json scene_ir_from_blend(const ParsedBlend &blend,
}
else if (record.type_name == "VFont") {
const std::string filepath = read_string(*blend.sdna, element, "filepath");
vfonts.push_back({{"id", record.id}, {"name", record.name}, {"sourcePath", filepath},
{"builtin", filepath == "<builtin>"},
{"packed", read_pointer(*blend.sdna, element, "packedfile").value_or(0) != 0}});
const std::optional<uint64_t> packed_file = read_pointer(*blend.sdna, element, "packedfile");
const std::vector<uint8_t> packed_bytes = packed_file ? packed_file_bytes(blend, *packed_file) :
std::vector<uint8_t>();
json resource = {{"id", record.id},
{"name", record.name},
{"sourcePath", filepath},
{"builtin", filepath == "<builtin>"},
{"packed", !packed_bytes.empty()}};
if (!packed_bytes.empty()) {
resource["packedByteLength"] = packed_bytes.size();
resource["sha256"] = sha256_hex(std::string(
reinterpret_cast<const char *>(packed_bytes.data()), packed_bytes.size()));
}
vfonts.push_back(std::move(resource));
}
else if (record.type_name == "Camera") {
const int64_t camera_type = read_integer(*blend.sdna, element, "type").value_or(0);

View File

@@ -80,6 +80,7 @@ using json = nlohmann::json;
using namespace blender;
std::once_flag blender_runtime_once;
constexpr int64_t geometry_node_max_json_scalar_values = 65'536;
void initialize_blender_runtime()
{
@@ -341,6 +342,11 @@ json evaluated_modifier_report(const Object &object, const Object *object_eval)
entry["suggestion"] =
"Bake a deterministic cache in desktop Blender or disable the simulation zone";
}
else if (error.starts_with("WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED:")) {
entry["errorCode"] = "GEOMETRY_NODES_EVALUATOR_UNSUPPORTED";
entry["suggestion"] =
"Use only the versioned bounded Geometry Nodes evaluator closure";
}
else if (error.starts_with("WEB_DISPLACE_CONFIGURATION_UNAVAILABLE:")) {
entry["errorCode"] = "MODIFIER_CONFIGURATION_UNSUPPORTED";
entry["suggestion"] =
@@ -399,7 +405,15 @@ json evaluated_mesh_report(const Depsgraph *graph, Object *object)
{"modifiers", evaluated_modifier_report(*object, object_eval)},
{"worldMatrix", json::array()},
{"positions", json::array()},
{"indices", json::array()}};
{"indices", json::array()},
{"domainCardinality",
{{"POINT", mesh->verts_num},
{"EDGE", mesh->edges_num},
{"FACE", mesh->faces_num},
{"CORNER", mesh->corners_num},
{"CURVE", 0},
{"INSTANCE", 0},
{"LAYER", 0}}}};
for (int row = 0; row < 4; row++) {
for (int column = 0; column < 4; column++) {
/* The Web scene IR uses the same column-major flattening as Blender's Python API and glTF. */
@@ -411,6 +425,32 @@ json evaluated_mesh_report(const Depsgraph *graph, Object *object)
report["positions"].push_back(position.y);
report["positions"].push_back(position.z);
}
const bke::AttributeReader<float> m10_value = mesh->attributes().lookup<float>(
"m10_value", bke::AttrDomain::Point);
if (m10_value) {
json materialization = {{"schemaVersion", 1},
{"fieldId", "attribute:m10_value"},
{"domain", "POINT"},
{"dataType", "FLOAT"},
{"elementCount", positions.size()},
{"scalarValueCount", positions.size()},
{"materializedByteLength", positions.size() * sizeof(float)}};
if (positions.size() <= geometry_node_max_json_scalar_values) {
materialization["transport"] = "JSON";
report["attributes"] = {{"m10_value",
{{"domain", "POINT"},
{"dataType", "FLOAT"},
{"values", json::array()}}}};
for (const int index : positions.index_range()) {
report["attributes"]["m10_value"]["values"].push_back(m10_value.varray[index]);
}
}
else {
materialization["transport"] = "BINARY_REQUIRED";
materialization["errorCode"] = "GN_FIELD_JSON_BUDGET_EXCEEDED";
}
report["fieldMaterializations"] = json::array({std::move(materialization)});
}
for (const int3 &triangle : corner_tris) {
for (int index = 0; index < 3; index++) {
report["indices"].push_back(corner_verts[triangle[index]]);

View File

@@ -8,6 +8,8 @@
#include <cstring>
#include <limits>
#include <new>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
@@ -36,7 +38,9 @@
#include "BKE_nla.hh"
#include "BKE_object.hh"
#include "BKE_object_deform.h"
#include "BKE_packedFile.hh"
#include "BKE_scene.hh"
#include "BKE_vfont.hh"
#include "BKE_volume.hh"
#include "BKE_fcurve.hh"
#include "BKE_image.hh"
@@ -615,6 +619,217 @@ bool append_write_bytes(const void *data, const size_t size, void *user_data)
}
}
const char *geometry_socket_data_type(const eNodeSocketDatatype type)
{
switch (type) {
case SOCK_FLOAT: return "FLOAT";
case SOCK_VECTOR: return "VECTOR";
case SOCK_RGBA: return "COLOR";
case SOCK_SHADER: return "SHADER";
case SOCK_BOOLEAN: return "BOOLEAN";
case SOCK_INT: return "INT";
case SOCK_STRING: return "STRING";
case SOCK_OBJECT: return "OBJECT";
case SOCK_IMAGE: return "IMAGE";
case SOCK_GEOMETRY: return "GEOMETRY";
case SOCK_COLLECTION: return "COLLECTION";
case SOCK_TEXTURE: return "TEXTURE";
case SOCK_MATERIAL: return "MATERIAL";
case SOCK_ROTATION: return "ROTATION";
case SOCK_MENU: return "MENU";
case SOCK_MATRIX: return "MATRIX";
case SOCK_BUNDLE: return "BUNDLE";
case SOCK_CLOSURE: return "CLOSURE";
case SOCK_FONT: return "FONT";
case SOCK_SCENE: return "SCENE";
case SOCK_TEXT_ID: return "TEXT";
case SOCK_MASK: return "MASK";
case SOCK_SOUND: return "SOUND";
case SOCK_INT_VECTOR: return "INT_VECTOR";
case SOCK_CUSTOM: return "CUSTOM";
}
return "CUSTOM";
}
eNodeSocketDatatype geometry_interface_socket_type(const char *socket_type)
{
if (socket_type == nullptr) return SOCK_CUSTOM;
const std::string type(socket_type);
if (type.starts_with("NodeSocketIntVector")) return SOCK_INT_VECTOR;
if (type.starts_with("NodeSocketFloat")) return SOCK_FLOAT;
if (type.starts_with("NodeSocketVector")) return SOCK_VECTOR;
if (type == "NodeSocketColor") return SOCK_RGBA;
if (type == "NodeSocketShader") return SOCK_SHADER;
if (type == "NodeSocketBool") return SOCK_BOOLEAN;
if (type.starts_with("NodeSocketInt")) return SOCK_INT;
if (type.starts_with("NodeSocketString")) return SOCK_STRING;
if (type == "NodeSocketObject") return SOCK_OBJECT;
if (type == "NodeSocketImage") return SOCK_IMAGE;
if (type == "NodeSocketGeometry") return SOCK_GEOMETRY;
if (type == "NodeSocketCollection") return SOCK_COLLECTION;
if (type == "NodeSocketTexture") return SOCK_TEXTURE;
if (type == "NodeSocketMaterial") return SOCK_MATERIAL;
if (type == "NodeSocketRotation") return SOCK_ROTATION;
if (type == "NodeSocketMenu") return SOCK_MENU;
if (type == "NodeSocketMatrix") return SOCK_MATRIX;
if (type == "NodeSocketBundle") return SOCK_BUNDLE;
if (type == "NodeSocketClosure") return SOCK_CLOSURE;
if (type == "NodeSocketFont") return SOCK_FONT;
if (type == "NodeSocketScene") return SOCK_SCENE;
if (type == "NodeSocketText") return SOCK_TEXT_ID;
if (type == "NodeSocketMask") return SOCK_MASK;
if (type == "NodeSocketSound") return SOCK_SOUND;
return SOCK_CUSTOM;
}
std::string geometry_id_reference(const ID *id)
{
if (id == nullptr) return {};
const std::string code(id->name, 2);
const char *prefix = code == "OB" ? "object:" : code == "IM" ? "image:" :
code == "GR" ? "collection:" : code == "TE" ? "texture:" :
code == "MA" ? "material:" : code == "VF" ? "vfont:" :
code == "SC" ? "scene:" : code == "TX" ? "text:" :
code == "MS" ? "mask:" : code == "SO" ? "sound:" :
code == "NT" ? "node-group:" : "id:";
return std::string(prefix) + id_name(*id);
}
std::optional<json> geometry_socket_default(const eNodeSocketDatatype type,
const void *default_value)
{
if (default_value == nullptr) return std::nullopt;
switch (type) {
case SOCK_FLOAT: {
const float value = static_cast<const bNodeSocketValueFloat *>(default_value)->value;
return std::isfinite(value) ? std::optional<json>(value) : std::nullopt;
}
case SOCK_INT:
return static_cast<const bNodeSocketValueInt *>(default_value)->value;
case SOCK_BOOLEAN:
return static_cast<const bNodeSocketValueBoolean *>(default_value)->value != 0;
case SOCK_VECTOR: {
const bNodeSocketValueVector &value = *static_cast<const bNodeSocketValueVector *>(default_value);
const int dimensions = std::clamp(value.dimensions, 2, 4);
json result = json::array();
for (int index = 0; index < dimensions; index++) {
if (!std::isfinite(value.value[index])) return std::nullopt;
result.push_back(value.value[index]);
}
return result;
}
case SOCK_INT_VECTOR: {
const bNodeSocketValueIntVector &value = *static_cast<const bNodeSocketValueIntVector *>(default_value);
const int dimensions = std::clamp(value.dimensions, 2, 3);
json result = json::array();
for (int index = 0; index < dimensions; index++) result.push_back(value.value[index]);
return result;
}
case SOCK_ROTATION: {
const bNodeSocketValueRotation &value = *static_cast<const bNodeSocketValueRotation *>(default_value);
if (!std::all_of(std::begin(value.value_euler), std::end(value.value_euler),
[](const float component) { return std::isfinite(component); }))
{
return std::nullopt;
}
return json::array({value.value_euler[0], value.value_euler[1], value.value_euler[2]});
}
case SOCK_RGBA: {
const bNodeSocketValueRGBA &value = *static_cast<const bNodeSocketValueRGBA *>(default_value);
if (!std::all_of(std::begin(value.value), std::end(value.value),
[](const float component) { return std::isfinite(component); }))
{
return std::nullopt;
}
return json::array({value.value[0], value.value[1], value.value[2], value.value[3]});
}
case SOCK_STRING: {
const bNodeSocketValueString &value = *static_cast<const bNodeSocketValueString *>(default_value);
return std::string(value.value, strnlen(value.value, sizeof(value.value)));
}
case SOCK_MENU:
return static_cast<const bNodeSocketValueMenu *>(default_value)->value;
case SOCK_OBJECT:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueObject *>(default_value)->value));
case SOCK_IMAGE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueImage *>(default_value)->value));
case SOCK_COLLECTION:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueCollection *>(default_value)->value));
case SOCK_TEXTURE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueTexture *>(default_value)->value));
case SOCK_MATERIAL:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueMaterial *>(default_value)->value));
case SOCK_FONT:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueFont *>(default_value)->value));
case SOCK_SCENE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueScene *>(default_value)->value));
case SOCK_TEXT_ID:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueText *>(default_value)->value));
case SOCK_MASK:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueMask *>(default_value)->value));
case SOCK_SOUND:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueSound *>(default_value)->value));
case SOCK_CUSTOM:
case SOCK_SHADER:
case SOCK_GEOMETRY:
case SOCK_MATRIX:
case SOCK_BUNDLE:
case SOCK_CLOSURE:
return std::nullopt;
}
return std::nullopt;
}
std::string geometry_node_id(const bNode &node)
{
return "geometry-node:" + std::to_string(node.identifier);
}
std::string geometry_node_type(const bNode &node)
{
std::string stored(node.idname);
constexpr std::string_view undefined_prefix = "Undefined[";
while (stored.starts_with(undefined_prefix) && stored.ends_with("]") &&
stored.size() > undefined_prefix.size() + 1)
{
stored = stored.substr(undefined_prefix.size(), stored.size() - undefined_prefix.size() - 1);
}
return stored;
}
std::string geometry_socket_id(const bNodeSocket &socket, const eNodeSocketInOut direction)
{
return std::string("geometry-socket:") + (direction == SOCK_IN ? "input:" : "output:") +
socket.identifier;
}
json geometry_socket_json(const bNodeSocket &socket, const eNodeSocketInOut direction)
{
json result = {{"id", geometry_socket_id(socket, direction)},
{"name", socket.name},
{"direction", direction == SOCK_IN ? "INPUT" : "OUTPUT"},
{"dataType", geometry_socket_data_type(socket.type)}};
if (const std::optional<json> value = geometry_socket_default(socket.type, socket.default_value)) {
if (!(value->is_string() && value->get_ref<const std::string &>().empty() &&
ELEM(socket.type, SOCK_OBJECT, SOCK_IMAGE, SOCK_COLLECTION, SOCK_TEXTURE, SOCK_MATERIAL,
SOCK_FONT, SOCK_SCENE, SOCK_TEXT_ID, SOCK_MASK, SOCK_SOUND)))
{
result["defaultValue"] = *value;
}
}
return result;
}
} // namespace
WebBlendMainState *web_engine_blend_main_open(const uint8_t *data,
@@ -3451,6 +3666,58 @@ bool web_engine_blend_main_set_font_advanced(
return true;
}
bool web_engine_blend_main_import_vfont(WebBlendMainState *state,
const char *name,
const char *source_path,
const char *base64,
std::string &font_id,
std::string &error)
{
if (state == nullptr || state->main == nullptr || name == nullptr || source_path == nullptr ||
base64 == nullptr || name[0] == '\0' || strlen(name) > 63 || strlen(source_path) >= FILE_MAX ||
strncmp(source_path, "//fonts/", 8) != 0 || strstr(source_path, "\\") != nullptr ||
strstr(source_path, "/../") != nullptr || strstr(source_path, "/./") != nullptr)
{
error = "NON_MESH_RESOURCE_OUTSIDE_PROJECT: VFont source must be a bounded //fonts project path";
return false;
}
for (const VFont &existing : state->main->fonts) {
if (STREQ(existing.filepath, source_path)) {
error = "NON_MESH_PROPERTY_INVALID: A VFont with the same project source path already exists";
return false;
}
}
const std::vector<uint8_t> decoded = decode_base64(base64);
if (decoded.empty() || decoded.size() > 32 * 1024 * 1024) {
error = "NON_MESH_DATA_BUDGET_EXCEEDED: VFont payload is empty or exceeds 32 MiB";
return false;
}
uint8_t *owned = MEM_new_array_uninitialized<uint8_t>(decoded.size(), "WebEngine packed VFont");
if (owned == nullptr) {
error = "NON_MESH_DATA_BUDGET_EXCEEDED: VFont packed allocation failed";
return false;
}
memcpy(owned, decoded.data(), decoded.size());
PackedFile *packed = BKE_packedfile_new_from_memory(owned, int(decoded.size()));
VFont *vfont = static_cast<VFont *>(BKE_libblock_alloc(state->main, ID_VF, name, 0));
if (packed == nullptr || vfont == nullptr) {
if (packed != nullptr) BKE_packedfile_free(packed);
else MEM_delete(owned);
error = "NON_MESH_BINARY_INVALID: Blender could not allocate the packed VFont";
return false;
}
STRNCPY(vfont->filepath, source_path);
vfont->packedfile = packed;
BKE_vfont_data_ensure(vfont);
if (vfont->data == nullptr) {
BKE_id_delete(state->main, vfont);
error = "NON_MESH_BINARY_INVALID: Blender rejected the external VFont bytes";
return false;
}
font_id = "vfont:" + id_name(vfont->id);
return true;
}
bool web_engine_blend_main_set_font_links(WebBlendMainState *state,
const char *data_id,
const std::array<std::string, 4> &font_ids,
@@ -3630,6 +3897,19 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
{{"name", "vertex_color"}, {"domain", "POINT"}, {"dataType", "FLOAT_COLOR"}, {"components", 4}},
{{"name", "cyclic"}, {"domain", "STROKE"}, {"dataType", "BOOL"}, {"components", 1}},
{{"name", "material_index"}, {"domain", "STROKE"}, {"dataType", "INT"}, {"components", 1}}})}};
const bke::greasepencil::Layer *active_layer = grease_pencil.get_active_layer();
if (active_layer == nullptr) {
for (const bke::greasepencil::Layer *layer : layers) {
if (layer != nullptr) {
active_layer = layer;
break;
}
}
}
if (active_layer != nullptr) {
data["activeLayerId"] = "grease-pencil-layer:" + id_name(grease_pencil.id) + ":" +
std::string(active_layer->name());
}
if (budget_blocked) {
data["errorCode"] = "GREASE_PENCIL_BUDGET_EXCEEDED";
grease_pencils.push_back(std::move(data));
@@ -3644,6 +3924,9 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
if (frame.is_end()) continue;
json strokes = json::array();
uint64_t drawing_point_count = 0;
const std::string drawing_suffix = id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index);
const std::string drawing_id = "grease-pencil-drawing:" + drawing_suffix;
const bke::greasepencil::Drawing *drawing = grease_pencil.get_drawing_at(*layer,
frame_number);
if (drawing != nullptr) {
@@ -3665,13 +3948,15 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
for (const int point_index : point_range) {
const float3 &position = positions[point_index];
const ColorGeometry4f color = vertex_colors[point_index];
points.push_back({{"position", json::array({position.x, position.y, position.z})},
points.push_back({{"id", "grease-pencil-point:" + drawing_suffix + ":" +
std::to_string(curve_index) + ":" +
std::to_string(point_index - point_range.start())},
{"position", json::array({position.x, position.y, position.z})},
{"radius", radii[point_index]},
{"opacity", opacities[point_index]},
{"vertexColor", json::array({color.r, color.g, color.b, color.a})}});
}
strokes.push_back({{"id", "grease-pencil-stroke:" + id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index) + ":" +
strokes.push_back({{"id", "grease-pencil-stroke:" + drawing_suffix + ":" +
std::to_string(curve_index)},
{"cyclic", cyclic[curve_index]},
{"pointCount", point_range.size()},
@@ -3682,8 +3967,7 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
json frame_entry = {
{"frame", frame_number},
{"drawing",
{{"id", "grease-pencil-drawing:" + id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index)},
{{"id", drawing_id},
{"strokeCount", strokes.size()},
{"pointCount", drawing_point_count},
{"strokes", std::move(strokes)}}}};
@@ -3707,6 +3991,183 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
return true;
}
bool web_engine_blend_main_geometry_node_graphs_json(WebBlendMainState *state,
std::string &geometry_node_graphs_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr) {
error = "GN_INVALID_GRAPH: authoritative Main is not open";
return false;
}
constexpr size_t max_graphs = 4096;
constexpr size_t max_nodes_per_graph = 4096;
constexpr size_t max_links_per_graph = 16384;
constexpr size_t max_sockets_per_graph = 65536;
constexpr size_t max_interface_sockets_per_graph = 4096;
json graphs = json::array();
try {
for (const bNodeTree &tree : state->main->nodetrees) {
if (strcmp(tree.idname, "GeometryNodeTree") != 0) continue;
if (graphs.size() >= max_graphs) {
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node graph count exceeds 4096";
return false;
}
const size_t node_count = size_t(BLI_listbase_count(&tree.nodes));
const size_t link_count = size_t(BLI_listbase_count(&tree.links));
if (node_count > max_nodes_per_graph || link_count > max_links_per_graph ||
tree.interface_inputs().size() > max_interface_sockets_per_graph ||
tree.interface_outputs().size() > max_interface_sockets_per_graph)
{
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node graph topology exceeds its bounded reader limits";
return false;
}
const std::string graph_id = "node-group:" + id_name(tree.id);
json interface_inputs = json::array();
json interface_outputs = json::array();
auto append_interface_socket = [&](const bNodeTreeInterfaceSocket &socket,
const char *direction,
json &destination) -> bool {
if (socket.identifier == nullptr || socket.identifier[0] == '\0' ||
socket.name == nullptr || socket.socket_type == nullptr)
{
error = "GN_INVALID_GRAPH: Geometry Node interface socket has no stable identifier, name, or type";
return false;
}
const eNodeSocketDatatype type = geometry_interface_socket_type(socket.socket_type);
json socket_json = {
{"id", std::string("geometry-interface:") +
(strcmp(direction, "INPUT") == 0 ? "input:" : "output:") +
socket.identifier},
{"name", socket.name},
{"direction", direction},
{"dataType", geometry_socket_data_type(type)}};
if (const std::optional<json> value = geometry_socket_default(type, socket.socket_data)) {
if (!(value->is_string() && value->get_ref<const std::string &>().empty())) {
socket_json["defaultValue"] = *value;
}
}
destination.push_back(std::move(socket_json));
return true;
};
for (const bNodeTreeInterfaceSocket *socket : tree.interface_inputs()) {
if (socket != nullptr && !append_interface_socket(*socket, "INPUT", interface_inputs)) {
return false;
}
}
for (const bNodeTreeInterfaceSocket *socket : tree.interface_outputs()) {
if (socket != nullptr && !append_interface_socket(*socket, "OUTPUT", interface_outputs)) {
return false;
}
}
std::vector<const bNode *> source_nodes;
source_nodes.reserve(node_count);
for (const bNode &node : tree.nodes) source_nodes.push_back(&node);
std::sort(source_nodes.begin(), source_nodes.end(), [](const bNode *a, const bNode *b) {
return a->identifier < b->identifier;
});
json nodes = json::array();
std::unordered_set<int32_t> node_identifiers;
std::unordered_set<std::string> group_references;
size_t socket_count = 0;
for (const bNode *node : source_nodes) {
if (node == nullptr || node->identifier <= 0 ||
!node_identifiers.insert(node->identifier).second || node->idname[0] == '\0')
{
error = "GN_INVALID_GRAPH: Geometry Node has a missing or duplicate stable identifier";
return false;
}
json sockets = json::array();
std::unordered_set<std::string> socket_ids;
auto append_node_sockets = [&](const ListBaseT<bNodeSocket> &source,
const eNodeSocketInOut direction) -> bool {
for (const bNodeSocket &socket : source) {
if (socket.identifier[0] == '\0' || strcmp(socket.identifier, "__extend__") == 0) {
continue;
}
const std::string socket_id = geometry_socket_id(socket, direction);
if (!socket_ids.insert(socket_id).second) {
error = "GN_INVALID_GRAPH: Geometry Node contains duplicate stable socket identifiers";
return false;
}
socket_count++;
if (socket_count > max_sockets_per_graph) {
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node socket count exceeds 65536";
return false;
}
sockets.push_back(geometry_socket_json(socket, direction));
}
return true;
};
if (!append_node_sockets(node->inputs, SOCK_IN) ||
!append_node_sockets(node->outputs, SOCK_OUT))
{
return false;
}
const std::string node_type = geometry_node_type(*node);
json node_json = {{"id", geometry_node_id(*node)},
{"type", node_type},
{"name", node->name[0] != '\0' ? node->name : node_type},
{"sockets", std::move(sockets)}};
if (node->id != nullptr && std::string(node->id->name, 2) == "NT") {
const std::string reference = geometry_id_reference(node->id);
node_json["groupTreeId"] = reference;
group_references.insert(reference);
}
nodes.push_back(std::move(node_json));
}
std::vector<json> source_links;
source_links.reserve(link_count);
for (const bNodeLink &link : tree.links) {
if (link.fromnode == nullptr || link.tonode == nullptr || link.fromsock == nullptr ||
link.tosock == nullptr || strcmp(link.fromsock->identifier, "__extend__") == 0 ||
strcmp(link.tosock->identifier, "__extend__") == 0)
{
continue;
}
if (!node_identifiers.contains(link.fromnode->identifier) ||
!node_identifiers.contains(link.tonode->identifier))
{
error = "GN_INVALID_GRAPH: Geometry Node link references a node outside its graph";
return false;
}
source_links.push_back({{"fromNodeId", geometry_node_id(*link.fromnode)},
{"fromSocketId", geometry_socket_id(*link.fromsock, SOCK_OUT)},
{"toNodeId", geometry_node_id(*link.tonode)},
{"toSocketId", geometry_socket_id(*link.tosock, SOCK_IN)}});
}
std::sort(source_links.begin(), source_links.end(), [](const json &a, const json &b) {
return std::tie(a.at("fromNodeId"), a.at("fromSocketId"), a.at("toNodeId"),
a.at("toSocketId")) <
std::tie(b.at("fromNodeId"), b.at("fromSocketId"), b.at("toNodeId"),
b.at("toSocketId"));
});
json links = json::array();
for (json &link : source_links) links.push_back(std::move(link));
std::vector<std::string> references(group_references.begin(), group_references.end());
std::sort(references.begin(), references.end());
json graph = {{"schemaVersion", 1},
{"id", graph_id},
{"name", id_name(tree.id)},
{"interfaceInputs", std::move(interface_inputs)},
{"interfaceOutputs", std::move(interface_outputs)},
{"nodes", std::move(nodes)},
{"links", std::move(links)},
{"groupReferences", std::move(references)}};
graph["graphHash"] = web_engine_sha256_hex(graph.dump());
graphs.push_back(std::move(graph));
}
geometry_node_graphs_json = graphs.dump();
return true;
}
catch (const std::exception &exception) {
error = std::string("GN_INVALID_GRAPH: Geometry Node Main reader failed: ") + exception.what();
return false;
}
}
bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
std::string &physics_json,
std::string &error)
@@ -3884,6 +4345,50 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
return true;
}
bool web_engine_blend_main_move_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
const int frame,
const int target_frame,
const char *drawing_id,
std::string &error)
{
GreasePencil *grease_pencil = find_grease_pencil(state != nullptr ? state->main : nullptr,
data_id);
bke::greasepencil::Layer *layer = find_grease_pencil_layer(grease_pencil, layer_id);
if (grease_pencil == nullptr || layer == nullptr || drawing_id == nullptr ||
!ensure_single_user_data(state != nullptr ? state->main : nullptr,
grease_pencil != nullptr ? &grease_pencil->id : nullptr,
error))
{
if (error.empty()) error = "GREASE_PENCIL_SCHEMA_INVALID: Grease Pencil frame target was not found";
return false;
}
if (frame < -1000000 || frame > 1000000 || target_frame < -1000000 ||
target_frame > 1000000 || frame == target_frame || layer->frames().contains(target_frame))
{
error = "GREASE_PENCIL_SCHEMA_INVALID: frame move is outside the bounded range or target frame already exists";
return false;
}
const GreasePencilFrame *source = layer->frames().lookup_ptr(frame);
if (source == nullptr || source->is_end()) {
error = "GREASE_PENCIL_SCHEMA_INVALID: source Grease Pencil frame was not found";
return false;
}
const std::string expected_drawing_id = "grease-pencil-drawing:" +
id_name(grease_pencil->id) + ":" +
std::to_string(source->drawing_index);
if (expected_drawing_id != drawing_id) {
error = "GREASE_PENCIL_SCHEMA_INVALID: source drawing identity does not match the frame";
return false;
}
blender::Map<int, int> destinations;
destinations.add(frame, target_frame);
grease_pencil->move_frames(*layer, destinations);
grease_pencil->id.recalc |= ID_RECALC_GEOMETRY;
return true;
}
bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
@@ -4039,7 +4544,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices,
const std::vector<float> &values,
const bool normalize,
const uint32_t limit,
const bool mirror,
const int mirror_axis,
const float mirror_tolerance,
std::string &error)
{
Object *object = find_object(state != nullptr ? state->main : nullptr, object_id);
@@ -4050,10 +4558,6 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
if (error.empty()) error = "PAINT_SCHEMA_INVALID: Mesh object was not found";
return false;
}
if (mirror) {
error = "CAPABILITY_MISSING: topology mirror requires a verified mesh symmetry map";
return false;
}
if (strlen(vertex_group) == 0 || strlen(vertex_group) > 63 || indices.empty() ||
indices.size() > 1000000 || indices.size() != values.size() ||
std::any_of(indices.begin(), indices.end(), [&](const uint32_t index) { return index >= uint32_t(mesh->verts_num); }) ||
@@ -4061,6 +4565,88 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
error = "PAINT_SCHEMA_INVALID: vertex weight patch is outside the bounded domain";
return false;
}
if (limit > 32 || (mirror && (mirror_axis < 0 || mirror_axis > 2 || !std::isfinite(mirror_tolerance) ||
mirror_tolerance <= 0.0f || mirror_tolerance > 1.0f))) {
error = "PAINT_SCHEMA_INVALID: vertex weight limit or mirror options are outside the bounded domain";
return false;
}
std::unordered_set<uint32_t> seen_indices;
for (const uint32_t index : indices) {
if (!seen_indices.insert(index).second) {
error = "PAINT_SCHEMA_INVALID: vertex weight patch contains duplicate vertex indices";
return false;
}
}
std::unordered_map<uint32_t, float> updates;
updates.reserve(indices.size() * (mirror ? 2 : 1));
for (size_t index = 0; index < indices.size(); index++) updates.emplace(indices[index], values[index]);
if (mirror) {
/* Build a reciprocal local-coordinate symmetry map before mutating Main. */
const Span<float3> positions = mesh->vert_positions();
if (positions.size() > 100000) {
error = "PAINT_BUDGET_EXCEEDED: verified weight mirror is limited to 100000 vertices";
return false;
}
auto cell_key = [](const float3 &position, const float cell_size, const int dx = 0,
const int dy = 0, const int dz = 0) {
const int64_t x = int64_t(std::llround(position.x / cell_size)) + dx;
const int64_t y = int64_t(std::llround(position.y / cell_size)) + dy;
const int64_t z = int64_t(std::llround(position.z / cell_size)) + dz;
return std::to_string(x) + ":" + std::to_string(y) + ":" + std::to_string(z);
};
std::unordered_map<std::string, std::vector<int32_t>> buckets;
buckets.reserve(positions.size());
for (int64_t index = 0; index < int64_t(positions.size()); index++) {
buckets[cell_key(positions[index], mirror_tolerance)].push_back(int32_t(index));
}
std::vector<int32_t> mirror_vertices(positions.size(), -1);
for (int64_t source = 0; source < int64_t(positions.size()); source++) {
const float3 reflected = [&]() {
float3 value = positions[source];
value[mirror_axis] = -value[mirror_axis];
return value;
}();
float best_distance = std::numeric_limits<float>::max();
int32_t best = -1;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
const auto bucket = buckets.find(cell_key(reflected, mirror_tolerance, dx, dy, dz));
if (bucket == buckets.end()) continue;
for (const int32_t candidate : bucket->second) {
const float3 delta = positions[candidate] - reflected;
const float distance = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
if (distance < best_distance || (distance == best_distance && candidate < best)) {
best_distance = distance;
best = candidate;
}
}
}
}
}
if (best < 0 || best_distance > mirror_tolerance) {
error = "CAPABILITY_MISSING: WEIGHT_MIRROR_SYMMETRY_UNVERIFIED";
return false;
}
mirror_vertices[source] = best;
}
for (size_t source = 0; source < mirror_vertices.size(); source++) {
const int32_t target = mirror_vertices[source];
if (target < 0 || mirror_vertices[size_t(target)] != int32_t(source)) {
error = "CAPABILITY_MISSING: WEIGHT_MIRROR_SYMMETRY_UNVERIFIED";
return false;
}
}
for (const auto &[source, value] : std::vector<std::pair<uint32_t, float>>(updates.begin(), updates.end())) {
const uint32_t target = uint32_t(mirror_vertices[source]);
const auto existing = updates.find(target);
if (existing != updates.end() && std::abs(existing->second - value) > 1e-6f) {
error = "PAINT_SCHEMA_INVALID: mirrored weight patch contains conflicting values";
return false;
}
updates[target] = value;
}
}
int group_index = BKE_object_defgroup_name_index(object, vertex_group);
if (group_index < 0) {
if (BKE_object_defgroup_add_name(object, vertex_group) == nullptr) {
@@ -4070,15 +4656,35 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
group_index = BKE_object_defgroup_name_index(object, vertex_group);
}
MutableSpan<MDeformVert> deform_verts = mesh->deform_verts_for_write();
for (size_t index = 0; index < indices.size(); index++) {
MDeformVert &deform_vert = deform_verts[indices[index]];
if (values[index] == 0.0f) {
std::vector<uint32_t> touched_vertices;
touched_vertices.reserve(updates.size());
for (const auto &[vertex_index, value] : updates) {
MDeformVert &deform_vert = deform_verts[vertex_index];
if (value == 0.0f) {
if (MDeformWeight *weight = BKE_defvert_find_index(&deform_vert, group_index)) {
BKE_defvert_remove_group(&deform_vert, weight);
}
}
else {
BKE_defvert_ensure_index(&deform_vert, group_index)->weight = values[index];
BKE_defvert_ensure_index(&deform_vert, group_index)->weight = value;
}
touched_vertices.push_back(vertex_index);
}
for (const uint32_t vertex_index : touched_vertices) {
MDeformVert &deform_vert = deform_verts[vertex_index];
if (limit > 0) {
while (deform_vert.totweight > int(limit)) {
int remove_index = 0;
for (int index = 1; index < deform_vert.totweight; index++) {
const MDeformWeight &candidate = deform_vert.dw[index];
const MDeformWeight &current = deform_vert.dw[remove_index];
if (candidate.weight < current.weight ||
(candidate.weight == current.weight && candidate.def_nr > current.def_nr)) {
remove_index = index;
}
}
BKE_defvert_remove_group(&deform_vert, &deform_vert.dw[remove_index]);
}
}
if (normalize) BKE_defvert_normalize(deform_vert);
}

View File

@@ -346,6 +346,12 @@ bool web_engine_blend_main_set_font_advanced(WebBlendMainState *state,
const std::vector<WebFontTextBoxEdit> &text_boxes,
int active_text_box,
std::string &error);
bool web_engine_blend_main_import_vfont(WebBlendMainState *state,
const char *name,
const char *source_path,
const char *base64,
std::string &font_id,
std::string &error);
bool web_engine_blend_main_set_font_links(WebBlendMainState *state,
const char *data_id,
const std::array<std::string, 4> &font_ids,
@@ -361,6 +367,9 @@ bool web_engine_blend_main_set_metaball_elements(WebBlendMainState *state,
bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
std::string &grease_pencils_json,
std::string &error);
bool web_engine_blend_main_geometry_node_graphs_json(WebBlendMainState *state,
std::string &geometry_node_graphs_json,
std::string &error);
bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
std::string &physics_json,
std::string &error);
@@ -377,6 +386,13 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
const char *layer_id,
const char *direction,
std::string &error);
bool web_engine_blend_main_move_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
int frame,
int target_frame,
const char *drawing_id,
std::string &error);
bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
@@ -407,7 +423,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices,
const std::vector<float> &values,
bool normalize,
uint32_t limit,
bool mirror,
int mirror_axis,
float mirror_tolerance,
std::string &error);
bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
const char *target_id,