Advance Blender WebEngine N-015 through N-022 parity

This commit is contained in:
mes123456
2026-08-12 13:26:34 -04:00
parent 9fd26010f6
commit b3cefaeec5
51 changed files with 1809 additions and 171 deletions

View File

@@ -1525,7 +1525,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
type == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" ||
type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" ||
type == "setGreasePencilStrokes" || type == "setVertexColors" ||
type == "setVertexWeights" || type == "setLightProperties" ||
type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" ||
type == "setWorldProperties" ||
type == "setSculptMeshAttributes" ||
type == "sculptStroke") {
@@ -2031,7 +2031,7 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("values", std::vector<float>()),
command.value("normalize", false), command.value("mirror", false), main_error);
}
else if (type == "setLightProperties" || type == "setWorldProperties")
else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties")
{
const json properties = command.value("properties", json::object());
applied = web_engine_blend_main_set_render_properties(

View File

@@ -23,15 +23,19 @@
#include "BLI_endian_defines.h"
#include "BLI_filereader.h"
#include "BLI_utildefines.h"
#include "BLO_core_bhead.hh"
#include "BLO_core_blend_header.hh"
#include "BLO_core_file_reader.hh"
#include "DNA_genfile.h"
#include "DNA_curve_enums.h"
#include "DNA_mask_types.h"
#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
#include "DNA_sdna_types.h"
#include "DNA_sequence_types.h"
namespace {
@@ -460,6 +464,7 @@ std::string id_prefix(const std::string &type_name)
if (type_name == "GreasePencil") return "grease-pencil";
if (type_name == "Text") return "text";
if (type_name == "VFont") return "vfont";
if (type_name == "Mask") return "mask";
return "datablock";
}
@@ -472,7 +477,7 @@ bool is_exported_id_type(const std::string &type_name)
type_name == "bArmature" || type_name == "World" || type_name == "Curve" ||
type_name == "MetaBall" || type_name == "PointCloud" || type_name == "Curves" ||
type_name == "Volume" || type_name == "GreasePencil" || type_name == "Text" ||
type_name == "VFont";
type_name == "VFont" || type_name == "Mask";
}
std::string unique_id(const std::string &prefix,
@@ -655,6 +660,396 @@ std::vector<float> node_socket_default_values(const ParsedBlend &blend, const El
return read_float_array(*blend.sdna, *default_value, "value", 4);
}
std::optional<ElementRef> raw_array_element(const ParsedBlend &blend,
uint64_t pointer,
const std::string &type_name,
size_t index);
std::string node_element_id(const ParsedBlend &blend, const ElementRef &node)
{
const int64_t identifier = read_integer(*blend.sdna, node, "identifier").value_or(0);
return "compositor-node:" + std::to_string(identifier);
}
std::optional<json> compositor_graph_from_scene(const ParsedBlend &blend,
const ElementRef &scene,
const std::string &scene_id,
const std::string &scene_name)
{
std::optional<uint64_t> node_tree_pointer = read_pointer(
*blend.sdna, scene, "compositing_node_group");
if (!node_tree_pointer || *node_tree_pointer == 0) {
node_tree_pointer = read_pointer(*blend.sdna, scene, "nodetree");
}
if (!node_tree_pointer || *node_tree_pointer == 0) return std::nullopt;
const std::optional<ElementRef> node_tree = element_for_pointer(blend, *node_tree_pointer);
if (!node_tree) return json{{"status", "BLOCKED"}};
const std::vector<ElementRef> source_nodes = linked_list_elements(blend, *node_tree, "nodes");
const std::vector<ElementRef> source_links = linked_list_elements(blend, *node_tree, "links");
if (source_nodes.empty() || source_nodes.size() > 4096 || source_links.size() > 16384) {
return json{{"status", "BLOCKED"}};
}
json nodes = json::array();
json links = json::array();
std::unordered_map<uint64_t, std::string> node_ids;
std::unordered_set<std::string> used_node_ids;
std::string output_node_id;
std::string fallback_output_node_id;
for (const ElementRef &node : source_nodes) {
if (node.block == nullptr) return json{{"status", "BLOCKED"}};
const uint64_t pointer = block_pointer(*node.block) + node.offset;
std::string node_id = node_element_id(blend, node);
if (!used_node_ids.insert(node_id).second) node_id += ":" + std::to_string(pointer);
if (node_id.size() > 256) return json{{"status", "BLOCKED"}};
node_ids[pointer] = node_id;
const std::string blender_type = read_string(*blend.sdna, node, "idname");
const std::string node_name = read_string(*blend.sdna, node, "name");
json node_ir = {{"id", node_id},
{"type", "UNSUPPORTED"},
{"name", (node_name.empty() ? blender_type : node_name).substr(0, 256)},
{"blenderType", blender_type.empty() ? "UNKNOWN" : blender_type.substr(0, 256)},
{"properties", json::object()}};
if (blender_type == "CompositorNodeComposite" || blender_type == "NodeGroupOutput") {
node_ir["type"] = "COMPOSITE";
node_ir.erase("blenderType");
fallback_output_node_id = node_id;
if ((read_integer(*blend.sdna, node, "flag").value_or(0) & NODE_DO_OUTPUT) != 0) {
output_node_id = node_id;
}
}
else if (blender_type == "CompositorNodeViewer") {
node_ir["type"] = "VIEWER";
node_ir.erase("blenderType");
}
else if (blender_type == "CompositorNodeRGB") {
const std::vector<ElementRef> outputs = linked_list_elements(blend, node, "outputs");
const std::vector<float> values = outputs.empty() ? std::vector<float>() :
node_socket_default_values(blend, outputs.front());
if (values.size() >= 4 && std::all_of(values.begin(), values.begin() + 4, [](const float value) {
return std::isfinite(value) && value >= -65504.0f && value <= 65504.0f;
}))
{
node_ir["type"] = "CONSTANT_COLOR";
node_ir.erase("blenderType");
node_ir["properties"] = {{"color", {values[0], values[1], values[2], values[3]}}};
}
}
nodes.push_back(std::move(node_ir));
}
if (output_node_id.empty()) output_node_id = fallback_output_node_id;
if (output_node_id.empty()) return json{{"status", "BLOCKED"}};
std::unordered_set<std::string> destinations;
for (const ElementRef &link : source_links) {
if ((read_integer(*blend.sdna, link, "flag").value_or(0) & NODE_LINK_MUTED) != 0) continue;
const std::optional<uint64_t> from_node_pointer = read_pointer(*blend.sdna, link, "fromnode");
const std::optional<uint64_t> to_node_pointer = read_pointer(*blend.sdna, link, "tonode");
const std::optional<uint64_t> from_socket_pointer = read_pointer(*blend.sdna, link, "fromsock");
const std::optional<uint64_t> to_socket_pointer = read_pointer(*blend.sdna, link, "tosock");
if (!from_node_pointer || !to_node_pointer || !from_socket_pointer || !to_socket_pointer) continue;
const auto from_id = node_ids.find(*from_node_pointer);
const auto to_id = node_ids.find(*to_node_pointer);
const std::optional<ElementRef> from_socket = element_for_pointer(blend, *from_socket_pointer);
const std::optional<ElementRef> to_socket = element_for_pointer(blend, *to_socket_pointer);
if (from_id == node_ids.end() || to_id == node_ids.end() || !from_socket || !to_socket) continue;
std::string from_socket_name = read_string(*blend.sdna, *from_socket, "identifier");
std::string to_socket_name = read_string(*blend.sdna, *to_socket, "identifier");
if (from_socket_name.empty()) from_socket_name = read_string(*blend.sdna, *from_socket, "name");
if (to_socket_name.empty()) to_socket_name = read_string(*blend.sdna, *to_socket, "name");
if (from_socket_name.empty() || to_socket_name.empty() || from_socket_name.size() > 256 ||
to_socket_name.size() > 256)
{
return json{{"status", "BLOCKED"}};
}
const std::string destination = to_id->second + std::string(1, '\0') + to_socket_name;
if (!destinations.insert(destination).second) return json{{"status", "BLOCKED"}};
links.push_back({{"fromNodeId", from_id->second},
{"fromSocket", from_socket_name},
{"toNodeId", to_id->second},
{"toSocket", to_socket_name}});
}
return json{{"status", "AVAILABLE"},
{"graph",
{{"schemaVersion", 1},
{"id", "compositor:" + scene_id},
{"name", scene_name.empty() ? "Compositor" : scene_name + " Compositor"},
{"outputNodeId", output_node_id},
{"nodes", std::move(nodes)},
{"links", std::move(links)},
{"resources", json::array()}}}};
}
std::optional<json> sequencer_timeline_from_scene(const ParsedBlend &blend,
const ElementRef &scene,
const std::string &scene_id,
const uint64_t revision,
const int frame_start,
const int frame_end,
const int fps,
const float fps_base,
const std::unordered_map<uint64_t, std::string> &ids_by_pointer)
{
const std::optional<uint64_t> editing_pointer = read_pointer(*blend.sdna, scene, "ed");
if (!editing_pointer || *editing_pointer == 0) return std::nullopt;
const std::optional<ElementRef> editing = element_for_pointer(blend, *editing_pointer);
if (!editing) return json{{"status", "BLOCKED"}};
const std::vector<ElementRef> top_level = linked_list_elements(blend, *editing, "seqbase");
if (top_level.size() > 100000 || fps <= 0 || !std::isfinite(fps_base) || fps_base <= 0.0f) {
return json{{"status", "BLOCKED"}};
}
json strips = json::array();
std::unordered_map<uint64_t, std::string> strip_ids;
std::unordered_set<std::string> used_strip_ids;
size_t total_strip_count = 0;
auto reject = [](const std::string &) { return false; };
auto strip_name = [&](const ElementRef &strip) {
const std::string stored = read_string(*blend.sdna, strip, "name");
return stored.rfind("SQ", 0) == 0 ? stored.substr(2) : stored;
};
std::function<bool(const std::vector<ElementRef> &)> collect = [&](const std::vector<ElementRef> &items) {
for (const ElementRef &strip : items) {
if (strip.block == nullptr || ++total_strip_count > 100000) return reject("strip-budget");
const uint64_t pointer = block_pointer(*strip.block) + strip.offset;
const std::string name = strip_name(strip);
std::string id = "strip:" + (name.empty() ? std::to_string(pointer) : name);
if (!used_strip_ids.insert(id).second) id += ":" + std::to_string(pointer);
if (id.size() > 256) return reject("strip-id");
strip_ids[pointer] = id;
const int64_t strip_type = read_integer(*blend.sdna, strip, "type").value_or(-1);
if (strip_type == STRIP_TYPE_META) {
const std::vector<ElementRef> children = linked_list_elements(blend, strip, "seqbase");
if (!collect(children)) return reject("meta-collect");
}
}
return true;
};
if (!collect(top_level)) return json{{"status", "BLOCKED"}};
std::function<bool(const std::vector<ElementRef> &)> emit = [&](const std::vector<ElementRef> &items) {
for (const ElementRef &strip : items) {
const uint64_t pointer = block_pointer(*strip.block) + strip.offset;
const auto id_it = strip_ids.find(pointer);
if (id_it == strip_ids.end()) return reject("strip-identity");
const int64_t strip_type = read_integer(*blend.sdna, strip, "type").value_or(-1);
const char *type = strip_type == STRIP_TYPE_SCENE ? "SCENE" :
strip_type == STRIP_TYPE_MOVIE ? "MOVIE" :
strip_type == STRIP_TYPE_IMAGE ? "IMAGE" :
strip_type == STRIP_TYPE_SOUND ? "SOUND" :
strip_type == STRIP_TYPE_META ? "META" :
ELEM(strip_type,
STRIP_TYPE_CROSS,
STRIP_TYPE_GAMCROSS,
STRIP_TYPE_ADD,
STRIP_TYPE_MUL,
STRIP_TYPE_TRANSFORM_LEGACY,
STRIP_TYPE_COLOR) ?
"EFFECT" :
nullptr;
if (type == nullptr) return reject("strip-type:" + std::to_string(strip_type));
const int channel = int(read_integer(*blend.sdna, strip, "channel").value_or(0));
const int length = int(read_integer(*blend.sdna, strip, "len").value_or(0));
const float content_start = read_float(*blend.sdna, strip, "start").value_or(0.0f);
const float start_offset = read_float(*blend.sdna, strip, "startofs").value_or(0.0f);
const float end_offset = read_float(*blend.sdna, strip, "endofs").value_or(0.0f);
const int display_start = int(std::round(content_start + start_offset));
const int display_end = int(std::round(content_start + float(length) - end_offset));
const int animation_start_offset = int(
read_integer(*blend.sdna, strip, "anim_startofs").value_or(0));
const int animation_end_offset = int(
read_integer(*blend.sdna, strip, "anim_endofs").value_or(0));
const float speed_factor = read_float(*blend.sdna, strip, "speed_factor").value_or(1.0f);
const int64_t flags = read_integer(*blend.sdna, strip, "flag").value_or(0);
if (channel < 1 || channel > 128 || display_end <= display_start || length < 0 ||
!std::isfinite(speed_factor))
{
return reject("strip-range:" + id_it->second + ":" + std::to_string(channel) + ":" +
std::to_string(display_start) + ":" + std::to_string(display_end) + ":" +
std::to_string(length));
}
const int source_start = animation_start_offset;
const int source_end = std::max(source_start, length - animation_end_offset);
const float speed = speed_factor > 0.0f ? std::min(speed_factor, 1000.0f) : 1.0f;
json result = {{"id", id_it->second},
{"name", strip_name(strip)},
{"type", type},
{"channel", channel},
{"frameStart", display_start},
{"frameEnd", display_end},
{"sourceStart", source_start},
{"sourceEnd", source_end},
{"speed", speed},
{"muted", (flags & SEQ_MUTE) != 0},
{"locked", (flags & SEQ_LOCK) != 0}};
if (strip_type == STRIP_TYPE_SCENE) {
const std::optional<uint64_t> source_scene = read_pointer(*blend.sdna, strip, "scene");
const auto source_id = source_scene ? ids_by_pointer.find(*source_scene) : ids_by_pointer.end();
if (source_id == ids_by_pointer.end()) return reject("scene-source");
result["sourceId"] = source_id->second;
}
else if (ELEM(strip_type, STRIP_TYPE_MOVIE, STRIP_TYPE_IMAGE)) {
const std::optional<uint64_t> data_pointer = read_pointer(*blend.sdna, strip, "data");
const std::optional<ElementRef> data = data_pointer ? element_for_pointer(blend, *data_pointer) :
std::nullopt;
if (!data) return reject("media-data");
std::string path = read_string(*blend.sdna, *data, "dirpath");
const std::optional<uint64_t> element_pointer = read_pointer(*blend.sdna, *data, "stripdata");
const std::optional<ElementRef> source_element = element_pointer ?
element_for_pointer(blend, *element_pointer) :
std::nullopt;
if (source_element) path += read_string(*blend.sdna, *source_element, "filename");
if (path.rfind("//", 0) != 0 || path.size() <= 2 || path.size() > 2050) return reject("media-path:" + path);
result["sourcePath"] = path;
}
else if (strip_type == STRIP_TYPE_SOUND) {
const std::optional<uint64_t> sound_pointer = read_pointer(*blend.sdna, strip, "sound");
const std::optional<ElementRef> sound = sound_pointer ? element_for_pointer(blend, *sound_pointer) :
std::nullopt;
if (!sound) return reject("sound-data");
const std::string path = read_string(*blend.sdna, *sound, "filepath");
if (path.rfind("//", 0) != 0 || path.size() <= 2 || path.size() > 2050) return reject("sound-path:" + path);
result["sourcePath"] = path;
}
else if (strip_type == STRIP_TYPE_META) {
const std::vector<ElementRef> children = linked_list_elements(blend, strip, "seqbase");
json child_ids = json::array();
for (const ElementRef &child : children) {
const uint64_t child_pointer = block_pointer(*child.block) + child.offset;
const auto child_id = strip_ids.find(child_pointer);
if (child_id == strip_ids.end()) return reject("meta-child");
child_ids.push_back(child_id->second);
}
result["childStripIds"] = std::move(child_ids);
if (!emit(children)) return reject("meta-emit");
}
else {
const char *effect = strip_type == STRIP_TYPE_CROSS ? "CROSS" :
strip_type == STRIP_TYPE_GAMCROSS ? "GAMMA_CROSS" :
strip_type == STRIP_TYPE_ADD ? "ADD" :
strip_type == STRIP_TYPE_MUL ? "MULTIPLY" :
strip_type == STRIP_TYPE_TRANSFORM_LEGACY ? "TRANSFORM" : "COLOR";
json inputs = json::array();
for (const char *member : {"input1", "input2"}) {
const std::optional<uint64_t> input_pointer = read_pointer(*blend.sdna, strip, member);
if (!input_pointer || *input_pointer == 0) continue;
const auto input_id = strip_ids.find(*input_pointer);
if (input_id == strip_ids.end()) return reject("effect-input");
inputs.push_back(input_id->second);
}
if (inputs.empty()) return reject("effect-empty");
result["effectType"] = effect;
result["inputStripIds"] = std::move(inputs);
}
strips.push_back(std::move(result));
}
return true;
};
if (!emit(top_level)) return json{{"status", "BLOCKED"}};
const int fps_denominator = std::max(1, int(std::round(fps_base * 1000.0f)));
return json{{"status", "AVAILABLE"},
{"timeline",
{{"schemaVersion", 1},
{"id", "sequencer:" + scene_id},
{"revision", revision},
{"frameStart", frame_start},
{"frameEnd", frame_end},
{"fpsNumerator", fps * 1000},
{"fpsDenominator", fps_denominator},
{"strips", std::move(strips)}}}};
}
std::optional<json> mask_from_record(const ParsedBlend &blend,
const ElementRef &mask,
const std::string &mask_id,
const std::string &mask_name)
{
const std::vector<ElementRef> source_layers = linked_list_elements(blend, mask, "masklayers");
if (source_layers.size() > 1024) return std::nullopt;
json layers = json::array();
size_t spline_count = 0;
size_t point_count = 0;
size_t layer_index = 0;
for (const ElementRef &layer : source_layers) {
const std::string layer_name = read_string(*blend.sdna, layer, "name");
const int64_t layer_flags = read_integer(*blend.sdna, layer, "flag").value_or(0);
const int64_t visibility_flags = read_integer(*blend.sdna, layer, "visibility_flag").value_or(0);
const float opacity = read_float(*blend.sdna, layer, "alpha").value_or(1.0f);
if (!std::isfinite(opacity) || opacity < 0.0f || opacity > 1.0f) return std::nullopt;
json splines = json::array();
size_t spline_index = 0;
for (const ElementRef &spline : linked_list_elements(blend, layer, "splines")) {
if (++spline_count > 100000) return std::nullopt;
const int64_t spline_flags = read_integer(*blend.sdna, spline, "flag").value_or(0);
const int64_t total_points = read_integer(*blend.sdna, spline, "tot_point").value_or(0);
const std::optional<uint64_t> points_pointer = read_pointer(*blend.sdna, spline, "points");
if (!points_pointer || total_points < 2 || total_points > 1000000 ||
point_count > 1000000 - size_t(total_points))
{
return std::nullopt;
}
point_count += size_t(total_points);
json points = json::array();
for (int64_t point_index = 0; point_index < total_points; point_index++) {
const std::optional<ElementRef> point = raw_array_element(
blend, *points_pointer, "MaskSplinePoint", size_t(point_index));
if (!point) return std::nullopt;
const std::optional<ElementRef> bezier = embedded_element(*blend.sdna, *point, "bezt");
if (!bezier) return std::nullopt;
const std::vector<float> coordinates = read_float_array(*blend.sdna, *bezier, "vec", 9);
const int64_t left_handle = read_integer(*blend.sdna, *bezier, "h1").value_or(HD_FREE);
const int64_t right_handle = read_integer(*blend.sdna, *bezier, "h2").value_or(HD_FREE);
const int64_t selection = read_integer(*blend.sdna, *bezier, "f2").value_or(0);
const float feather = read_float(*blend.sdna, *bezier, "weight").value_or(0.0f);
if (coordinates.size() < 8 || std::any_of(coordinates.begin(), coordinates.end(), [](const float value) {
return !std::isfinite(value) || value < -4.0f || value > 4.0f;
}) || !std::isfinite(feather) || feather < 0.0f || feather > 100.0f)
{
return std::nullopt;
}
const char *handle_type = ELEM(left_handle, HD_AUTO, HD_AUTO_ANIM) &&
ELEM(right_handle, HD_AUTO, HD_AUTO_ANIM) ?
"AUTO" :
left_handle == HD_VECT && right_handle == HD_VECT ? "VECTOR" :
ELEM(left_handle, HD_ALIGN, HD_ALIGN_DOUBLESIDE) &&
ELEM(right_handle, HD_ALIGN, HD_ALIGN_DOUBLESIDE) ?
"ALIGNED" :
"FREE";
points.push_back({{"id", "mask-point:" + std::to_string(layer_index) + ":" +
std::to_string(spline_index) + ":" + std::to_string(point_index)},
{"co", {coordinates[3], coordinates[4]}},
{"handleLeft", {coordinates[0], coordinates[1]}},
{"handleRight", {coordinates[6], coordinates[7]}},
{"handleType", handle_type},
{"feather", feather},
{"selected", (selection & BEZT_FLAG_SELECT) != 0}});
}
splines.push_back({{"id", "mask-spline:" + std::to_string(layer_index) + ":" +
std::to_string(spline_index)},
{"cyclic", (spline_flags & MASK_SPLINE_CYCLIC) != 0},
{"fill", (spline_flags & MASK_SPLINE_NOFILL) == 0},
{"points", std::move(points)}});
spline_index++;
}
layers.push_back({{"id", "mask-layer:" + std::to_string(layer_index)},
{"name", layer_name.empty() ? "Layer " + std::to_string(layer_index + 1) :
layer_name},
{"visible", (visibility_flags & MASK_HIDE_VIEW) == 0},
{"locked", (layer_flags & MASK_LAYERFLAG_LOCKED) != 0 ||
(visibility_flags & MASK_HIDE_SELECT) != 0},
{"opacity", opacity},
{"splines", std::move(splines)}});
layer_index++;
}
return json{{"id", mask_id}, {"name", mask_name}, {"layers", std::move(layers)}};
}
struct RawBlockSpan {
const BlendBlock *block = nullptr;
size_t offset = 0;
@@ -2361,6 +2756,8 @@ json scene_ir_from_blend(const ParsedBlend &blend,
json non_mesh_data = json::array();
json vfonts = json::array();
json libraries = json::array();
json masks = json::array();
bool masks_blocked = false;
json animations = json::array();
json nla_tracks = json::array();
json armatures = json::array();
@@ -2960,6 +3357,10 @@ json scene_ir_from_blend(const ParsedBlend &blend,
}
else if (record.type_name == "Scene") {
json scene = {{"id", record.id}, {"name", record.name}};
int scene_frame_start = frame_start;
int scene_frame_end = frame_end;
int scene_fps = 24;
float scene_fps_base = 1.0f;
if (const std::optional<uint64_t> root =
read_pointer(*blend.sdna, element, "master_collection"))
{
@@ -2983,6 +3384,10 @@ json scene_ir_from_blend(const ParsedBlend &blend,
frame_current = int(read_integer(*blend.sdna, *render, "cfra").value_or(frame_current));
frame_start = int(read_integer(*blend.sdna, *render, "sfra").value_or(frame_start));
frame_end = int(read_integer(*blend.sdna, *render, "efra").value_or(frame_end));
scene_frame_start = frame_start;
scene_frame_end = frame_end;
scene_fps = int(read_integer(*blend.sdna, *render, "frs_sec").value_or(24));
scene_fps_base = read_float(*blend.sdna, *render, "frs_sec_base").value_or(1.0f);
scene["renderEngine"] = read_string(*blend.sdna, *render, "engine");
}
if (const std::optional<ElementRef> units = embedded_element(*blend.sdna, element, "unit")) {
@@ -3015,11 +3420,40 @@ json scene_ir_from_blend(const ParsedBlend &blend,
}
scene["colorManagement"] = std::move(color_management);
}
if (const std::optional<json> compositor = compositor_graph_from_scene(
blend, element, record.id, record.name))
{
scene["compositorStatus"] = compositor->value("status", "BLOCKED");
if (compositor->contains("graph")) scene["compositorGraph"] = compositor->at("graph");
}
if (const std::optional<json> sequencer = sequencer_timeline_from_scene(blend,
element,
record.id,
revision,
scene_frame_start,
scene_frame_end,
scene_fps,
scene_fps_base,
ids_by_pointer))
{
scene["sequencerStatus"] = sequencer->value("status", "BLOCKED");
if (sequencer->contains("timeline")) scene["sequencerTimeline"] = sequencer->at("timeline");
}
if (first_scene_id.empty()) {
first_scene_id = record.id;
}
scenes.push_back(std::move(scene));
}
else if (record.type_name == "Mask") {
if (const std::optional<json> parsed_mask = mask_from_record(
blend, element, record.id, record.name))
{
masks.push_back(*parsed_mask);
}
else {
masks_blocked = true;
}
}
}
for (const BlendBlock &block : blend.blocks) {
@@ -3111,6 +3545,7 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"nonMeshData", std::move(non_mesh_data)},
{"vfonts", std::move(vfonts)},
{"libraries", std::move(libraries)},
{"trackingMaskStatus", masks_blocked ? "BLOCKED" : "AVAILABLE"},
{"animations", std::move(animations)},
{"nlaTracks", std::move(nla_tracks)},
{"armatures", std::move(armatures)},
@@ -3121,6 +3556,13 @@ json scene_ir_from_blend(const ParsedBlend &blend,
json(nullptr) :
json(first_mesh_object_id.empty() ? first_object_id : first_mesh_object_id)},
{"frame", {{"current", frame_current}, {"start", frame_start}, {"end", frame_end}}}};
if (!masks_blocked) {
snapshot["trackingMasks"] = {{"schemaVersion", 1},
{"revision", revision},
{"clips", json::array()},
{"masks", std::move(masks)},
{"bindings", json::array()}};
}
return snapshot;
}

View File

@@ -154,6 +154,14 @@ Curve *find_curve(Main *main, const char *data_id)
return nullptr;
}
Camera *find_camera(Main *main, const char *data_id)
{
if (main == nullptr || data_id == nullptr || strncmp(data_id, "camera:", 7) != 0) return nullptr;
const std::string name(data_id + 7);
for (Camera &camera : main->cameras) if (id_name(camera.id) == name) return &camera;
return nullptr;
}
VFont *find_vfont(Main *main, const char *font_id)
{
if (main == nullptr || font_id == nullptr || strncmp(font_id, "vfont:", 6) != 0) return nullptr;
@@ -3976,6 +3984,120 @@ bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
return true;
};
if (strncmp(target_id, "camera:", 7) == 0) {
Camera *camera = find_camera(state->main, target_id);
if (camera == nullptr || !ensure_single_user_data(state->main, &camera->id, error) ||
!reject_unknown({"projection", "lensMm", "sensorWidthMm", "sensorHeightMm", "sensorFit",
"shift", "near", "far", "orthoScale", "depthOfField"}) ||
!read_float("lensMm", 0.1f, 10000.0f, camera->lens) ||
!read_float("sensorWidthMm", 0.1f, 10000.0f, camera->sensor_x) ||
!read_float("sensorHeightMm", 0.1f, 10000.0f, camera->sensor_y) ||
!read_float("near", 0.0001f, 1.0e9f, camera->clip_start) ||
!read_float("far", 0.0002f, 1.0e12f, camera->clip_end) ||
!read_float("orthoScale", 0.0001f, 1.0e9f, camera->ortho_scale))
{
return false;
}
if (camera->clip_end <= camera->clip_start) {
error = "RENDER_PROPERTY_INVALID: far must exceed near";
return false;
}
if (properties.contains("projection")) {
if (!properties["projection"].is_string()) {
error = "RENDER_PROPERTY_INVALID: projection must be a string";
return false;
}
const std::string projection = properties["projection"].get<std::string>();
if (projection == "PERSPECTIVE") camera->type = CAM_PERSP;
else if (projection == "ORTHOGRAPHIC") camera->type = CAM_ORTHO;
else {
error = "RENDER_PROPERTY_INVALID: projection is unsupported";
return false;
}
}
if (properties.contains("sensorFit")) {
if (!properties["sensorFit"].is_number_integer()) {
error = "RENDER_PROPERTY_INVALID: sensorFit must be an integer";
return false;
}
const int fit = properties["sensorFit"].get<int>();
if (fit < CAMERA_SENSOR_FIT_AUTO || fit > CAMERA_SENSOR_FIT_VERT) {
error = "RENDER_PROPERTY_INVALID: sensorFit is outside the bounded enum range";
return false;
}
camera->sensor_fit = eCamera_SensorFit(fit);
}
if (properties.contains("shift")) {
const json &shift = properties["shift"];
if (!shift.is_array() || shift.size() != 2 || !shift[0].is_number() || !shift[1].is_number()) {
error = "RENDER_PROPERTY_INVALID: shift must contain two numbers";
return false;
}
const float x = shift[0].get<float>();
const float y = shift[1].get<float>();
if (!std::isfinite(x) || !std::isfinite(y) || x < -1000.0f || x > 1000.0f || y < -1000.0f || y > 1000.0f) {
error = "RENDER_PROPERTY_INVALID: shift is outside the bounded range";
return false;
}
camera->shiftx = x;
camera->shifty = y;
}
if (properties.contains("depthOfField")) {
const json &dof = properties["depthOfField"];
if (!dof.is_object() || dof.empty()) {
error = "RENDER_PROPERTY_INVALID: depthOfField must be a non-empty object";
return false;
}
for (const auto &[key, value] : dof.items()) {
(void)value;
if (!std::unordered_set<std::string>{"enabled", "focusDistance", "apertureFStop",
"apertureBlades", "apertureRotation",
"apertureRatio"}.contains(key)) {
error = "RENDER_PROPERTY_INVALID: unsupported depthOfField property " + key;
return false;
}
}
if (dof.contains("enabled")) {
if (!dof["enabled"].is_boolean()) {
error = "RENDER_PROPERTY_INVALID: depthOfField.enabled must be boolean";
return false;
}
if (dof["enabled"].get<bool>()) camera->dof.flag |= CAM_DOF_ENABLED;
else camera->dof.flag &= ~CAM_DOF_ENABLED;
}
const auto dof_float = [&](const char *key, float minimum, float maximum, float &target) {
if (!dof.contains(key)) return true;
if (!dof[key].is_number()) return false;
const float value = dof[key].get<float>();
if (!std::isfinite(value) || value < minimum || value > maximum) return false;
target = value;
return true;
};
if (!dof_float("focusDistance", 0.0f, 1.0e9f, camera->dof.focus_distance) ||
!dof_float("apertureFStop", 0.01f, 1000.0f, camera->dof.aperture_fstop) ||
!dof_float("apertureRotation", -6.28318531f, 6.28318531f, camera->dof.aperture_rotation) ||
!dof_float("apertureRatio", 0.01f, 100.0f, camera->dof.aperture_ratio)) {
error = "RENDER_PROPERTY_INVALID: depthOfField numeric value is outside the bounded range";
return false;
}
if (dof.contains("apertureBlades")) {
if (!dof["apertureBlades"].is_number_integer()) {
error = "RENDER_PROPERTY_INVALID: depthOfField.apertureBlades must be an integer";
return false;
}
const int blades = dof["apertureBlades"].get<int>();
if (blades < 0 || blades > 64) {
error = "RENDER_PROPERTY_INVALID: depthOfField.apertureBlades is outside the bounded range";
return false;
}
camera->dof.aperture_blades = blades;
}
}
camera->id.recalc |= ID_RECALC_PARAMETERS;
restore_view_settings();
return true;
}
if (strncmp(target_id, "light:", 6) == 0) {
Light *light = find_light(state->main, target_id);
if (light == nullptr || !ensure_single_user_data(state->main, &light->id, error)) return false;