Advance M8-M11 parity workflows
This commit is contained in:
@@ -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")
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]]);
|
||||
|
||||
@@ -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 ¤t = 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user