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

@@ -5,12 +5,17 @@
/** \file
* \ingroup modifiers
*
* Native Web evaluator for the first bounded Geometry Nodes closure. It
* accepts a single Group Input -> Transform Geometry -> Group Output graph.
* Unsupported graphs remain visible and report an explicit modifier error.
* Native Web evaluator for the bounded Geometry Nodes allowlist. Unsupported
* graphs remain visible and report an explicit modifier error.
*/
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>
#include <variant>
#include "MEM_guardedalloc.h"
@@ -18,19 +23,32 @@
#include "BLI_math_rotation.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_vector.hh"
#include "BKE_attribute.hh"
#include "BKE_geometry_set.hh"
#include "BKE_instances.hh"
#include "BKE_lib_query.hh"
#include "BKE_mesh.hh"
#include "BKE_modifier.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_object.hh"
#include "BLO_read_write.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_collection_types.h"
#include "DNA_image_types.h"
#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
#include "DNA_object_types.h"
#include "GEO_join_geometries.hh"
#include "GEO_realize_instances.hh"
#include "GEO_transform.hh"
#include "RNA_prototypes.hh"
#include "UI_resources.hh"
@@ -89,8 +107,39 @@ static void foreach_ID_link(ModifierData *md, Object *object, IDWalkFunc walk, v
static void update_depsgraph(ModifierData *md, const ModifierUpdateDepsgraphContext *ctx)
{
NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
if (nmd->node_group != nullptr) {
DEG_add_node_tree_output_relation(ctx->node, nmd->node_group, "Web Geometry Nodes Modifier");
if (nmd->node_group == nullptr) {
return;
}
DEG_add_node_tree_output_relation(ctx->node, nmd->node_group, "Web Geometry Nodes Modifier");
for (const bNode &node : nmd->node_group->nodes) {
for (const bNodeSocket &socket : node.inputs) {
if (socket.default_value == nullptr) {
continue;
}
if (socket.type == SOCK_OBJECT) {
Object *object = static_cast<const bNodeSocketValueObject *>(socket.default_value)->value;
if (object != nullptr && object != ctx->object) {
DEG_add_object_relation(
ctx->node, object, DEG_OB_COMP_TRANSFORM, "Web Geometry Nodes Object Info");
DEG_add_object_relation(
ctx->node, object, DEG_OB_COMP_GEOMETRY, "Web Geometry Nodes Object Info");
}
}
else if (socket.type == SOCK_COLLECTION) {
Collection *collection =
static_cast<const bNodeSocketValueCollection *>(socket.default_value)->value;
if (collection != nullptr) {
DEG_add_collection_geometry_relation(
ctx->node, collection, "Web Geometry Nodes Collection Info");
}
}
else if (socket.type == SOCK_IMAGE) {
Image *image = static_cast<const bNodeSocketValueImage *>(socket.default_value)->value;
if (image != nullptr) {
DEG_add_generic_id_relation(ctx->node, &image->id, "Web Geometry Nodes Image Info");
}
}
}
}
}
@@ -102,7 +151,7 @@ static bool is_disabled(const Scene *, ModifierData *md, bool)
static const bNodeSocket *find_input(const bNode &node, const char *identifier)
{
for (const bNodeSocket &socket : node.inputs) {
if (STREQ(socket.identifier, identifier)) {
if (STREQ(socket.identifier, identifier) || STREQ(socket.name, identifier)) {
return &socket;
}
}
@@ -119,24 +168,12 @@ static const bNodeSocket *find_output(const bNode &node, const char *identifier)
return nullptr;
}
static bool has_link(const bNodeTree &tree,
const bNode &from_node,
const char *from_socket,
const bNode &to_node,
const char *to_socket)
static bool node_is(const bNode &node, const char *idname)
{
const auto matches_socket = [](const bNodeSocket &socket, const char *name) {
return STREQ(socket.identifier, name) || STREQ(socket.name, name);
};
for (const bNodeLink &link : tree.links) {
if (link.fromnode == &from_node && link.tonode == &to_node && link.fromsock != nullptr &&
link.tosock != nullptr && matches_socket(*link.fromsock, from_socket) &&
matches_socket(*link.tosock, to_socket))
{
return true;
}
if (STREQ(node.idname, idname)) {
return true;
}
return false;
return std::string(node.idname) == "Undefined[" + std::string(idname) + "]";
}
static bool has_input_link(const bNodeTree &tree, const bNode &node, const char *socket_name)
@@ -151,6 +188,659 @@ static bool has_input_link(const bNodeTree &tree, const bNode &node, const char
return false;
}
using WebNodeValue = std::variant<std::monostate,
bke::GeometrySet,
bool,
int,
float,
float3,
std::string,
Object *,
Collection *,
Image *>;
class WebGeometryNodeEvaluator {
public:
static constexpr int max_evaluations = 4096;
static constexpr int max_point_elements = 1'000'000;
static constexpr int max_edge_elements = 2'000'000;
static constexpr int max_face_elements = 2'000'000;
static constexpr int max_corner_elements = 4'000'000;
static constexpr int max_instance_elements = 100'000;
static constexpr int64_t max_field_bytes = 64 * 1024 * 1024;
private:
const bNodeTree &tree_;
const ModifierEvalContext &ctx_;
const Mesh &input_mesh_;
std::string error_;
int evaluation_count_ = 0;
bool mesh_within_domain_budget(const Mesh &mesh)
{
if (mesh.verts_num > max_point_elements || mesh.edges_num > max_edge_elements ||
mesh.faces_num > max_face_elements || mesh.corners_num > max_corner_elements)
{
error_ = "field/domain element budget exceeded";
return false;
}
return true;
}
bool geometry_within_domain_budget(const bke::GeometrySet &geometry)
{
if (const Mesh *mesh = geometry.get_mesh()) {
if (!mesh_within_domain_budget(*mesh)) {
return false;
}
}
if (const bke::Instances *instances = geometry.get_instances()) {
if (instances->instances_num() > max_instance_elements) {
error_ = "instance domain element budget exceeded";
return false;
}
}
return true;
}
WebNodeValue bounded_geometry(bke::GeometrySet geometry)
{
if (!geometry_within_domain_budget(geometry)) {
return {};
}
return std::move(geometry);
}
const bNode *owner_of(const bNodeSocket &socket) const
{
for (const bNode &node : tree_.nodes) {
for (const bNodeSocket &candidate : node.inputs) {
if (&candidate == &socket) {
return &node;
}
}
for (const bNodeSocket &candidate : node.outputs) {
if (&candidate == &socket) {
return &node;
}
}
}
return nullptr;
}
Vector<const bNodeLink *> input_links(const bNodeSocket &socket) const
{
Vector<const bNodeLink *> links;
for (const bNodeLink &link : tree_.links) {
if (link.tosock == &socket && link.fromnode != nullptr && link.fromsock != nullptr) {
links.append(&link);
}
}
std::ranges::sort(links, [](const bNodeLink *a, const bNodeLink *b) {
return a->multi_input_sort_id > b->multi_input_sort_id;
});
return links;
}
WebNodeValue default_value(const bNodeSocket &socket)
{
if (socket.default_value == nullptr) {
return {};
}
switch (socket.type) {
case SOCK_BOOLEAN:
return static_cast<const bNodeSocketValueBoolean *>(socket.default_value)->value != 0;
case SOCK_INT:
return static_cast<const bNodeSocketValueInt *>(socket.default_value)->value;
case SOCK_FLOAT:
return static_cast<const bNodeSocketValueFloat *>(socket.default_value)->value;
case SOCK_VECTOR:
return float3(static_cast<const bNodeSocketValueVector *>(socket.default_value)->value);
case SOCK_STRING:
return std::string(
static_cast<const bNodeSocketValueString *>(socket.default_value)->value);
case SOCK_OBJECT:
return static_cast<const bNodeSocketValueObject *>(socket.default_value)->value;
case SOCK_COLLECTION:
return static_cast<const bNodeSocketValueCollection *>(socket.default_value)->value;
case SOCK_IMAGE:
return static_cast<const bNodeSocketValueImage *>(socket.default_value)->value;
default:
return {};
}
}
std::optional<float> number(const WebNodeValue &value) const
{
if (const int *integer = std::get_if<int>(&value)) {
return float(*integer);
}
if (const float *scalar = std::get_if<float>(&value)) {
return *scalar;
}
return std::nullopt;
}
bke::GeometrySet copy_geometry(const bke::GeometrySet &geometry) const
{
if (const Mesh *mesh = geometry.get_mesh()) {
return bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*mesh));
}
bke::GeometrySet copy = geometry;
copy.ensure_owns_all_data();
return copy;
}
WebNodeValue evaluate_input(const bNodeSocket &socket)
{
const Vector<const bNodeLink *> links = input_links(socket);
if (links.size() == 1) {
return evaluate_output(*links.first()->fromsock);
}
if (links.size() > 1) {
error_ = "multiple links on a non-multi-input socket";
return {};
}
return default_value(socket);
}
std::optional<bke::GeometrySet> geometry_input(const bNode &node, const char *name)
{
const bNodeSocket *socket = find_input(node, name);
if (socket == nullptr) {
error_ = std::string("missing geometry input: ") + name;
return std::nullopt;
}
WebNodeValue value = evaluate_input(*socket);
if (bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value)) {
return std::move(*geometry);
}
error_ = std::string("geometry input did not evaluate to geometry: ") + name;
return std::nullopt;
}
WebNodeValue evaluate_compare(const bNode &node)
{
const NodeFunctionCompare *storage = static_cast<const NodeFunctionCompare *>(node.storage);
const bNodeSocket *a_socket = find_input(node, "A");
const bNodeSocket *b_socket = find_input(node, "B");
if (storage == nullptr || a_socket == nullptr || b_socket == nullptr ||
!ELEM(storage->data_type, SOCK_INT, SOCK_FLOAT))
{
error_ = "Compare supports bounded Int/Float scalar inputs only";
return {};
}
const std::optional<float> a = number(evaluate_input(*a_socket));
const std::optional<float> b = number(evaluate_input(*b_socket));
if (!a || !b) {
error_ = "Compare scalar input is unavailable";
return {};
}
switch (storage->operation) {
case NODE_COMPARE_LESS_THAN:
return *a < *b;
case NODE_COMPARE_LESS_EQUAL:
return *a <= *b;
case NODE_COMPARE_GREATER_THAN:
return *a > *b;
case NODE_COMPARE_GREATER_EQUAL:
return *a >= *b;
case NODE_COMPARE_EQUAL:
return *a == *b;
case NODE_COMPARE_NOT_EQUAL:
return *a != *b;
default:
error_ = "Compare operation is outside the bounded evaluator";
return {};
}
}
WebNodeValue evaluate_math(const bNode &node)
{
const bNodeSocket *a_socket = find_input(node, "Value");
const bNodeSocket *b_socket = nullptr;
for (const bNodeSocket &socket : node.inputs) {
if (STREQ(socket.identifier, "Value_001")) {
b_socket = &socket;
}
}
if (a_socket == nullptr || b_socket == nullptr) {
error_ = "Math sockets are incomplete";
return {};
}
const std::optional<float> a = number(evaluate_input(*a_socket));
const std::optional<float> b = number(evaluate_input(*b_socket));
if (!a || !b) {
error_ = "Math scalar input is unavailable";
return {};
}
switch (node.custom1) {
case NODE_MATH_ADD:
return *a + *b;
case NODE_MATH_SUBTRACT:
return *a - *b;
case NODE_MATH_MULTIPLY:
return *a * *b;
case NODE_MATH_DIVIDE:
return *b == 0.0f ? 0.0f : *a / *b;
case NODE_MATH_MINIMUM:
return std::min(*a, *b);
case NODE_MATH_MAXIMUM:
return std::max(*a, *b);
default:
error_ = "Math operation is outside the bounded evaluator";
return {};
}
}
WebNodeValue evaluate_collection_info(const bNode &node)
{
const bNodeSocket *collection_socket = find_input(node, "Collection");
const bNodeSocket *separate_socket = find_input(node, "Separate Children");
const bNodeSocket *reset_socket = find_input(node, "Reset Children");
if (collection_socket == nullptr || separate_socket == nullptr || reset_socket == nullptr) {
error_ = "Collection Info sockets are incomplete";
return {};
}
const WebNodeValue collection_value = evaluate_input(*collection_socket);
const WebNodeValue separate_value = evaluate_input(*separate_socket);
const WebNodeValue reset_value = evaluate_input(*reset_socket);
Collection *const *collection = std::get_if<Collection *>(&collection_value);
const bool *separate_children = std::get_if<bool>(&separate_value);
const bool *reset_children = std::get_if<bool>(&reset_value);
if (collection == nullptr || *collection == nullptr || separate_children == nullptr ||
reset_children == nullptr || !*separate_children)
{
error_ = "Collection Info requires a concrete collection with Separate Children enabled";
return {};
}
Vector<Object *> objects;
for (const CollectionObject &entry : (*collection)->gobject) {
if (entry.ob != nullptr && entry.ob != ctx_.object) {
objects.append(entry.ob);
}
}
if (objects.size() > max_instance_elements) {
error_ = "Collection Info instance domain budget exceeded";
return {};
}
auto instances = std::make_unique<bke::Instances>();
instances->resize(objects.size());
MutableSpan<int> handles = instances->reference_handles_for_write();
MutableSpan<float4x4> transforms = instances->transforms_for_write();
for (const int index : objects.index_range()) {
Object *evaluated = reinterpret_cast<Object *>(
DEG_get_evaluated_id(ctx_.depsgraph, &objects[index]->id));
if (evaluated == nullptr) {
error_ = "Collection Info object is not available in the evaluated dependency graph";
return {};
}
handles[index] = instances->add_reference(*evaluated);
transforms[index] = *reset_children ? float4x4::identity() : evaluated->object_to_world();
}
instances->tag_reference_handles_changed();
return bounded_geometry(bke::GeometrySet::from_instances(std::move(instances)));
}
WebNodeValue evaluate_output(const bNodeSocket &socket)
{
if (++evaluation_count_ > max_evaluations) {
error_ = "evaluation budget exceeded";
return {};
}
const bNode *node = owner_of(socket);
if (node == nullptr) {
error_ = "output socket owner is missing";
return {};
}
if (node_is(*node, "NodeGroupInput")) {
if (socket.type != SOCK_GEOMETRY) {
error_ = "only the Geometry group input is supported";
return {};
}
if (!mesh_within_domain_budget(input_mesh_)) {
return {};
}
return bounded_geometry(
bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(input_mesh_)));
}
if (node_is(*node, "FunctionNodeInputInt")) {
const NodeInputInt *storage = static_cast<const NodeInputInt *>(node->storage);
return storage == nullptr ? WebNodeValue{} : WebNodeValue{storage->integer};
}
if (node_is(*node, "FunctionNodeInputVector")) {
const NodeInputVector *storage = static_cast<const NodeInputVector *>(node->storage);
if (storage == nullptr || storage->dimensions != 3) {
error_ = "Vector input requires exactly three dimensions";
return {};
}
return float3(storage->vector);
}
if (node_is(*node, "ShaderNodeValue")) {
return default_value(socket);
}
if (node_is(*node, "ShaderNodeMath")) {
return evaluate_math(*node);
}
if (node_is(*node, "FunctionNodeCompare")) {
return evaluate_compare(*node);
}
if (node_is(*node, "GeometryNodeImageInfo")) {
const bNodeSocket *image_socket = find_input(*node, "Image");
if (image_socket == nullptr) {
error_ = "Image Info image input is missing";
return {};
}
const WebNodeValue image_value = evaluate_input(*image_socket);
Image *const *image = std::get_if<Image *>(&image_value);
if (image == nullptr || *image == nullptr) {
error_ = "Image Info image is unavailable";
return {};
}
if (STREQ(socket.identifier, "Width")) {
return (*image)->gen_x;
}
if (STREQ(socket.identifier, "Height")) {
return (*image)->gen_y;
}
if (STREQ(socket.identifier, "Has Alpha")) {
return true;
}
if (STREQ(socket.identifier, "Frame Count")) {
return 1;
}
if (STREQ(socket.identifier, "FPS")) {
return 0.0f;
}
error_ = "Image Info output is unsupported";
return {};
}
if (node_is(*node, "GeometryNodeObjectInfo")) {
const bNodeSocket *object_socket = find_input(*node, "Object");
if (object_socket == nullptr) {
error_ = "Object Info object input is missing";
return {};
}
const WebNodeValue object_value = evaluate_input(*object_socket);
Object *const *object = std::get_if<Object *>(&object_value);
if (object == nullptr || *object == nullptr || *object == ctx_.object) {
error_ = "Object Info target is unavailable or recursive";
return {};
}
Object *evaluated = reinterpret_cast<Object *>(
DEG_get_evaluated_id(ctx_.depsgraph, &(*object)->id));
if (evaluated == nullptr) {
error_ = "Object Info target is not evaluated";
return {};
}
if (STREQ(socket.identifier, "Location")) {
return float3(evaluated->object_to_world().location());
}
if (STREQ(socket.identifier, "Scale")) {
return float3(evaluated->scale);
}
if (STREQ(socket.identifier, "Geometry")) {
Mesh *target_mesh = BKE_object_get_evaluated_mesh(evaluated);
if (target_mesh == nullptr) {
error_ = "Object Info target has no evaluated mesh";
return {};
}
if (!mesh_within_domain_budget(*target_mesh)) {
return {};
}
return bounded_geometry(
bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*target_mesh)));
}
error_ = "Object Info output is outside the bounded evaluator";
return {};
}
if (node_is(*node, "GeometryNodeCollectionInfo")) {
return evaluate_collection_info(*node);
}
if (node_is(*node, "GeometryNodeJoinGeometry")) {
const bNodeSocket *input = find_input(*node, "Geometry");
if (input == nullptr) {
error_ = "Join Geometry input is missing";
return {};
}
Vector<bke::GeometrySet> geometries;
for (const bNodeLink *link : input_links(*input)) {
WebNodeValue value = evaluate_output(*link->fromsock);
bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value);
if (geometry == nullptr) {
error_ = "Join Geometry received a non-geometry input";
return {};
}
geometries.append(std::move(*geometry));
}
if (geometries.is_empty()) {
return bke::GeometrySet();
}
int64_t points = 0;
int64_t edges = 0;
int64_t faces = 0;
int64_t corners = 0;
int64_t instances = 0;
for (const bke::GeometrySet &geometry : geometries) {
if (const Mesh *mesh = geometry.get_mesh()) {
points += mesh->verts_num;
edges += mesh->edges_num;
faces += mesh->faces_num;
corners += mesh->corners_num;
}
if (const bke::Instances *geometry_instances = geometry.get_instances()) {
instances += geometry_instances->instances_num();
}
}
if (points > max_point_elements || edges > max_edge_elements ||
faces > max_face_elements || corners > max_corner_elements ||
instances > max_instance_elements)
{
error_ = "Join Geometry field/domain budget exceeded";
return {};
}
return bounded_geometry(geometry::join_geometries(
geometries.as_span(), bke::AttributeFilter::default_filter()));
}
if (node_is(*node, "GeometryNodeSeparateGeometry")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
if (!geometry || selection_socket == nullptr) {
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const bool *selection = std::get_if<bool>(&selection_value);
if (selection == nullptr) {
error_ = "Separate Geometry requires a bounded constant selection";
return {};
}
const bool selected_output = STREQ(socket.identifier, "Selection");
const bool inverted_output = STREQ(socket.identifier, "Inverted");
if (!selected_output && !inverted_output) {
error_ = "Separate Geometry output is invalid";
return {};
}
return bounded_geometry((*selection == selected_output) ? std::move(*geometry) :
bke::GeometrySet());
}
if (node_is(*node, "GeometryNodeRealizeInstances")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
if (!geometry) {
return {};
}
geometry::RealizeInstancesOptions options;
options.keep_original_ids = false;
options.realize_instance_attributes = true;
options.realize_to_point_domain = true;
geometry::RealizeInstancesResult result = geometry::realize_instances(std::move(*geometry),
options);
if (!result.errors.is_empty()) {
error_ = result.errors.first();
return {};
}
return bounded_geometry(std::move(result.geometry));
}
if (node_is(*node, "GeometryNodeStoreNamedAttribute")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
const bNodeSocket *name_socket = find_input(*node, "Name");
const bNodeSocket *value_socket = find_input(*node, "Value");
const NodeGeometryStoreNamedAttribute *storage =
static_cast<const NodeGeometryStoreNamedAttribute *>(node->storage);
if (!geometry || selection_socket == nullptr || name_socket == nullptr ||
value_socket == nullptr || storage == nullptr || storage->data_type != CD_PROP_FLOAT ||
storage->domain != int8_t(bke::AttrDomain::Point))
{
error_ = "Store Named Attribute supports Float point attributes only";
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const WebNodeValue name_value = evaluate_input(*name_socket);
const std::optional<float> attribute_value = number(evaluate_input(*value_socket));
const bool *selection = std::get_if<bool>(&selection_value);
const std::string *name = std::get_if<std::string>(&name_value);
if (selection == nullptr || name == nullptr || name->empty() || name->size() > 64 ||
!attribute_value)
{
error_ = "Store Named Attribute inputs are invalid";
return {};
}
bke::GeometrySet result = copy_geometry(*geometry);
if (*selection) {
Mesh *result_mesh = result.get_mesh_for_write();
if (result_mesh == nullptr) {
error_ = "Store Named Attribute requires mesh geometry";
return {};
}
if (!mesh_within_domain_budget(*result_mesh) ||
int64_t(result_mesh->verts_num) * int64_t(sizeof(float)) > max_field_bytes)
{
error_ = "Store Named Attribute field budget exceeded";
return {};
}
bke::SpanAttributeWriter<float> writer =
result_mesh->attributes_for_write().lookup_or_add_for_write_span<float>(
*name, bke::AttrDomain::Point);
if (!writer) {
error_ = "Store Named Attribute could not allocate the point attribute";
return {};
}
writer.span.fill(*attribute_value);
writer.finish();
}
return bounded_geometry(std::move(result));
}
if (node_is(*node, "GeometryNodeTransform")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *translation_socket = find_input(*node, "Translation");
const bNodeSocket *rotation_socket = find_input(*node, "Rotation");
const bNodeSocket *scale_socket = find_input(*node, "Scale");
if (!geometry || translation_socket == nullptr || rotation_socket == nullptr ||
scale_socket == nullptr || translation_socket->default_value == nullptr ||
rotation_socket->default_value == nullptr || scale_socket->default_value == nullptr ||
has_input_link(tree_, *node, "Rotation") || has_input_link(tree_, *node, "Scale"))
{
error_ = "Transform Geometry inputs are outside the bounded evaluator";
return {};
}
WebNodeValue translation_value = evaluate_input(*translation_socket);
const float3 *translation = std::get_if<float3>(&translation_value);
if (translation == nullptr) {
error_ = "Transform Geometry translation is unavailable";
return {};
}
const bNodeSocketValueRotation &rotation_value =
*static_cast<const bNodeSocketValueRotation *>(rotation_socket->default_value);
const math::Quaternion rotation = math::to_quaternion(
math::EulerXYZ(float3(rotation_value.value_euler)));
const float3 scale(
static_cast<const bNodeSocketValueVector *>(scale_socket->default_value)->value);
bke::GeometrySet result = copy_geometry(*geometry);
geometry::transform_geometry(
result, math::from_loc_rot_scale<float4x4>(*translation, rotation, scale));
return bounded_geometry(std::move(result));
}
if (node_is(*node, "GeometryNodeSetPosition")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
const bNodeSocket *offset_socket = find_input(*node, "Offset");
if (!geometry || selection_socket == nullptr || offset_socket == nullptr ||
has_input_link(tree_, *node, "Position"))
{
error_ = "Set Position inputs are outside the bounded evaluator";
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const WebNodeValue offset_value = evaluate_input(*offset_socket);
const bool *selection = std::get_if<bool>(&selection_value);
const float3 *offset = std::get_if<float3>(&offset_value);
if (selection == nullptr || offset == nullptr) {
error_ = "Set Position requires a constant selection and vector offset";
return {};
}
bke::GeometrySet result = copy_geometry(*geometry);
if (*selection) {
geometry::translate_geometry(result, *offset);
}
return bounded_geometry(std::move(result));
}
error_ = std::string("unsupported node type: ") + node->idname;
return {};
}
public:
WebGeometryNodeEvaluator(const bNodeTree &tree,
const ModifierEvalContext &ctx,
const Mesh &input_mesh)
: tree_(tree), ctx_(ctx), input_mesh_(input_mesh)
{
}
Mesh *evaluate()
{
const bNode *group_output = nullptr;
for (const bNode &node : tree_.nodes) {
if (node_is(node, "NodeGroupOutput")) {
if (group_output != nullptr) {
error_ = "multiple Group Output nodes are unsupported";
return nullptr;
}
group_output = &node;
}
}
const bNodeSocket *geometry_socket = group_output == nullptr ? nullptr :
find_input(*group_output,
"Geometry");
if (geometry_socket == nullptr) {
error_ = "Geometry Group Output is missing";
return nullptr;
}
WebNodeValue value = evaluate_input(*geometry_socket);
bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value);
if (geometry == nullptr || !error_.empty()) {
if (error_.empty()) {
error_ = "Geometry Group Output did not evaluate to geometry";
}
return nullptr;
}
if (!geometry_within_domain_budget(*geometry)) {
return nullptr;
}
bke::MeshComponent &mesh_component =
geometry->get_component_for_write<bke::MeshComponent>();
mesh_component.ensure_owns_direct_data();
Mesh *result = mesh_component.release();
return result == nullptr ? BKE_mesh_new_nomain(0, 0, 0, 0) : result;
}
const std::string &error() const
{
return error_;
}
};
static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh *mesh)
{
const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(md);
@@ -168,13 +858,8 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
return mesh;
}
const bNode *group_input = nullptr;
const bNode *group_output = nullptr;
const bNode *transform = nullptr;
const bNode *set_position = nullptr;
bool has_simulation_node = false;
int node_count = 0;
int link_count = 0;
for (const bNode &node : tree->nodes) {
node_count++;
if (ELEM(node.type_legacy,
@@ -188,25 +873,6 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
{
has_simulation_node = true;
}
if (find_input(node, "Translation") != nullptr && find_input(node, "Rotation") != nullptr &&
find_input(node, "Scale") != nullptr)
{
transform = &node;
}
else if (find_input(node, "Offset") != nullptr && find_input(node, "Position") != nullptr &&
find_input(node, "Selection") != nullptr)
{
set_position = &node;
}
else if (node.inputs.first == nullptr && node.outputs.first != nullptr) {
group_input = &node;
}
else if (node.inputs.first != nullptr && node.outputs.first == nullptr) {
group_output = &node;
}
}
for ([[maybe_unused]] const bNodeLink &link : tree->links) {
link_count++;
}
if (has_simulation_node) {
@@ -217,85 +883,20 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
return mesh;
}
const bNode *geometry_node = transform != nullptr ? transform : set_position;
const bool input_link_valid = group_input != nullptr && geometry_node != nullptr &&
has_link(*tree, *group_input, "Geometry", *geometry_node, "Geometry");
const bool output_link_valid = geometry_node != nullptr && group_output != nullptr &&
has_link(*tree, *geometry_node, "Geometry", *group_output, "Geometry");
if (node_count != 3 || link_count != 2 || group_input == nullptr ||
group_output == nullptr || geometry_node == nullptr || (transform != nullptr && set_position != nullptr) ||
!input_link_valid || !output_link_valid)
{
BKE_modifier_set_error(
ctx->object,
md,
"Web Geometry Nodes supports only a single constant Transform Geometry or Set Position node");
return mesh;
}
if (set_position != nullptr) {
const bNodeSocket *selection_socket = find_input(*set_position, "Selection");
const bNodeSocket *offset_socket = find_input(*set_position, "Offset");
if (selection_socket == nullptr || offset_socket == nullptr ||
selection_socket->default_value == nullptr || offset_socket->default_value == nullptr ||
has_input_link(*tree, *set_position, "Selection") ||
has_input_link(*tree, *set_position, "Position") ||
has_input_link(*tree, *set_position, "Offset"))
{
BKE_modifier_set_error(
ctx->object, md, "Web Geometry Nodes Set Position supports only unlinked constant inputs");
return mesh;
}
Mesh *result = BKE_mesh_copy_for_eval(*mesh);
const bool selected = static_cast<const bNodeSocketValueBoolean *>(
selection_socket->default_value)
->value;
if (selected) {
const float3 offset(
static_cast<const bNodeSocketValueVector *>(offset_socket->default_value)->value);
for (float3 &position : result->vert_positions_for_write()) {
position += offset;
}
result->tag_positions_changed();
}
return result;
}
const bNodeSocket *mode_socket = find_input(*transform, "Mode");
const int mode = mode_socket && mode_socket->default_value ?
static_cast<const bNodeSocketValueMenu *>(mode_socket->default_value)->value :
GEO_NODE_TRANSFORM_MODE_COMPONENTS;
if (mode != GEO_NODE_TRANSFORM_MODE_COMPONENTS) {
if (node_count > WebGeometryNodeEvaluator::max_evaluations) {
BKE_modifier_set_error(ctx->object,
md,
"Web Geometry Nodes Transform supports Components mode only");
"WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: node budget exceeded");
return mesh;
}
const bNodeSocket *translation_socket = find_input(*transform, "Translation");
const bNodeSocket *rotation_socket = find_input(*transform, "Rotation");
const bNodeSocket *scale_socket = find_input(*transform, "Scale");
if (translation_socket == nullptr || rotation_socket == nullptr || scale_socket == nullptr ||
translation_socket->default_value == nullptr || rotation_socket->default_value == nullptr ||
scale_socket->default_value == nullptr)
{
BKE_modifier_set_error(ctx->object, md, "Web Geometry Nodes Transform sockets are incomplete");
WebGeometryNodeEvaluator evaluator(*tree, *ctx, *mesh);
Mesh *result = evaluator.evaluate();
if (result == nullptr) {
const std::string message = "WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: " + evaluator.error();
BKE_modifier_set_error(ctx->object, md, "%s", message.c_str());
return mesh;
}
const float3 translation(
static_cast<const bNodeSocketValueVector *>(translation_socket->default_value)->value);
const bNodeSocketValueRotation &rotation_value =
*static_cast<const bNodeSocketValueRotation *>(rotation_socket->default_value);
const math::Quaternion rotation = math::to_quaternion(
math::EulerXYZ(float3(rotation_value.value_euler)));
const float3 scale(
static_cast<const bNodeSocketValueVector *>(scale_socket->default_value)->value);
Mesh *result = BKE_mesh_copy_for_eval(*mesh);
bke::mesh_transform(
*result, math::from_loc_rot_scale<float4x4>(translation, rotation, scale), false);
return result;
}

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,