Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: 2020 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
../../makesrna
)
set(INC_SYS
)
set(SRC
intern/abstract_hierarchy_iterator.cc
intern/dupli_parent_finder.cc
intern/dupli_persistent_id.cc
intern/mesh_utils.cc
intern/object_identifier.cc
intern/orientation.cc
intern/path_util.cc
intern/string_utils.cc
intern/subdiv_disabler.cc
IO_abstract_hierarchy_iterator.h
IO_dupli_persistent_id.hh
IO_mesh_utils.hh
IO_orientation.hh
IO_path_util.hh
IO_path_util_types.hh
IO_string_utils.hh
IO_subdiv_disabler.hh
IO_types.hh
IO_validate.hh
intern/dupli_parent_finder.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::extern::fast_float
)
blender_add_lib(bf_io_common "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
intern/abstract_hierarchy_iterator_test.cc
intern/object_identifier_test.cc
intern/string_utils_tests.cc
)
set(TEST_INC
../../blenloader
)
set(TEST_LIB
bf_blenloader_test_util
bf_io_common
)
blender_add_test_suite_lib(io_common "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,401 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* This file contains the AbstractHierarchyIterator. It is intended for exporters for file
* formats that concern an entire hierarchy of objects (rather than, for example, an OBJ file that
* contains only a single mesh). Examples are Universal Scene Description (USD) and Alembic.
* AbstractHierarchyIterator is intended to be subclassed to support concrete file formats.
*
* The AbstractHierarchyIterator makes a distinction between the actual object hierarchy and the
* export hierarchy. The former is the parent/child structure in Blender, which can have multiple
* parent-like objects. For example, a duplicated object can have both a duplicator and a parent,
* both determining the final transform. The export hierarchy is the hierarchy as written to the
* file, and every object has only one export-parent.
*
* Currently the AbstractHierarchyIterator does not make any decisions about *what* to export.
* Selections like "selected only" or "no hair systems" are left to concrete subclasses.
*/
#pragma once
#include "IO_dupli_persistent_id.hh"
#include "BLI_hash.hh"
#include "BLI_map.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_set.hh"
#include "DEG_depsgraph.hh"
#include <string>
namespace blender {
struct Depsgraph;
struct DupliObject;
struct ID;
struct Main;
struct Object;
struct ParticleSystem;
namespace io {
class AbstractHierarchyWriter;
class DupliParentFinder;
/* HierarchyContext structs are created by the AbstractHierarchyIterator. Each HierarchyContext
* struct contains everything necessary to export a single object to a file. */
struct HierarchyContext {
/*********** Determined during hierarchy iteration: ***************/
Object *object; /* Evaluated object. */
Object *export_parent;
Object *duplicator;
PersistentID persistent_id;
float4x4 matrix_world;
std::string export_name;
/* When weak_export=true, the object will be exported only as transform, and only if is an
* ancestor of an object with weak_export=false.
*
* In other words: when weak_export=true but this object has no children, or all descendants also
* have weak_export=true, this object (and by recursive reasoning all its descendants) will be
* excluded from the export.
*
* The export hierarchy is kept as close to the hierarchy in Blender as possible. As such, an
* object that serves as a parent for another object, but which should NOT be exported itself, is
* exported only as transform (i.e. as empty). This happens with objects that are invisible when
* exporting with "Visible Only" enabled, for example. */
bool weak_export;
/* When true, this object should check its parents for animation data when determining whether
* it's animated. This is necessary when a parent object in Blender is not part of the export. */
bool animation_check_include_parent;
/* The flag makes unambiguous the fact that the current context targets object or data. This is
* notably used in USDHierarchyIterator::create_usd_export_context: options like
* merge_parent_xform option is meaningless for object, it only makes sense for data. */
bool is_object_data_context;
/* This flag tells, within a object data context, if an object is the parent of other objects.
* This is useful when exporting UsdGeomGprim: those cannot be nested into each other. For
* example, an UsdGeomMesh cannot have other UsdGeomMesh as descendants and other hierarchy
* strategies need to be adopted.
*/
bool is_parent;
/* When true this is duplisource object. This flag is used to identify instance prototypes. */
bool is_duplisource;
/* This flag tells whether an object is a valid point instance of other objects.
* If true, it means the object has a valid reference path and its value can be included
* in the instances data of UsdGeomPointInstancer. */
bool is_point_instance;
/* This flag tells if an object is a valid prototype of a point instancer. */
bool is_point_proto;
/* True if this context is a descendant of any context with is_point_instance set to true.
* This helps skip redundant instancing data during export. */
bool has_point_instance_ancestor;
/*********** Determined during writer creation: ***************/
float4x4 parent_matrix_inv_world; /* Inverse of the parent's world matrix. */
std::string export_path; /* Hierarchical path, such as "/grandparent/parent/object_name". */
ParticleSystem *particle_system; /* Only set for particle/hair writers. */
/* Hierarchical path of the object this object is duplicating; only set when this object should
* be stored as a reference to its original. It can happen that the original is not part of the
* exported objects, in which case this string is empty even though 'duplicator' is set. */
std::string original_export_path;
/* Export path of the higher-up exported data. For transforms, this is the export path of the
* parent object. For object data, this is the export path of that object's transform.
*
* From the exported file's point of view, this is the path to the parent in that file. The term
* "parent" is not used here to avoid confusion with Blender's meaning of the word (which always
* refers to a different object). */
std::string higher_up_export_path;
/* Return a HierarchyContext representing the root of the export hierarchy. */
static const HierarchyContext *root();
/* For handling instanced collections, instances created by particles, etc. */
bool is_instance() const;
void mark_as_instance_of(const std::string &reference_export_path);
void mark_as_not_instanced();
bool is_prototype() const;
/* For handling point instancing (Instance on Points geometry node). */
bool is_point_instancer() const;
bool is_object_visible(enum eEvaluationMode evaluation_mode) const;
};
/* Abstract writer for objects. Create concrete subclasses to write to USD, Alembic, etc.
*
* Instantiated by the AbstractHierarchyIterator on the first frame an object exists. Generally
* that's the first frame to be exported, but can be later, for example when objects are
* instantiated by particles. The AbstractHierarchyWriter::write() function is called on every
* frame the object exists in the dependency graph and should be exported.
*/
class AbstractHierarchyWriter {
public:
virtual ~AbstractHierarchyWriter() = default;
virtual void write(HierarchyContext &context) = 0;
/* TODO(Sybren): add function like absent() that's called when a writer was previously created,
* but wasn't used while exporting the current frame (for example, a particle-instanced mesh of
* which the particle is no longer alive). */
protected:
/* Return true if the data written by this writer changes over time.
* Note that this function assumes this is an object data writer. Transform writers should not
* call this but implement their own logic. */
virtual bool check_is_animated(const HierarchyContext &context) const;
/* Helper functions for animation checks. */
static bool check_has_physics(const HierarchyContext &context);
static bool check_has_deforming_physics(const HierarchyContext &context);
};
/* Determines which subset of the writers actually gets to write. */
struct ExportSubset {
bool transforms : 1;
bool shapes : 1;
};
/* EnsuredWriter represents an AbstractHierarchyWriter* combined with information whether it was
* newly created or not. It's returned by AbstractHierarchyIterator::ensure_writer(). */
class EnsuredWriter {
private:
AbstractHierarchyWriter *writer_;
/* Is set to truth when ensure_writer() did not find existing writer and created a new one.
* Is set to false when writer has been re-used or when allocation of the new one has failed
* (`writer` will be `nullptr` in that case and bool(ensured_writer) will be false). */
bool newly_created_;
EnsuredWriter(AbstractHierarchyWriter *writer, bool newly_created);
public:
EnsuredWriter();
static EnsuredWriter empty();
static EnsuredWriter existing(AbstractHierarchyWriter *writer);
static EnsuredWriter newly_created(AbstractHierarchyWriter *writer);
bool is_newly_created() const;
/* These operators make an EnsuredWriter* act as an AbstractHierarchyWriter* */
operator bool() const;
AbstractHierarchyWriter *operator->();
};
/* Unique identifier for a (potentially duplicated) object.
*
* Instances of this class serve as key in the export graph of the
* AbstractHierarchyIterator. */
class ObjectIdentifier {
public:
Object *object;
Object *duplicated_by; /* nullptr for real objects. */
PersistentID persistent_id;
protected:
ObjectIdentifier(Object *object, Object *duplicated_by, const PersistentID &persistent_id);
public:
static ObjectIdentifier for_graph_root();
static ObjectIdentifier for_real_object(Object *object);
static ObjectIdentifier for_hierarchy_context(const HierarchyContext *context);
static ObjectIdentifier for_duplicated_object(const DupliObject *dupli_object,
Object *duplicated_by);
bool is_root() const;
uint64_t hash() const
{
return get_default_hash(object, duplicated_by, persistent_id);
}
};
bool operator==(const ObjectIdentifier &obj_ident_a, const ObjectIdentifier &obj_ident_b);
/* AbstractHierarchyIterator iterates over objects in a dependency graph, and constructs export
* writers. These writers are then called to perform the actual writing to a USD or Alembic file.
*
* Dealing with file- and scene-level data (for example, creating a USD scene, setting the frame
* rate, etc.) is not part of the AbstractHierarchyIterator class structure, and should be done
* in separate code.
*/
class AbstractHierarchyIterator {
public:
/* Mapping from export path to writer. */
using WriterMap = Map<std::string, AbstractHierarchyWriter *>;
/* All the children of some object, as per the export hierarchy. */
using ExportChildren = Set<HierarchyContext *>;
/* Mapping from an object and its duplicator to the object's export-children. */
using ExportGraph = Map<ObjectIdentifier, ExportChildren>;
/* Mapping from ID to its export path. This is used for instancing; given an
* instanced datablock, the export path of the original can be looked up. */
using ExportPathMap = Map<ID *, std::string>;
/* Mapping from ID name to a set of names logically residing "under" it. Used for unique
* name generation. */
using ExportUsedNameMap = Map<std::string, Set<std::string>>;
/* IDs of all duplisource objects, used to identify instance prototypes. */
using DupliSources = Set<ID *>;
protected:
ExportGraph export_graph_;
ExportPathMap duplisource_export_path_;
Main *bmain_;
Depsgraph *depsgraph_;
WriterMap writers_;
ExportSubset export_subset_;
DupliSources duplisources_;
ExportUsedNameMap used_names_;
public:
explicit AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph);
virtual ~AbstractHierarchyIterator();
/* Iterate over the depsgraph, create writers, and tell the writers to write.
* Main entry point for the AbstractHierarchyIterator, must be called for every to-be-exported
* (sub)frame. */
virtual void iterate_and_write();
/* Release all writers. Call after all frames have been exported. */
void release_writers();
/* Determine which subset of writers is used for exporting.
* Set this before calling iterate_and_write().
*
* Note that writers are created for each iterated object, regardless of this option. When a
* writer is created it will also write the current iteration, to ensure the hierarchy is
* complete. The `export_subset` option is only in effect when the writer already existed from a
* previous iteration. */
void set_export_subset(ExportSubset export_subset);
/* Convert the given name to something that is valid for the exported file format.
* This base implementation is a no-op; override in a concrete subclass. */
virtual std::string make_valid_name(const std::string &name) const;
virtual std::string make_unique_name(const std::string &original_name,
Set<std::string> &used_names);
/* Return the name of this ID datablock that is valid for the exported file format. Overriding is
* only necessary if make_valid_name(id->name+2) is not suitable for the exported file format.
* NULL-safe: when `id == nullptr` this returns an empty string. */
virtual std::string get_id_name(const ID *id) const;
/* Given a HierarchyContext of some Object *, return an export path that is valid for its
* object->data. Overriding is necessary when the exported format does NOT expect the object's
* data to be a child of the object. */
virtual std::string get_object_data_path(const HierarchyContext *context) const;
private:
void debug_print_export_graph(const ExportGraph &graph) const;
void export_graph_construct();
void connect_loose_objects();
void export_graph_prune();
void export_graph_clear();
void visit_object(Object *object, Object *export_parent, bool weak_export);
void visit_dupli_object(const DupliObject *dupli_object,
Object *duplicator,
const DupliParentFinder &dupli_parent_finder);
void context_update_for_graph_index(HierarchyContext *context,
const ObjectIdentifier &graph_index) const;
void determine_export_paths(const HierarchyContext *parent_context);
bool determine_duplication_references(const HierarchyContext *parent_context,
const std::string &indent);
/* These three functions create writers and call their write() method. */
void make_writers(const HierarchyContext *parent_context);
void make_writer_object_data(const HierarchyContext *context);
void make_writers_particle_systems(const HierarchyContext *transform_context);
/* Return the appropriate HierarchyContext for the data of the object represented by
* object_context. */
HierarchyContext context_for_object_data(const HierarchyContext *object_context) const;
/* Convenience wrappers around get_id_name(). */
std::string get_object_name(const Object *object) const;
std::string get_object_name(const Object *object, const Object *parent);
std::string get_object_data_name(const Object *object) const;
using create_writer_func =
AbstractHierarchyWriter *(AbstractHierarchyIterator::*)(const HierarchyContext *);
/* Ensure that a writer exists; if it doesn't, call create_func(context).
*
* The create_func function should be one of the create_XXXX_writer(context) functions declared
* below. */
EnsuredWriter ensure_writer(const HierarchyContext *context, create_writer_func create_func);
protected:
/* Construct a valid path for the export file format. This class concatenates by using '/' as a
* path separator, which is valid for both Alembic and USD. */
virtual std::string path_concatenate(const std::string &parent_path,
const std::string &child_path) const;
/* Return whether this object should be marked as 'weak export' or not.
*
* When this returns false, writers for the transform and data are created,
* and dupli-objects dupli-object generated from this object will be passed to
* should_visit_dupli_object().
*
* When this returns true, only a transform writer is created and marked as
* 'weak export'. In this case, the transform writer will be removed before
* exporting starts, unless a descendant of this object is to be exported.
* Dupli-object generated from this object will also be skipped.
*
* See HierarchyContext::weak_export.
*/
virtual bool mark_as_weak_export(const Object *object) const;
virtual bool should_visit_dupli_object(const DupliObject *dupli_object) const;
virtual ObjectIdentifier determine_graph_index_object(const HierarchyContext *context);
virtual ObjectIdentifier determine_graph_index_dupli(
const HierarchyContext *context,
const DupliObject *dupli_object,
const DupliParentFinder &dupli_parent_finder);
/* These functions should create an AbstractHierarchyWriter subclass instance, or return
* nullptr if the object or its data should not be exported. Returning a nullptr for
* data/hair/particle will NOT prevent the transform to be written.
*
* The returned writer is owned by the AbstractHierarchyWriter, and should be freed in
* delete_object_writer().
*
* The created AbstractHierarchyWriter instances should NOT keep a copy of the context pointer.
* The context can be stack-allocated and go out of scope. */
virtual AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) = 0;
virtual AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) = 0;
virtual AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) = 0;
virtual AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) = 0;
/* Called by release_writers() to free what the create_XXX_writer() functions allocated. */
virtual void release_writer(AbstractHierarchyWriter *writer) = 0;
/* Return true if data writers should be created for this context. */
virtual bool include_data_writers(const HierarchyContext *) const
{
return true;
}
/* Return true if children of the context should be converted to writers. */
virtual bool include_child_writers(const HierarchyContext *) const
{
return true;
}
AbstractHierarchyWriter *get_writer(const std::string &export_path) const;
ExportChildren *graph_children(const HierarchyContext *context);
};
} // namespace io
} // namespace blender

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_duplilist.hh"
#include <array>
#include <cstdint>
#include <string>
namespace blender::io {
/* Wrapper for DupliObject::persistent_id that can act as a map key. */
class PersistentID {
protected:
constexpr static int array_length_ = MAX_DUPLI_RECUR;
using PIDArray = std::array<int, array_length_>;
PIDArray persistent_id_;
explicit PersistentID(const PIDArray &persistent_id_values);
public:
PersistentID();
explicit PersistentID(const DupliObject *dupli_ob);
/* Return true if the persistent IDs are the same, ignoring the first digit. */
bool is_from_same_instancer_as(const PersistentID &other) const;
/* Construct the persistent ID of this instance's instancer. */
PersistentID instancer_pid() const;
/* Construct a string representation by reversing the persistent ID.
* In case of a duplicator that is duplicated itself as well, this
* results in strings like:
* "3" for the duplicated duplicator, and
* "3-0", "3-1", etc. for its duplis. */
std::string as_object_name_suffix() const;
uint64_t hash() const;
friend bool operator==(const PersistentID &persistent_id_a, const PersistentID &persistent_id_b);
private:
void copy_values_from(const PIDArray &persistent_id_values);
};
} // namespace blender::io

View File

@@ -0,0 +1,46 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
struct Depsgraph;
struct Mesh;
struct Object;
namespace io {
/**
* Conditionally coerce objects to mesh for export.
* For use by formats that only support meshes.
*/
struct MeshCoerceForExport {
/**
* Mesh to read geometry from, referencing evaluated, pre-modified or `owned` data.
*
* The same as `owned` when a conversion was performed.
*/
const Mesh *mesh = nullptr;
/**
* Mesh converted from a non-mesh, or null.
*/
Mesh *owned = nullptr;
~MeshCoerceForExport();
};
/**
* Return the mesh to export for `obj_eval`.
*
* The mesh may be converted on demand and stored in #MeshCoerceForExport::owned.
*
* \return The mesh to export.
*/
const Mesh *mesh_coerce_for_export_setup(MeshCoerceForExport &coerce,
Depsgraph *depsgraph,
Object *obj_eval,
bool apply_modifiers);
} // namespace io
} // namespace blender

View File

@@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
struct EnumPropertyItem;
struct Main;
struct PointerRNA;
struct Scene;
enum eIOAxis {
IO_AXIS_X = 0,
IO_AXIS_Y = 1,
IO_AXIS_Z = 2,
IO_AXIS_NEGATIVE_X = 3,
IO_AXIS_NEGATIVE_Y = 4,
IO_AXIS_NEGATIVE_Z = 5,
};
extern const EnumPropertyItem io_transform_axis[];
void io_ui_forward_axis_update(Main *main, Scene *scene, PointerRNA *ptr);
void io_ui_up_axis_update(Main *main, Scene *scene, PointerRNA *ptr);
} // namespace blender

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "IO_path_util_types.hh"
namespace blender::io {
/**
* Return a filepath relative to a destination directory, for use with
* exporters.
*
* When PATH_REFERENCE_COPY mode is used, the file path pair (source
* path, destination path) is added to the `copy_set`.
*
* Equivalent of bpy_extras.io_utils.path_reference.
*/
std::string path_reference(StringRefNull filepath,
StringRefNull base_src,
StringRefNull base_dst,
ePathReferenceMode mode,
Set<std::pair<std::string, std::string>> *copy_set = nullptr);
/** Execute copying files of path_reference. */
void path_reference_copy(const Set<std::pair<std::string, std::string>> &copy_set);
} // namespace blender::io

View File

@@ -0,0 +1,24 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
/** Method used to reference paths. Equivalent of bpy_extras.io_utils.path_reference_mode. */
enum ePathReferenceMode {
/** Use relative paths with subdirectories only. */
PATH_REFERENCE_AUTO = 0,
/** Always write absolute paths. */
PATH_REFERENCE_ABSOLUTE = 1,
/** Write relative paths where possible. */
PATH_REFERENCE_RELATIVE = 2,
/** Match absolute/relative setting with input path. */
PATH_REFERENCE_MATCH = 3,
/** Filename only. */
PATH_REFERENCE_STRIP = 4,
/** Copy the file to the destination path. */
PATH_REFERENCE_COPY = 5,
};
} // namespace blender

View File

@@ -0,0 +1,114 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
/*
* Various text parsing utilities used by importers.
*
* Many of these functions take two pointers (p, end) indicating
* which part of a string to operate on, and return a possibly
* changed new start of the string. They could be taking a StringRef
* as input and returning a new StringRef, but this is a hot path
* in CSV and OBJ parsing, and the StringRef approach does lose performance
* (mostly due to return of StringRef being two register-size values
* instead of just one pointer).
*/
namespace blender::io {
/**
* Fetches next line from an input string buffer.
*
* The returned line will not have '\n' characters at the end;
* the `buffer` is modified to contain remaining text without
* the input line.
*/
StringRef read_next_line(StringRef &buffer);
/**
* Drop leading white-space from a string part.
*/
const char *drop_whitespace(const char *p, const char *end);
/**
* Drop leading non-white-space from a string part.
*/
const char *drop_non_whitespace(const char *p, const char *end);
/**
* Parse an integer from an input string.
* The parsed result is stored in `dst`. The function skips
* leading white-space unless `skip_space=false`. If the
* number can't be parsed (invalid syntax, out of range),
* `success` value is false.
*
* Returns the start of remainder of the input string after parsing.
*/
const char *try_parse_int(
const char *p, const char *end, int fallback, bool &success, int &dst, bool skip_space = true);
/**
* Parse a float from an input string.
* The parsed result is stored in `dst`. The function skips
* leading white-space unless `skip_space=false`. If the
* number can't be parsed (invalid syntax, out of range),
* `success` value is false.
*
* Returns the start of remainder of the input string after parsing.
*/
const char *try_parse_float(const char *p,
const char *end,
int fallback,
bool &success,
float &dst,
bool skip_space = true);
/**
* Parse an integer from an input string.
* The parsed result is stored in `dst`. The function skips
* leading white-space unless `skip_space=false`. If the
* number can't be parsed (invalid syntax, out of range),
* `fallback` value is stored instead.
*
* Returns the start of remainder of the input string after parsing.
*/
const char *parse_int(
const char *p, const char *end, int fallback, int &dst, bool skip_space = true);
/**
* Parse a float from an input string.
* The parsed result is stored in `dst`. The function skips
* leading white-space unless `skip_space=false`. If the
* number can't be parsed (invalid syntax, out of range),
* `fallback` value is stored instead. If `require_trailing_space`
* is true, the character after the number has to be whitespace.
*
* Returns the start of remainder of the input string after parsing.
*/
const char *parse_float(const char *p,
const char *end,
float fallback,
float &dst,
bool skip_space = true,
bool require_trailing_space = false);
/**
* Parse a number of white-space separated floats from an input string.
* The parsed `count` numbers are stored in `dst`. If a
* number can't be parsed (invalid syntax, out of range),
* `fallback` value is stored instead.
*
* Returns the start of remainder of the input string after parsing.
*/
const char *parse_floats(const char *p,
const char *end,
float fallback,
float *dst,
int count,
bool require_trailing_space = false);
} // namespace blender::io

View File

@@ -0,0 +1,67 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_modifier_types.h"
#include "BLI_vector.hh"
namespace blender {
struct Depsgraph;
struct ModifierData;
struct Object;
struct Scene;
namespace io {
/**
* This code is shared between the Alembic and USD exporters.
* Temporarily disable the subdiv modifier on mesh objects,
* if the subdiv modifier is last on the modifier stack.
*
* The destructor restores all disabled modifiers.
*
* Currently, this class is used to disable Catmull-Clark subdivision modifiers.
* It is done in a separate step before the exporter starts iterating over all
* the frames, so that it only has to happen once per export.
*/
class SubdivModifierDisabler final {
private:
Depsgraph *depsgraph_;
/* TODO: Track the object and its disabled modifier in a single struct and use just 1 Vector. */
Vector<ModifierData *> disabled_modifiers_;
Vector<Object *> modified_objects_;
public:
explicit SubdivModifierDisabler(Depsgraph *depsgraph);
~SubdivModifierDisabler();
/**
* Disable subdiv modifiers on all mesh objects.
*/
void disable_modifiers();
/**
* Return the Catmull-Clark subdiv modifier on the mesh, if it's the last modifier
* in the list or if it's the last modifier preceding any particle system modifiers.
* This function ignores Simple subdiv modifiers.
*/
static ModifierData *get_subdiv_modifier(Scene *scene, const Object *ob, ModifierMode mode);
/* Disallow copying. */
SubdivModifierDisabler(const SubdivModifierDisabler &) = delete;
SubdivModifierDisabler &operator=(const SubdivModifierDisabler &) = delete;
private:
/**
* Disable the given modifier and add it to the disabled
* modifiers list.
*/
void disable_modifier(ModifierData *mod);
};
} // namespace io
} // namespace blender

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
/* The CacheArchiveHandle struct is only used for anonymous pointers,
* to interface between C and C++ code. This is currently used
* to hide pointers to alembic ArchiveReader and USDStageReader. */
struct CacheArchiveHandle {
int unused;
};
/* The CacheReader struct is only used for anonymous pointers,
* to interface between C and C++ code. This is currently used
* to hide pointers to AbcObjectReader and USDPrimReader
* (or subclasses thereof). */
struct CacheReader {
int unused;
};
} // namespace blender

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup io
*
* Validation utilities for importers.
*/
#include <climits>
#include <cstdint>
namespace blender::io::validate {
/** Check if size fits in an `int` as used by Blender geometry types. */
inline bool size_fits_in_int(const int64_t size)
{
return size >= 0 && size <= INT_MAX;
}
/** Check if index fits in the range. */
inline bool index_in_range(const int64_t index, const int64_t size)
{
return (index >= 0 && index < size);
}
} // namespace blender::io::validate

View File

@@ -0,0 +1,849 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_abstract_hierarchy_iterator.h"
#include "dupli_parent_finder.hh"
#include <string>
#include <fmt/core.h>
#include "BKE_anim_data.hh"
#include "BKE_duplilist.hh"
#include "BKE_geometry_set.hh"
#include "BKE_geometry_set_instances.hh"
#include "BKE_key.hh"
#include "BKE_object.hh"
#include "BKE_particle.h"
#include "BLI_assert.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_set.hh"
#include "BLI_string_utils.hh"
#include "DNA_ID.h"
#include "DNA_layer_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "DNA_particle_types.h"
#include "DNA_rigidbody_types.h"
#include "DEG_depsgraph_query.hh"
namespace blender::io {
const HierarchyContext *HierarchyContext::root()
{
return nullptr;
}
bool HierarchyContext::is_instance() const
{
return !original_export_path.empty();
}
void HierarchyContext::mark_as_instance_of(const std::string &reference_export_path)
{
original_export_path = reference_export_path;
}
void HierarchyContext::mark_as_not_instanced()
{
original_export_path.clear();
}
bool HierarchyContext::is_prototype() const
{
/* The context is for a prototype if it's for a duplisource or
* for a duplicated object that was designated to be a prototype
* because the original was not included in the export. */
return is_duplisource || (duplicator != nullptr && !is_instance());
}
bool HierarchyContext::is_object_visible(const enum eEvaluationMode evaluation_mode) const
{
const bool is_dupli = duplicator != nullptr;
int base_flag;
if (is_dupli) {
/* Construct the object's base flags from its dupli-parent, just like is done in
* deg_objects_dupli_iterator_next(). Without this, the visibility check below will fail. Doing
* this here, instead of a more suitable location in AbstractHierarchyIterator, prevents
* copying the Object for every dupli. */
base_flag = object->base_flag;
object->base_flag = duplicator->base_flag | BASE_FROM_DUPLI;
}
const int visibility = BKE_object_visibility(object, evaluation_mode);
if (is_dupli) {
object->base_flag = base_flag;
}
return (visibility & OB_VISIBLE_SELF) != 0;
}
EnsuredWriter::EnsuredWriter() : writer_(nullptr), newly_created_(false) {}
EnsuredWriter::EnsuredWriter(AbstractHierarchyWriter *writer, bool newly_created)
: writer_(writer), newly_created_(newly_created)
{
}
EnsuredWriter EnsuredWriter::empty()
{
return EnsuredWriter(nullptr, false);
}
EnsuredWriter EnsuredWriter::existing(AbstractHierarchyWriter *writer)
{
return EnsuredWriter(writer, false);
}
EnsuredWriter EnsuredWriter::newly_created(AbstractHierarchyWriter *writer)
{
return EnsuredWriter(writer, true);
}
bool EnsuredWriter::is_newly_created() const
{
return newly_created_;
}
EnsuredWriter::operator bool() const
{
return writer_ != nullptr;
}
AbstractHierarchyWriter *EnsuredWriter::operator->()
{
return writer_;
}
bool AbstractHierarchyWriter::check_is_animated(const HierarchyContext &context) const
{
Object *object = context.object;
if (BKE_animdata_id_is_animated(object->data)) {
return true;
}
if (BKE_key_from_object(object) != nullptr) {
return true;
}
if (check_has_deforming_physics(context)) {
return true;
}
/* Test modifiers. */
/* TODO(Sybren): replace this with a check on the depsgraph to properly check for dependency on
* time. */
ModifierData *md = static_cast<ModifierData *>(object->modifiers.first);
while (md) {
if (md->type != eModifierType_Subsurf) {
return true;
}
md = md->next;
}
return false;
}
bool AbstractHierarchyWriter::check_has_physics(const HierarchyContext &context)
{
const RigidBodyOb *rbo = context.object->rigidbody_object;
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE;
}
bool AbstractHierarchyWriter::check_has_deforming_physics(const HierarchyContext &context)
{
const RigidBodyOb *rbo = context.object->rigidbody_object;
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE && (rbo->flag & RBO_FLAG_USE_DEFORM) != 0;
}
bool HierarchyContext::is_point_instancer() const
{
if (!object) {
return false;
}
/* Collection instancers are handled elsewhere as part of Scene instancing. */
if (object->type == OB_EMPTY && object->instance_collection != nullptr) {
return false;
}
const bke::GeometrySet geometry_set = bke::object_get_evaluated_geometry_set(*object);
return geometry_set.has_instances();
}
AbstractHierarchyIterator::AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
: bmain_(bmain), depsgraph_(depsgraph), export_subset_({true, true})
{
}
AbstractHierarchyIterator::~AbstractHierarchyIterator()
{
/* release_writers() cannot be called here directly, as it calls into the pure-virtual
* release_writer() function. By the time this destructor is called, the subclass that implements
* that pure-virtual function is already destructed. */
BLI_assert_msg(
writers_.is_empty(),
"release_writers() should be called before the AbstractHierarchyIterator goes out of scope");
}
void AbstractHierarchyIterator::iterate_and_write()
{
export_graph_construct();
connect_loose_objects();
export_graph_prune();
determine_export_paths(HierarchyContext::root());
determine_duplication_references(HierarchyContext::root(), "");
make_writers(HierarchyContext::root());
export_graph_clear();
}
void AbstractHierarchyIterator::release_writers()
{
for (AbstractHierarchyWriter *writer : writers_.values()) {
release_writer(writer);
}
writers_.clear();
}
void AbstractHierarchyIterator::set_export_subset(ExportSubset export_subset)
{
export_subset_ = export_subset;
}
std::string AbstractHierarchyIterator::make_valid_name(const std::string &name) const
{
return name;
}
std::string AbstractHierarchyIterator::get_id_name(const ID *id) const
{
if (id == nullptr) {
return "";
}
return make_valid_name(std::string(id->name + 2));
}
std::string AbstractHierarchyIterator::make_unique_name(const std::string &original_name,
Set<std::string> &used_names)
{
if (original_name.empty()) {
return "";
}
std::string name = BLI_uniquename_cb(
[&](const StringRef check_name) { return used_names.contains_as(check_name); },
'_',
make_valid_name(original_name));
used_names.add_new(name);
return name;
}
std::string AbstractHierarchyIterator::get_object_data_path(const HierarchyContext *context) const
{
BLI_assert(!context->export_path.empty());
BLI_assert(context->object->data);
return path_concatenate(context->export_path, get_object_data_name(context->object));
}
void AbstractHierarchyIterator::debug_print_export_graph(const ExportGraph &graph) const
{
size_t total_graph_size = 0;
for (const auto item : graph.items()) {
const ObjectIdentifier &parent_info = item.key;
const Object *const export_parent = parent_info.object;
const Object *const duplicator = parent_info.duplicated_by;
if (duplicator != nullptr) {
fmt::println(" DU {} (as dupped by {}):",
export_parent == nullptr ? "-null-" : (export_parent->id.name + 2),
duplicator->id.name + 2);
}
else {
fmt::println(" OB {}:",
export_parent == nullptr ? "-null-" : (export_parent->id.name + 2));
}
total_graph_size += item.value.size();
for (HierarchyContext *child_ctx : item.value) {
if (child_ctx->duplicator == nullptr) {
fmt::println(" - {}{}{}",
child_ctx->export_name.c_str(),
child_ctx->weak_export ? " (weak)" : "",
child_ctx->original_export_path.empty() ?
"" :
(std::string("ref ") + child_ctx->original_export_path).c_str());
}
else {
fmt::println(" - {} (dup by {}{}) {}",
child_ctx->export_name.c_str(),
child_ctx->duplicator->id.name + 2,
child_ctx->weak_export ? ", weak" : "",
child_ctx->original_export_path.empty() ?
"" :
(std::string("ref ") + child_ctx->original_export_path).c_str());
}
}
}
fmt::println(" (Total graph size: {} objects)", total_graph_size);
}
void AbstractHierarchyIterator::export_graph_construct()
{
/* Add a "null" root node with no children immediately for the case where the top-most node in
* the scene is not being exported and a root node otherwise wouldn't get added. */
ObjectIdentifier root_node_id = ObjectIdentifier::for_real_object(nullptr);
export_graph_.add_new(root_node_id, {});
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph_;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET;
DupliList duplilist;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
/* Non-instanced objects always have their object-parent as export-parent. */
const bool weak_export = mark_as_weak_export(object);
visit_object(object, object->parent, weak_export);
if (weak_export) {
/* If a duplicator shouldn't be exported, its duplilist also shouldn't be. */
continue;
}
/* Export the duplicated objects instanced by this object. */
object_duplilist(depsgraph_, object, nullptr, duplilist);
if (!duplilist.is_empty()) {
DupliParentFinder dupli_parent_finder;
for (const DupliObject &dupli_object : duplilist) {
if (!should_visit_dupli_object(&dupli_object)) {
continue;
}
dupli_parent_finder.insert(&dupli_object);
}
for (const DupliObject &dupli_object : duplilist) {
if (!should_visit_dupli_object(&dupli_object)) {
continue;
}
visit_dupli_object(&dupli_object, object, dupli_parent_finder);
}
}
duplilist.clear();
}
DEG_OBJECT_ITER_END;
}
void AbstractHierarchyIterator::connect_loose_objects()
{
/* Find those objects whose parent is not part of the export graph; these
* objects would be skipped when traversing the graph as a hierarchy.
* These objects will have to be re-attached to some parent object in order to
* fit into the hierarchy. */
ExportGraph loose_objects_graph = export_graph_;
for (const ExportChildren &children : export_graph_.values()) {
for (const HierarchyContext *child : children) {
/* An object that is marked as a child of another object is not considered 'loose'. */
ObjectIdentifier child_oid = ObjectIdentifier::for_hierarchy_context(child);
loose_objects_graph.remove(child_oid);
}
}
/* The root of the hierarchy is always found, so it's never considered 'loose'. */
loose_objects_graph.remove_contained(ObjectIdentifier::for_graph_root());
/* Iterate over the loose objects and connect them to their export parent. */
for (const ObjectIdentifier &graph_key : loose_objects_graph.keys()) {
Object *object = graph_key.object;
while (true) {
/* Loose objects will all be real objects, as duplicated objects always have
* their duplicator or other exported duplicated object as ancestor. */
const bool found = export_graph_.contains(ObjectIdentifier::for_real_object(object->parent));
visit_object(object, object->parent, true);
if (found) {
break;
}
/* 'object->parent' will never be nullptr here, as the export graph contains the
* root as nullptr and thus will cause a break above. */
BLI_assert(object->parent != nullptr);
object = object->parent;
}
}
}
static bool remove_weak_subtrees(const HierarchyContext *context,
AbstractHierarchyIterator::ExportGraph &clean_graph,
const AbstractHierarchyIterator::ExportGraph &input_graph)
{
bool all_is_weak = context != nullptr && context->weak_export;
const ObjectIdentifier map_key = ObjectIdentifier::for_hierarchy_context(context);
const AbstractHierarchyIterator::ExportChildren *children = input_graph.lookup_ptr(map_key);
if (children) {
for (HierarchyContext *child_context : *children) {
bool child_tree_is_weak = remove_weak_subtrees(child_context, clean_graph, input_graph);
all_is_weak &= child_tree_is_weak;
if (child_tree_is_weak) {
/* This subtree is all weak, so we can remove it from the current object's children. */
clean_graph.lookup(map_key).remove(child_context);
delete child_context;
}
}
}
if (all_is_weak) {
/* This node and all its children are weak, so it can be removed from the export graph. */
clean_graph.remove(map_key);
}
return all_is_weak;
}
void AbstractHierarchyIterator::export_graph_prune()
{
/* Take a copy of the map so that we can modify while recusing. */
ExportGraph unpruned_export_graph = export_graph_;
remove_weak_subtrees(HierarchyContext::root(), export_graph_, unpruned_export_graph);
}
void AbstractHierarchyIterator::export_graph_clear()
{
for (const ExportChildren &children : export_graph_.values()) {
for (HierarchyContext *context : children) {
delete context;
}
}
export_graph_.clear();
used_names_.clear_and_keep_capacity();
}
void AbstractHierarchyIterator::visit_object(Object *object,
Object *export_parent,
bool weak_export)
{
HierarchyContext *context = new HierarchyContext();
context->object = object;
context->is_object_data_context = false;
context->export_name = get_object_name(object, export_parent);
context->export_parent = export_parent;
context->duplicator = nullptr;
context->weak_export = weak_export;
context->animation_check_include_parent = false;
context->export_path = "";
context->original_export_path = "";
context->higher_up_export_path = "";
context->is_duplisource = false;
context->matrix_world = object->object_to_world();
ObjectIdentifier graph_index = determine_graph_index_object(context);
context_update_for_graph_index(context, graph_index);
/* Store this HierarchyContext as child of the export parent. */
export_graph_.lookup_or_add(graph_index, {}).add_new(context);
/* Create an empty entry for this object to indicate it is part of the export. This will be used
* by connect_loose_objects(). Having such an "indicator" will make it possible to do an O(log n)
* check on whether an object is part of the export, rather than having to check all objects in
* the map. Note that it's not possible to simply search for (object->parent, nullptr), as the
* object's parent in Blender may not be the same as its export-parent. */
ObjectIdentifier object_key = ObjectIdentifier::for_real_object(object);
export_graph_.add(object_key, {});
}
ObjectIdentifier AbstractHierarchyIterator::determine_graph_index_object(
const HierarchyContext *context)
{
return ObjectIdentifier::for_real_object(context->export_parent);
}
void AbstractHierarchyIterator::visit_dupli_object(const DupliObject *dupli_object,
Object *duplicator,
const DupliParentFinder &dupli_parent_finder)
{
HierarchyContext *context = new HierarchyContext();
context->object = dupli_object->ob;
context->is_object_data_context = false;
context->duplicator = duplicator;
context->persistent_id = PersistentID(dupli_object);
context->weak_export = false;
context->export_path = "";
context->original_export_path = "";
context->animation_check_include_parent = false;
context->is_duplisource = false;
context->matrix_world = float4x4(dupli_object->mat);
/* Construct export name for the dupli-instance. */
std::string export_name = get_object_name(context->object) + "-" +
context->persistent_id.as_object_name_suffix();
Set<std::string> &used_names = used_names_.lookup_or_add(duplicator->id.name, {});
context->export_name = make_unique_name(make_valid_name(export_name), used_names);
ObjectIdentifier graph_index = determine_graph_index_dupli(
context, dupli_object, dupli_parent_finder);
context_update_for_graph_index(context, graph_index);
export_graph_.lookup_or_add(graph_index, {}).add_new(context);
if (dupli_object->ob) {
this->duplisources_.add(&dupli_object->ob->id);
}
}
ObjectIdentifier AbstractHierarchyIterator::determine_graph_index_dupli(
const HierarchyContext *context,
const DupliObject *dupli_object,
const DupliParentFinder &dupli_parent_finder)
{
const DupliObject *dupli_parent = dupli_parent_finder.find_suitable_export_parent(dupli_object);
if (dupli_parent != nullptr) {
return ObjectIdentifier::for_duplicated_object(dupli_parent, context->duplicator);
}
return ObjectIdentifier::for_real_object(context->duplicator);
}
void AbstractHierarchyIterator::context_update_for_graph_index(
HierarchyContext *context, const ObjectIdentifier &graph_index) const
{
/* Update the HierarchyContext so that it is consistent with the graph index. */
context->export_parent = graph_index.object;
/* If the parent type is such that it cannot be exported (at least not currently to USD or
* Alembic), always check the parent for animation. */
const eObject_Partype partype = context->object->partype & PARTYPE;
context->animation_check_include_parent |= ELEM(partype, PARBONE, PARVERT1, PARVERT3, PARSKEL);
if (context->export_parent != context->object->parent) {
/* The parent object in Blender is NOT used as the export parent. This means
* that the world transform of this object can be influenced by objects that
* are not part of its export graph. */
context->animation_check_include_parent = true;
}
}
AbstractHierarchyIterator::ExportChildren *AbstractHierarchyIterator::graph_children(
const HierarchyContext *context)
{
/* Note: `graph_children` is called during recursive iteration and MUST NOT change the export
* graph, which would invalidate the iteration. As a result, we cannot add an entry in the
* graph if the incoming `context` is not found. */
return export_graph_.lookup_ptr(ObjectIdentifier::for_hierarchy_context(context));
}
void AbstractHierarchyIterator::determine_export_paths(const HierarchyContext *parent_context)
{
const std::string &parent_export_path = parent_context ? parent_context->export_path : "";
const ExportChildren *children = graph_children(parent_context);
if (!children) {
return;
}
for (HierarchyContext *context : *children) {
context->export_path = path_concatenate(parent_export_path, context->export_name);
if (context->duplicator == nullptr) {
/* This is an original (i.e. non-instanced) object, so we should keep track of where it was
* exported to, just in case it gets instanced somewhere. */
ID *source_ob = &context->object->id;
duplisource_export_path_.add(source_ob, context->export_path);
if (context->object->data != nullptr) {
ID *source_data = context->object->data;
duplisource_export_path_.add(source_data, get_object_data_path(context));
}
}
determine_export_paths(context);
}
}
bool AbstractHierarchyIterator::determine_duplication_references(
const HierarchyContext *parent_context, const std::string &indent)
{
const ExportChildren *children = graph_children(parent_context);
if (!children) {
return false;
}
/* Will be set to true if any child contexts are instances that were designated
* as proxies for the original prototype. */
bool contains_proxy_prototype = false;
for (HierarchyContext *context : *children) {
if (context->duplicator != nullptr) {
ID *source_id = &context->object->id;
const std::string *source_path = duplisource_export_path_.lookup_ptr(source_id);
if (!source_path) {
/* The original was not found, so mark this instance as "the original". */
context->mark_as_not_instanced();
duplisource_export_path_.add_new(source_id, context->export_path);
contains_proxy_prototype = true;
}
else {
context->mark_as_instance_of(*source_path);
}
if (context->object->data) {
ID *source_data_id = context->object->data;
if (!duplisource_export_path_.contains(source_data_id)) {
/* The original was not found, so mark this instance as "original". */
std::string data_path = get_object_data_path(context);
context->mark_as_not_instanced();
duplisource_export_path_.add_overwrite(source_id, context->export_path);
duplisource_export_path_.add_new(source_data_id, data_path);
}
}
}
else {
/* Determine is this context is for an instance prototype. */
ID *id = &context->object->id;
if (duplisources_.contains(id)) {
context->is_duplisource = true;
}
}
if (determine_duplication_references(context, indent + " ")) {
/* A descendant was designated a prototype proxy. If the current context
* is an instance, we must change it to a prototype proxy as well. */
if (context->is_instance()) {
context->mark_as_not_instanced();
ID *source_id = &context->object->id;
duplisource_export_path_.add_overwrite(source_id, context->export_path);
}
contains_proxy_prototype = true;
}
}
return contains_proxy_prototype;
}
void AbstractHierarchyIterator::make_writers(const HierarchyContext *parent_context)
{
float4x4 parent_matrix_inv_world;
if (parent_context) {
parent_matrix_inv_world = math::invert(parent_context->matrix_world);
}
else {
parent_matrix_inv_world = float4x4::identity();
}
const ExportChildren *children = graph_children(parent_context);
if (!children) {
return;
}
bool has_point_instance_ancestor = false;
if (parent_context &&
(parent_context->is_point_instance || parent_context->has_point_instance_ancestor))
{
has_point_instance_ancestor = true;
}
for (HierarchyContext *context : *children) {
context->has_point_instance_ancestor = has_point_instance_ancestor;
/* Update the context so that it is correct for this parent-child relation. */
context->parent_matrix_inv_world = parent_matrix_inv_world;
if (parent_context != nullptr) {
context->higher_up_export_path = parent_context->export_path;
}
/* Get or create the transform writer. */
EnsuredWriter transform_writer = ensure_writer(
context, &AbstractHierarchyIterator::create_transform_writer);
if (!transform_writer) {
/* Unable to export, so there is nothing to attach any children to; just abort this entire
* branch of the export hierarchy. */
continue;
}
const bool need_writers = context->is_point_proto || (!context->is_point_instance &&
!context->has_point_instance_ancestor);
BLI_assert(DEG_is_evaluated_id(&context->object->id));
if ((transform_writer.is_newly_created() || export_subset_.transforms) && need_writers) {
/* XXX This can lead to too many XForms being written. For example, a camera writer can
* refuse to write an orthographic camera. By the time that this is known, the XForm has
* already been written. */
transform_writer->write(*context);
}
if (!context->weak_export && include_data_writers(context) && need_writers) {
make_writers_particle_systems(context);
make_writer_object_data(context);
}
if (include_child_writers(context)) {
/* Recurse into this object's children. */
make_writers(context);
}
}
/* TODO(Sybren): iterate over all unused writers and call unused_during_iteration() or something.
*/
}
HierarchyContext AbstractHierarchyIterator::context_for_object_data(
const HierarchyContext *object_context) const
{
HierarchyContext data_context = *object_context;
data_context.is_object_data_context = true;
data_context.higher_up_export_path = object_context->export_path;
data_context.export_name = get_object_data_name(data_context.object);
data_context.export_path = path_concatenate(data_context.higher_up_export_path,
data_context.export_name);
const ObjectIdentifier object_key = ObjectIdentifier::for_hierarchy_context(&data_context);
const ExportChildren *children = export_graph_.lookup_ptr(object_key);
data_context.is_parent = children ? (children->size() > 0) : false;
return data_context;
}
void AbstractHierarchyIterator::make_writer_object_data(const HierarchyContext *context)
{
if (context->object->data == nullptr) {
return;
}
HierarchyContext data_context = context_for_object_data(context);
if (data_context.is_instance()) {
ID *object_data = context->object->data;
data_context.original_export_path = duplisource_export_path_.lookup(object_data);
/* If the object is marked as an instance, so should the object data. */
BLI_assert(data_context.is_instance());
}
/* Always write upon creation, otherwise depend on which subset is active. */
EnsuredWriter data_writer = ensure_writer(&data_context,
&AbstractHierarchyIterator::create_data_writer);
if (!data_writer) {
return;
}
if (data_writer.is_newly_created() || export_subset_.shapes) {
data_writer->write(data_context);
}
}
void AbstractHierarchyIterator::make_writers_particle_systems(
const HierarchyContext *transform_context)
{
Object *object = transform_context->object;
ParticleSystem *psys = static_cast<ParticleSystem *>(object->particlesystem.first);
for (; psys; psys = psys->next) {
if (!psys_check_enabled(object, psys, true)) {
continue;
}
HierarchyContext hair_context = *transform_context;
hair_context.export_name = make_valid_name(psys->name);
hair_context.export_path = path_concatenate(transform_context->export_path,
hair_context.export_name);
hair_context.higher_up_export_path = transform_context->export_path;
hair_context.particle_system = psys;
EnsuredWriter writer;
switch (psys->part->type) {
case PART_HAIR:
writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_hair_writer);
break;
case PART_EMITTER:
case PART_FLUID_FLIP:
case PART_FLUID_SPRAY:
case PART_FLUID_BUBBLE:
case PART_FLUID_FOAM:
case PART_FLUID_TRACER:
case PART_FLUID_SPRAYFOAM:
case PART_FLUID_SPRAYBUBBLE:
case PART_FLUID_FOAMBUBBLE:
case PART_FLUID_SPRAYFOAMBUBBLE:
writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_particle_writer);
break;
default:
break;
}
if (!writer) {
continue;
}
/* Always write upon creation, otherwise depend on which subset is active. */
if (writer.is_newly_created() || export_subset_.shapes) {
writer->write(hair_context);
}
}
}
std::string AbstractHierarchyIterator::get_object_name(const Object *object) const
{
return get_id_name(&object->id);
}
std::string AbstractHierarchyIterator::get_object_name(const Object *object, const Object *parent)
{
Set<std::string> &used_names = used_names_.lookup_or_add(parent ? parent->id.name : "", {});
return make_unique_name(object->id.name + 2, used_names);
}
std::string AbstractHierarchyIterator::get_object_data_name(const Object *object) const
{
const ID *object_data = object->data;
return get_id_name(object_data);
}
AbstractHierarchyWriter *AbstractHierarchyIterator::get_writer(
const std::string &export_path) const
{
return writers_.lookup_default(export_path, nullptr);
}
EnsuredWriter AbstractHierarchyIterator::ensure_writer(
const HierarchyContext *context, AbstractHierarchyIterator::create_writer_func create_func)
{
AbstractHierarchyWriter *writer = get_writer(context->export_path);
if (writer != nullptr) {
return EnsuredWriter::existing(writer);
}
writer = (this->*create_func)(context);
if (writer == nullptr) {
return EnsuredWriter::empty();
}
writers_.add_new(context->export_path, writer);
return EnsuredWriter::newly_created(writer);
}
std::string AbstractHierarchyIterator::path_concatenate(const std::string &parent_path,
const std::string &child_path) const
{
return parent_path + "/" + child_path;
}
bool AbstractHierarchyIterator::mark_as_weak_export(const Object * /*object*/) const
{
return false;
}
bool AbstractHierarchyIterator::should_visit_dupli_object(const DupliObject *dupli_object) const
{
/* Do not visit dupli objects if their `no_draw` flag is set (things like custom bone shapes) or
* if they are meta-balls / text objects / NURBS surfaces. */
if (dupli_object->no_draw || ELEM(dupli_object->ob->type, OB_MBALL, OB_FONT, OB_SURF)) {
return false;
}
return true;
}
} // namespace blender::io

View File

@@ -0,0 +1,348 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_abstract_hierarchy_iterator.h"
#include "tests/blendfile_loading_base_test.h"
#include "BKE_scene.hh"
#include "BLI_map.hh"
#include "BLI_path_utils.hh"
#include "BLI_set.hh"
#include "BLO_readfile.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DNA_object_types.h"
namespace blender::io {
namespace {
/* Mapping from ID.name to set of export hierarchy path. Duplicated objects can be exported
* multiple times with different export paths, hence the set. */
using used_writers = Map<std::string, Set<std::string>>;
class TestHierarchyWriter : public AbstractHierarchyWriter {
public:
std::string writer_type;
used_writers &writers_map;
TestHierarchyWriter(const std::string &writer_type, used_writers &writers_map)
: writer_type(writer_type), writers_map(writers_map)
{
}
void write(HierarchyContext &context) override
{
const char *id_name = context.object->id.name;
Set<std::string> &writers = writers_map.lookup_or_add(id_name, {});
if (writers.contains(context.export_path)) {
ADD_FAILURE() << "Unexpectedly found another " << writer_type << " writer for " << id_name
<< " to export to " << context.export_path;
}
writers.add_new(context.export_path);
}
};
} // namespace
class TestingHierarchyIterator : public AbstractHierarchyIterator {
public: /* Public so that the test cases can directly inspect the created writers. */
used_writers transform_writers;
used_writers data_writers;
used_writers hair_writers;
used_writers particle_writers;
explicit TestingHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
: AbstractHierarchyIterator(bmain, depsgraph)
{
}
~TestingHierarchyIterator() override
{
release_writers();
}
protected:
AbstractHierarchyWriter *create_transform_writer(const HierarchyContext * /*context*/) override
{
return new TestHierarchyWriter("transform", transform_writers);
}
AbstractHierarchyWriter *create_data_writer(const HierarchyContext * /*context*/) override
{
return new TestHierarchyWriter("data", data_writers);
}
AbstractHierarchyWriter *create_hair_writer(const HierarchyContext * /*context*/) override
{
return new TestHierarchyWriter("hair", hair_writers);
}
AbstractHierarchyWriter *create_particle_writer(const HierarchyContext * /*context*/) override
{
return new TestHierarchyWriter("particle", particle_writers);
}
void release_writer(AbstractHierarchyWriter *writer) override
{
delete writer;
}
};
class AbstractHierarchyIteratorTest : public BlendfileLoadingBaseTest {
protected:
TestingHierarchyIterator *iterator;
void SetUp() override
{
BlendfileLoadingBaseTest::SetUp();
iterator = nullptr;
}
void TearDown() override
{
iterator_free();
BlendfileLoadingBaseTest::TearDown();
}
/* Create a test iterator. */
void iterator_create()
{
iterator = new TestingHierarchyIterator(bfile->main, depsgraph);
}
/* Free the test iterator if it is not nullptr. */
void iterator_free()
{
if (iterator == nullptr) {
return;
}
delete iterator;
iterator = nullptr;
}
};
TEST_F(AbstractHierarchyIteratorTest, ExportHierarchyTest)
{
/* Load the test blend file. */
if (!blendfile_load("usd" SEP_STR "usd_hierarchy_export_test.blend")) {
return;
}
depsgraph_create(DAG_EVAL_RENDER);
iterator_create();
iterator->iterate_and_write();
/* Mapping from object name to set of export paths. */
used_writers expected_transforms = {
{"OBCamera", {"/Camera"}},
{"OBDupli1", {"/Dupli1"}},
{"OBDupli2", {"/ParentOfDupli2/Dupli2"}},
{"OBGEO_Ear_L",
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1"}},
{"OBGEO_Ear_R",
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2"}},
{"OBGEO_Head",
{"/Dupli1/GEO_Head-0",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head",
"/ParentOfDupli2/Dupli2/GEO_Head-0"}},
{"OBGEO_Nose",
{"/Dupli1/GEO_Head-0/GEO_Nose-3",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3"}},
{"OBGround plane", {"/Ground plane"}},
{"OBOutsideDupliGrandParent", {"/Ground plane/OutsideDupliGrandParent"}},
{"OBOutsideDupliParent", {"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent"}},
{"OBParentOfDupli2", {"/ParentOfDupli2"}}};
EXPECT_EQ(expected_transforms, iterator->transform_writers);
used_writers expected_data = {
{"OBCamera", {"/Camera/Camera"}},
{"OBGEO_Ear_L",
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1/Ear",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1/Ear"}},
{"OBGEO_Ear_R",
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2/Ear",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2/Ear"}},
{"OBGEO_Head",
{"/Dupli1/GEO_Head-0/Face",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face",
"/ParentOfDupli2/Dupli2/GEO_Head-0/Face"}},
{"OBGEO_Nose",
{"/Dupli1/GEO_Head-0/GEO_Nose-3/Nose",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3/Nose"}},
{"OBGround plane", {"/Ground plane/Plane"}},
{"OBParentOfDupli2", {"/ParentOfDupli2/Icosphere"}},
};
EXPECT_EQ(expected_data, iterator->data_writers);
/* The scene has no hair or particle systems. */
EXPECT_EQ(0, iterator->hair_writers.size());
EXPECT_EQ(0, iterator->particle_writers.size());
/* On the second iteration, everything should be written as well.
* This tests the default value of iterator->export_subset_. */
iterator->transform_writers.clear();
iterator->data_writers.clear();
iterator->iterate_and_write();
EXPECT_EQ(expected_transforms, iterator->transform_writers);
EXPECT_EQ(expected_data, iterator->data_writers);
}
TEST_F(AbstractHierarchyIteratorTest, ExportSubsetTest)
{
/* The scene has no hair or particle systems, and this is already covered by ExportHierarchyTest,
* so not included here. Update this test when hair & particle systems are included. */
/* Load the test blend file. */
if (!blendfile_load("usd" SEP_STR "usd_hierarchy_export_test.blend")) {
return;
}
depsgraph_create(DAG_EVAL_RENDER);
iterator_create();
/* Mapping from object name to set of export paths. */
used_writers expected_transforms = {
{"OBCamera", {"/Camera"}},
{"OBDupli1", {"/Dupli1"}},
{"OBDupli2", {"/ParentOfDupli2/Dupli2"}},
{"OBGEO_Ear_L",
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1"}},
{"OBGEO_Ear_R",
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2"}},
{"OBGEO_Head",
{"/Dupli1/GEO_Head-0",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head",
"/ParentOfDupli2/Dupli2/GEO_Head-0"}},
{"OBGEO_Nose",
{"/Dupli1/GEO_Head-0/GEO_Nose-3",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3"}},
{"OBGround plane", {"/Ground plane"}},
{"OBOutsideDupliGrandParent", {"/Ground plane/OutsideDupliGrandParent"}},
{"OBOutsideDupliParent", {"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent"}},
{"OBParentOfDupli2", {"/ParentOfDupli2"}}};
used_writers expected_data = {
{"OBCamera", {"/Camera/Camera"}},
{"OBGEO_Ear_L",
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1/Ear",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1/Ear"}},
{"OBGEO_Ear_R",
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2/Ear",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2/Ear"}},
{"OBGEO_Head",
{"/Dupli1/GEO_Head-0/Face",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face",
"/ParentOfDupli2/Dupli2/GEO_Head-0/Face"}},
{"OBGEO_Nose",
{"/Dupli1/GEO_Head-0/GEO_Nose-3/Nose",
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose",
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3/Nose"}},
{"OBGround plane", {"/Ground plane/Plane"}},
{"OBParentOfDupli2", {"/ParentOfDupli2/Icosphere"}},
};
/* Even when only asking an export of transforms, on the first frame everything should be
* exported. */
{
ExportSubset export_subset = {false};
export_subset.transforms = true;
export_subset.shapes = false;
iterator->set_export_subset(export_subset);
}
iterator->iterate_and_write();
EXPECT_EQ(expected_transforms, iterator->transform_writers);
EXPECT_EQ(expected_data, iterator->data_writers);
/* Clear data to prepare for the next iteration. */
iterator->transform_writers.clear();
iterator->data_writers.clear();
/* Second iteration, should only write transforms now. */
iterator->iterate_and_write();
EXPECT_EQ(expected_transforms, iterator->transform_writers);
EXPECT_EQ(0, iterator->data_writers.size());
/* Clear data to prepare for the next iteration. */
iterator->transform_writers.clear();
iterator->data_writers.clear();
/* Third iteration, should only write data now. */
{
ExportSubset export_subset = {false};
export_subset.transforms = false;
export_subset.shapes = true;
iterator->set_export_subset(export_subset);
}
iterator->iterate_and_write();
EXPECT_EQ(0, iterator->transform_writers.size());
EXPECT_EQ(expected_data, iterator->data_writers);
/* Clear data to prepare for the next iteration. */
iterator->transform_writers.clear();
iterator->data_writers.clear();
/* Fourth iteration, should export everything now. */
{
ExportSubset export_subset = {false};
export_subset.transforms = true;
export_subset.shapes = true;
iterator->set_export_subset(export_subset);
}
iterator->iterate_and_write();
EXPECT_EQ(expected_transforms, iterator->transform_writers);
EXPECT_EQ(expected_data, iterator->data_writers);
}
/* Test class that constructs a depsgraph in such a way that it includes invisible objects. */
class AbstractHierarchyIteratorInvisibleTest : public AbstractHierarchyIteratorTest {
protected:
void depsgraph_create(eEvaluationMode depsgraph_evaluation_mode) override
{
depsgraph = DEG_graph_new(
bfile->main, bfile->curscene, bfile->cur_view_layer, depsgraph_evaluation_mode);
DEG_graph_build_for_all_objects(depsgraph);
BKE_scene_graph_update_tagged(depsgraph, bfile->main);
}
};
TEST_F(AbstractHierarchyIteratorInvisibleTest, ExportInvisibleTest)
{
if (!blendfile_load("alembic" SEP_STR "visibility.blend")) {
return;
}
depsgraph_create(DAG_EVAL_RENDER);
iterator_create();
iterator->iterate_and_write();
/* Mapping from object name to set of export paths. */
used_writers expected_transforms = {{"OBInvisibleAnimatedCube", {"/InvisibleAnimatedCube"}},
{"OBInvisibleCube", {"/InvisibleCube"}},
{"OBVisibleCube", {"/VisibleCube"}}};
EXPECT_EQ(expected_transforms, iterator->transform_writers);
used_writers expected_data = {{"OBInvisibleAnimatedCube", {"/InvisibleAnimatedCube/Cube"}},
{"OBInvisibleCube", {"/InvisibleCube/Cube"}},
{"OBVisibleCube", {"/VisibleCube/Cube"}}};
EXPECT_EQ(expected_data, iterator->data_writers);
/* The scene has no hair or particle systems. */
EXPECT_EQ(0, iterator->hair_writers.size());
EXPECT_EQ(0, iterator->particle_writers.size());
}
} // namespace blender::io

View File

@@ -0,0 +1,81 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "dupli_parent_finder.hh"
#include "BLI_assert.h"
#include "DNA_object_types.h"
namespace blender::io {
void DupliParentFinder::insert(const DupliObject *dupli_ob)
{
dupli_set_.add(dupli_ob->ob);
PersistentID dupli_pid(dupli_ob);
pid_to_dupli_.add(dupli_pid, dupli_ob);
instancer_pid_to_duplis_.lookup_or_add(dupli_pid.instancer_pid(), {}).add(dupli_ob);
}
bool DupliParentFinder::is_duplicated(const Object *object) const
{
return dupli_set_.contains(object);
}
const DupliObject *DupliParentFinder::find_suitable_export_parent(
const DupliObject *dupli_ob) const
{
if (dupli_ob->ob->parent != nullptr) {
const DupliObject *parent = find_duplicated_parent(dupli_ob);
if (parent != nullptr) {
return parent;
}
}
return find_instancer(dupli_ob);
}
const DupliObject *DupliParentFinder::find_duplicated_parent(const DupliObject *dupli_ob) const
{
const PersistentID dupli_pid(dupli_ob);
PersistentID parent_pid = dupli_pid.instancer_pid();
const Object *parent_ob = dupli_ob->ob->parent;
BLI_assert(parent_ob != nullptr);
const Set<const DupliObject *> *found = instancer_pid_to_duplis_.lookup_ptr(parent_pid);
if (!found) {
/* Unexpected, as there should be at least one entry here, for the dupli_ob itself. */
return nullptr;
}
for (const DupliObject *potential_parent_dupli : *found) {
if (potential_parent_dupli->ob != parent_ob) {
continue;
}
PersistentID potential_parent_pid(potential_parent_dupli);
if (potential_parent_pid.is_from_same_instancer_as(dupli_pid)) {
return potential_parent_dupli;
}
}
return nullptr;
}
const DupliObject *DupliParentFinder::find_instancer(const DupliObject *dupli_ob) const
{
PersistentID dupli_pid(dupli_ob);
PersistentID parent_pid = dupli_pid.instancer_pid();
const DupliObject *const *found = pid_to_dupli_.lookup_ptr(parent_pid);
if (!found) {
return nullptr;
}
const DupliObject *instancer_dupli = *found;
return instancer_dupli;
}
} // namespace blender::io

View File

@@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "IO_dupli_persistent_id.hh"
#include "BKE_duplilist.hh"
#include "BLI_map.hh"
#include "BLI_set.hh"
namespace blender::io {
/* Find relations between duplicated objects. This class should be instanced for a single real
* object, and fed its dupli-objects. */
class DupliParentFinder final {
private:
/* To check whether an Object * is instanced by this duplicator. */
Set<const Object *> dupli_set_;
/* To find the DupliObject given its Persistent ID. */
using PIDToDupliMap = Map<const PersistentID, const DupliObject *>;
PIDToDupliMap pid_to_dupli_;
/* Mapping from instancer PID to duplis instanced by it. */
using InstancerPIDToDuplisMap = Map<const PersistentID, Set<const DupliObject *>>;
InstancerPIDToDuplisMap instancer_pid_to_duplis_;
public:
void insert(const DupliObject *dupli_ob);
bool is_duplicated(const Object *object) const;
const DupliObject *find_suitable_export_parent(const DupliObject *dupli_ob) const;
private:
const DupliObject *find_duplicated_parent(const DupliObject *dupli_ob) const;
const DupliObject *find_instancer(const DupliObject *dupli_ob) const;
};
} // namespace blender::io

View File

@@ -0,0 +1,126 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "dupli_parent_finder.hh"
#include <climits>
#include <fmt/format.h>
namespace blender::io {
PersistentID::PersistentID()
{
persistent_id_[0] = INT_MAX;
}
PersistentID::PersistentID(const DupliObject *dupli_ob)
{
for (int index = 0; index < array_length_; ++index) {
persistent_id_[index] = dupli_ob->persistent_id[index];
}
}
PersistentID::PersistentID(const PIDArray &persistent_id_values)
{
persistent_id_ = persistent_id_values;
}
bool PersistentID::is_from_same_instancer_as(const PersistentID &other) const
{
if (persistent_id_[0] == INT_MAX || other.persistent_id_[0] == INT_MAX) {
/* Either one or the other is not instanced at all, so definitely not from the same instancer.
*/
return false;
}
/* Start at index 1 to skip the first digit. */
for (int index = 1; index < array_length_; ++index) {
const int pid_digit_a = persistent_id_[index];
const int pid_digit_b = other.persistent_id_[index];
if (pid_digit_a != pid_digit_b) {
return false;
}
if (pid_digit_a == INT_MAX) {
/* Both persistent IDs were identical so far, and this marks the end of the useful data. */
break;
}
}
return true;
}
PersistentID PersistentID::instancer_pid() const
{
if (persistent_id_[0] == INT_MAX) {
return PersistentID();
}
/* Left-shift the entire PID by 1. */
PIDArray new_pid_values;
int index;
for (index = 0; index < array_length_ - 1; ++index) {
new_pid_values[index] = persistent_id_[index + 1];
}
new_pid_values[index] = INT_MAX;
return PersistentID(new_pid_values);
}
std::string PersistentID::as_object_name_suffix() const
{
fmt::basic_memory_buffer<char, 64> buf;
/* Find one past the last index. */
int index;
for (index = 0; index < array_length_ && persistent_id_[index] < INT_MAX; ++index) {
;
}
/* Iterate backward to construct the string. */
--index;
for (; index >= 0; --index) {
fmt::format_to(fmt::appender(buf), "{}", persistent_id_[index]);
if (index > 0) {
fmt::format_to(fmt::appender(buf), "-");
}
}
return fmt::to_string(buf);
}
uint64_t PersistentID::hash() const
{
uint64_t hash = 5381;
for (const int value : persistent_id_) {
if (value == INT_MAX) {
break;
}
hash = hash * 33 ^ uint64_t(value);
}
return hash;
}
bool operator==(const PersistentID &persistent_id_a, const PersistentID &persistent_id_b)
{
const PersistentID::PIDArray &pid_a = persistent_id_a.persistent_id_;
const PersistentID::PIDArray &pid_b = persistent_id_b.persistent_id_;
for (int index = 0; index < PersistentID::array_length_; ++index) {
const int pid_digit_a = pid_a[index];
const int pid_digit_b = pid_b[index];
if (pid_digit_a != pid_digit_b) {
return false;
}
if (pid_a[index] == INT_MAX) {
break;
}
}
return true;
}
} // namespace blender::io

View File

@@ -0,0 +1,55 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_ID.h"
#include "DNA_object_types.h"
#include "BKE_lib_id.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BKE_object_types.hh"
#include "DEG_depsgraph_query.hh"
#include "IO_mesh_utils.hh"
namespace blender::io {
const Mesh *mesh_coerce_for_export_setup(MeshCoerceForExport &coerce,
Depsgraph *depsgraph,
Object *obj_eval,
const bool apply_modifiers)
{
/* Curves and NURBS surfaces have no mesh in their pre-modified state,
* convert one on demand. */
bool is_original_mesh_type = true;
if (const ID *data_orig = obj_eval->runtime->data_orig) {
if (GS(data_orig->name) != ID_ME) {
is_original_mesh_type = false;
if (!apply_modifiers) {
coerce.owned = BKE_mesh_new_from_object(
depsgraph, DEG_get_original(obj_eval), true, true, true);
}
}
}
if (apply_modifiers) {
coerce.mesh = BKE_object_get_evaluated_mesh(obj_eval);
}
else {
coerce.mesh = is_original_mesh_type ? BKE_object_get_pre_modified_mesh(obj_eval) :
coerce.owned;
}
return coerce.mesh;
}
MeshCoerceForExport::~MeshCoerceForExport()
{
if (owned) {
BKE_id_free(nullptr, owned);
owned = nullptr;
}
}
} // namespace blender::io

View File

@@ -0,0 +1,74 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_abstract_hierarchy_iterator.h"
#include "BLI_assert.h"
#include "BKE_duplilist.hh"
namespace blender::io {
ObjectIdentifier::ObjectIdentifier(Object *object,
Object *duplicated_by,
const PersistentID &persistent_id)
: object(object), duplicated_by(duplicated_by), persistent_id(persistent_id)
{
/* Class invariants:
* If duplicated_by is null, persistent_id must be default.
* If duplicated_by is not null, persistent_id must not be default. */
BLI_assert(duplicated_by == nullptr ? persistent_id == PersistentID() :
!(persistent_id == PersistentID()));
}
ObjectIdentifier ObjectIdentifier::for_real_object(Object *object)
{
return ObjectIdentifier(object, nullptr, PersistentID());
}
ObjectIdentifier ObjectIdentifier::for_hierarchy_context(const HierarchyContext *context)
{
if (context == nullptr) {
return for_graph_root();
}
if (context->duplicator != nullptr) {
return ObjectIdentifier(context->object, context->duplicator, context->persistent_id);
}
return for_real_object(context->object);
}
ObjectIdentifier ObjectIdentifier::for_duplicated_object(const DupliObject *dupli_object,
Object *duplicated_by)
{
return ObjectIdentifier(dupli_object->ob, duplicated_by, PersistentID(dupli_object));
}
ObjectIdentifier ObjectIdentifier::for_graph_root()
{
return ObjectIdentifier(nullptr, nullptr, PersistentID());
}
bool ObjectIdentifier::is_root() const
{
return object == nullptr;
}
bool operator==(const ObjectIdentifier &obj_ident_a, const ObjectIdentifier &obj_ident_b)
{
if (obj_ident_a.object != obj_ident_b.object) {
return false;
}
if (obj_ident_a.duplicated_by != obj_ident_b.duplicated_by) {
return false;
}
/* Return early if we know the expensive persistent_id check won't be necessary. */
if (obj_ident_a.duplicated_by == nullptr) {
return true;
}
/* Same object, both are duplicated, use the persistent IDs to determine equality. */
return obj_ident_a.persistent_id == obj_ident_b.persistent_id;
}
} // namespace blender::io

View File

@@ -0,0 +1,209 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_abstract_hierarchy_iterator.h"
#include "testing/testing.h"
#include "BLI_utildefines.h"
#include <climits>
namespace blender::io {
namespace {
/* Return object pointer for use in tests. This makes it possible to reliably test for
* order/equality functions while using hard-coded values for simplicity. */
Object *fake_pointer(int value)
{
return static_cast<Object *>(POINTER_FROM_INT(value));
}
/* PersistentID subclass for use in tests, making it easier to construct test values. */
class TestPersistentID : public PersistentID {
public:
TestPersistentID(int value0,
int value1,
int value2,
int value3,
int value4,
int value5,
int value6,
int value7)
{
persistent_id_[0] = value0;
persistent_id_[1] = value1;
persistent_id_[2] = value2;
persistent_id_[3] = value3;
persistent_id_[4] = value4;
persistent_id_[5] = value5;
persistent_id_[6] = value6;
persistent_id_[7] = value7;
}
TestPersistentID(int value0, int value1, int value2)
: TestPersistentID(value0, value1, value2, INT_MAX, INT_MAX, INT_MAX, INT_MAX, INT_MAX)
{
}
TestPersistentID(int value0, int value1) : TestPersistentID(value0, value1, INT_MAX) {}
explicit TestPersistentID(int value0) : TestPersistentID(value0, INT_MAX) {}
};
/* ObjectIdentifier subclass for use in tests, making it easier to construct test values. */
class TestObjectIdentifier : public ObjectIdentifier {
public:
TestObjectIdentifier(Object *object, Object *duplicated_by, const PersistentID &persistent_id)
: ObjectIdentifier(object, duplicated_by, persistent_id)
{
}
};
} // namespace
class ObjectIdentifierOrderTest : public testing::Test {};
TEST_F(ObjectIdentifierOrderTest, graph_root)
{
ObjectIdentifier id_root_1 = ObjectIdentifier::for_graph_root();
ObjectIdentifier id_root_2 = ObjectIdentifier::for_graph_root();
EXPECT_TRUE(id_root_1 == id_root_2);
EXPECT_TRUE(id_root_1.hash() == id_root_2.hash());
ObjectIdentifier id_a = ObjectIdentifier::for_real_object(fake_pointer(1));
EXPECT_FALSE(id_root_1 == id_a);
ObjectIdentifier id_accidental_root = ObjectIdentifier::for_real_object(nullptr);
EXPECT_TRUE(id_root_1 == id_accidental_root);
EXPECT_TRUE(id_root_1.hash() == id_accidental_root.hash());
}
TEST_F(ObjectIdentifierOrderTest, real_objects)
{
ObjectIdentifier id_a = ObjectIdentifier::for_real_object(fake_pointer(1));
ObjectIdentifier id_b = ObjectIdentifier::for_real_object(fake_pointer(2));
EXPECT_FALSE(id_a == id_b);
ObjectIdentifier id_c = ObjectIdentifier::for_real_object(fake_pointer(1));
EXPECT_TRUE(id_a == id_c);
EXPECT_TRUE(id_a.hash() == id_c.hash());
}
TEST_F(ObjectIdentifierOrderTest, duplicated_objects)
{
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
TestObjectIdentifier id_same_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
TestObjectIdentifier id_different_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(1));
EXPECT_FALSE(id_real_a == id_dupli_a);
EXPECT_FALSE(id_dupli_a == id_dupli_b);
EXPECT_FALSE(id_dupli_b == id_different_dupli_b);
EXPECT_FALSE(id_dupli_a == id_different_dupli_b);
EXPECT_TRUE(id_dupli_a == id_same_dupli_a);
EXPECT_TRUE(id_dupli_a.hash() == id_same_dupli_a.hash());
}
TEST_F(ObjectIdentifierOrderTest, behavior_as_map_keys)
{
ObjectIdentifier id_root = ObjectIdentifier::for_graph_root();
ObjectIdentifier id_another_root = ObjectIdentifier::for_graph_root();
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
AbstractHierarchyIterator::ExportGraph graph;
/* This inserts the keys with default values. */
graph.add_new(id_root, {});
graph.add_new(id_real_a, {});
graph.add_new(id_dupli_a, {});
graph.add_new(id_dupli_b, {});
graph.add(id_another_root, {});
EXPECT_EQ(4, graph.size());
graph.remove_contained(id_another_root);
EXPECT_EQ(3, graph.size());
TestObjectIdentifier id_another_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
graph.remove_contained(id_another_dupli_b);
EXPECT_EQ(2, graph.size());
}
TEST_F(ObjectIdentifierOrderTest, map_copy_and_update)
{
ObjectIdentifier id_root = ObjectIdentifier::for_graph_root();
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
TestObjectIdentifier id_dupli_c(fake_pointer(1), fake_pointer(3), TestPersistentID(1));
AbstractHierarchyIterator::ExportGraph graph;
/* This inserts the keys with default values. */
graph.add_new(id_root, {});
graph.add_new(id_real_a, {});
graph.add_new(id_dupli_a, {});
graph.add_new(id_dupli_b, {});
graph.add_new(id_dupli_c, {});
EXPECT_EQ(5, graph.size());
AbstractHierarchyIterator::ExportGraph graph_copy = graph;
EXPECT_EQ(5, graph_copy.size());
/* Updating a value in a copy should not update the original. */
HierarchyContext ctx1;
HierarchyContext ctx2;
ctx1.object = fake_pointer(1);
ctx2.object = fake_pointer(2);
graph_copy.lookup(id_root).add_new(&ctx1);
EXPECT_EQ(0, graph.lookup(id_root).size());
/* Deleting a key in the copy should not update the original. */
graph_copy.remove_contained(id_dupli_c);
EXPECT_EQ(4, graph_copy.size());
EXPECT_EQ(5, graph.size());
}
class PersistentIDTest : public testing::Test {};
TEST_F(PersistentIDTest, is_from_same_instancer)
{
PersistentID child_id_a = TestPersistentID(42, 327);
PersistentID child_id_b = TestPersistentID(17, 327);
PersistentID child_id_c = TestPersistentID(17);
EXPECT_TRUE(child_id_a.is_from_same_instancer_as(child_id_b));
EXPECT_FALSE(child_id_a.is_from_same_instancer_as(child_id_c));
}
TEST_F(PersistentIDTest, instancer_id)
{
PersistentID child_id = TestPersistentID(42, 327);
PersistentID expect_instancer_id = TestPersistentID(327);
EXPECT_EQ(expect_instancer_id, child_id.instancer_pid());
EXPECT_EQ(expect_instancer_id.hash(), child_id.instancer_pid().hash());
PersistentID empty_id;
EXPECT_EQ(empty_id, child_id.instancer_pid().instancer_pid());
EXPECT_EQ(empty_id.hash(), child_id.instancer_pid().instancer_pid().hash());
}
TEST_F(PersistentIDTest, as_object_name_suffix)
{
EXPECT_EQ("", PersistentID().as_object_name_suffix());
EXPECT_EQ("47", TestPersistentID(47).as_object_name_suffix());
EXPECT_EQ("327-47", TestPersistentID(47, 327).as_object_name_suffix());
EXPECT_EQ("42-327-47", TestPersistentID(47, 327, 42).as_object_name_suffix());
EXPECT_EQ("7-6-5-4-3-2-1-0", TestPersistentID(0, 1, 2, 3, 4, 5, 6, 7).as_object_name_suffix());
EXPECT_EQ("0-0-0", TestPersistentID(0, 0, 0).as_object_name_suffix());
EXPECT_EQ("0-0", TestPersistentID(0, 0).as_object_name_suffix());
EXPECT_EQ("-3--2--1", TestPersistentID(-1, -2, -3).as_object_name_suffix());
}
} // namespace blender::io

View File

@@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "RNA_access.hh"
#include "RNA_types.hh"
#include "IO_orientation.hh"
namespace blender {
const EnumPropertyItem io_transform_axis[] = {
{IO_AXIS_X, "X", 0, "X", "Positive X axis"},
{IO_AXIS_Y, "Y", 0, "Y", "Positive Y axis"},
{IO_AXIS_Z, "Z", 0, "Z", "Positive Z axis"},
{IO_AXIS_NEGATIVE_X, "NEGATIVE_X", 0, "-X", "Negative X axis"},
{IO_AXIS_NEGATIVE_Y, "NEGATIVE_Y", 0, "-Y", "Negative Y axis"},
{IO_AXIS_NEGATIVE_Z, "NEGATIVE_Z", 0, "-Z", "Negative Z axis"},
{0, nullptr, 0, nullptr, nullptr}};
void io_ui_forward_axis_update(Main * /*main*/, Scene * /*scene*/, PointerRNA *ptr)
{
/* Both forward and up axes cannot be along the same direction. */
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "up_axis", (up + 1) % 6);
}
}
void io_ui_up_axis_update(Main * /*main*/, Scene * /*scene*/, PointerRNA *ptr)
{
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "forward_axis", (forward + 1) % 6);
}
}
} // namespace blender

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_path_util.hh"
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.common"};
namespace io {
std::string path_reference(StringRefNull filepath,
StringRefNull base_src,
StringRefNull base_dst,
ePathReferenceMode mode,
Set<std::pair<std::string, std::string>> *copy_set)
{
const bool is_relative = BLI_path_is_rel(filepath.c_str());
char filepath_abs[PATH_MAX];
STRNCPY(filepath_abs, filepath.c_str());
BLI_path_abs(filepath_abs, base_src.c_str());
BLI_path_normalize(filepath_abs);
/* Figure out final mode to be used. */
if (mode == PATH_REFERENCE_MATCH) {
mode = is_relative ? PATH_REFERENCE_RELATIVE : PATH_REFERENCE_ABSOLUTE;
}
else if (mode == PATH_REFERENCE_AUTO) {
mode = BLI_path_contains(base_dst.c_str(), filepath_abs) ? PATH_REFERENCE_RELATIVE :
PATH_REFERENCE_ABSOLUTE;
}
else if (mode == PATH_REFERENCE_COPY) {
char filepath_cpy[PATH_MAX];
BLI_path_join(filepath_cpy, PATH_MAX, base_dst.c_str(), BLI_path_basename(filepath_abs));
copy_set->add(std::make_pair(filepath_abs, filepath_cpy));
STRNCPY(filepath_abs, filepath_cpy);
mode = PATH_REFERENCE_RELATIVE;
}
/* Now we know the final path mode. */
if (mode == PATH_REFERENCE_ABSOLUTE) {
return filepath_abs;
}
if (mode == PATH_REFERENCE_RELATIVE) {
char rel_path[PATH_MAX];
STRNCPY(rel_path, filepath_abs);
BLI_path_rel(rel_path, base_dst.c_str());
/* Can't always find relative path (e.g. between different drives). */
if (!BLI_path_is_rel(rel_path)) {
return filepath_abs;
}
return rel_path + 2; /* Skip blender's internal "//" prefix. */
}
if (mode == PATH_REFERENCE_STRIP) {
return BLI_path_basename(filepath_abs);
}
BLI_assert_msg(false, "Invalid path reference mode");
return filepath_abs;
}
void path_reference_copy(const Set<std::pair<std::string, std::string>> &copy_set)
{
for (const auto &copy : copy_set) {
const char *src = copy.first.c_str();
const char *dst = copy.second.c_str();
if (!BLI_exists(src)) {
CLOG_WARN(&LOG, "Missing source file '%s', not copying", src);
continue;
}
if (0 == BLI_path_cmp_normalized(src, dst)) {
continue; /* Source and destination are the same. */
}
if (!BLI_file_ensure_parent_dir_exists(dst)) {
CLOG_WARN(&LOG, "Can't make directory for '%s', not copying", dst);
continue;
}
if (BLI_copy(src, dst) != 0) {
CLOG_WARN(&LOG, "Can't copy '%s' to '%s'", src, dst);
continue;
}
}
}
} // namespace io
} // namespace blender

View File

@@ -0,0 +1,169 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_string_utils.hh"
/* NOTE: we could use C++17 <charconv> from_chars to parse
* floats, but even if some compilers claim full support,
* their standard libraries are not quite there yet.
* LLVM/libc++ only has a float parser since LLVM 14,
* and GCC/libstdc++ since 11.1. So until at least these are
* the minimum spec, use an external library. */
#include "fast_float.h"
#include <charconv>
namespace blender::io {
StringRef read_next_line(StringRef &buffer)
{
const char *start = buffer.begin();
const char *end = buffer.end();
size_t len = 0;
const char *ptr = start;
while (ptr < end) {
char c = *ptr++;
if (c == '\n') {
break;
}
++len;
}
buffer = StringRef(ptr, end);
return StringRef(start, len);
}
static bool is_whitespace(char c)
{
return c <= ' ';
}
const char *drop_whitespace(const char *p, const char *end)
{
while (p < end && is_whitespace(*p)) {
++p;
}
return p;
}
const char *drop_non_whitespace(const char *p, const char *end)
{
while (p < end && !is_whitespace(*p)) {
++p;
}
return p;
}
static const char *drop_sign(const char *p, const char *end, int &sign)
{
sign = 1;
if (p < end) {
if (*p == '+') {
++p;
}
if (*p == '-') {
sign = -1;
++p;
}
}
return p;
}
const char *try_parse_float(
const char *p, const char *end, int fallback, bool &success, float &dst, bool skip_space)
{
if (skip_space) {
p = drop_whitespace(p, end);
}
int sign = 0;
p = drop_sign(p, end, sign);
fast_float::from_chars_result res = fast_float::from_chars(p, end, dst);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range) || res.ptr < end) {
dst = fallback;
success = false;
}
else {
dst *= sign;
success = true;
}
return res.ptr;
}
const char *try_parse_int(
const char *p, const char *end, int fallback, bool &success, int &dst, bool skip_space)
{
if (skip_space) {
p = drop_whitespace(p, end);
}
int sign = 0;
p = drop_sign(p, end, sign);
std::from_chars_result res = std::from_chars(p, end, dst);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range) || res.ptr < end) {
dst = fallback;
success = false;
}
else {
dst *= sign;
success = true;
}
return res.ptr;
}
static const char *drop_plus(const char *p, const char *end)
{
if (p < end && *p == '+') {
++p;
}
return p;
}
const char *parse_float(const char *p,
const char *end,
float fallback,
float &dst,
bool skip_space,
bool require_trailing_space)
{
if (skip_space) {
p = drop_whitespace(p, end);
}
p = drop_plus(p, end);
fast_float::from_chars_result res = fast_float::from_chars(p, end, dst);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
dst = fallback;
}
else if (require_trailing_space && res.ptr < end && !is_whitespace(*res.ptr)) {
/* If there are trailing non-space characters, do not eat up the number. */
dst = fallback;
return p;
}
return res.ptr;
}
const char *parse_floats(const char *p,
const char *end,
float fallback,
float *dst,
int count,
bool require_trailing_space)
{
for (int i = 0; i < count; ++i) {
p = parse_float(p, end, fallback, dst[i], true, require_trailing_space);
}
return p;
}
const char *parse_int(const char *p, const char *end, int fallback, int &dst, bool skip_space)
{
if (skip_space) {
p = drop_whitespace(p, end);
}
p = drop_plus(p, end);
std::from_chars_result res = std::from_chars(p, end, dst);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
dst = fallback;
}
return res.ptr;
}
} // namespace blender::io

View File

@@ -0,0 +1,139 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "IO_string_utils.hh"
#include "testing/testing.h"
namespace blender::io {
#define EXPECT_STRREF_EQ(str1, str2) EXPECT_STREQ(str1, std::string(str2).c_str())
TEST(io_common_string_utils, read_next_line)
{
std::string str = "abc\n \n\nline with \t spaces\nCRLF ending:\r\na";
StringRef s = str;
EXPECT_STRREF_EQ("abc", read_next_line(s));
EXPECT_STRREF_EQ(" ", read_next_line(s));
EXPECT_STRREF_EQ("", read_next_line(s));
EXPECT_STRREF_EQ("line with \t spaces", read_next_line(s));
EXPECT_STRREF_EQ("CRLF ending:\r", read_next_line(s));
EXPECT_STRREF_EQ("a", read_next_line(s));
EXPECT_TRUE(s.is_empty());
}
static StringRef drop_whitespace(StringRef s)
{
return StringRef(drop_whitespace(s.begin(), s.end()), s.end());
}
static StringRef parse_int(StringRef s, int fallback, int &dst, bool skip_space = true)
{
return StringRef(parse_int(s.begin(), s.end(), fallback, dst, skip_space), s.end());
}
static StringRef parse_float(StringRef s,
float fallback,
float &dst,
bool skip_space = true,
bool require_trailing_space = false)
{
return StringRef(
parse_float(s.begin(), s.end(), fallback, dst, skip_space, require_trailing_space), s.end());
}
TEST(io_common_string_utils, drop_whitespace)
{
/* Empty */
EXPECT_STRREF_EQ("", drop_whitespace(""));
/* Only whitespace */
EXPECT_STRREF_EQ("", drop_whitespace(" "));
EXPECT_STRREF_EQ("", drop_whitespace(" "));
EXPECT_STRREF_EQ("", drop_whitespace(" \t\n\r "));
/* Drops leading whitespace */
EXPECT_STRREF_EQ("a", drop_whitespace(" a"));
EXPECT_STRREF_EQ("a b", drop_whitespace(" a b"));
EXPECT_STRREF_EQ("a b ", drop_whitespace(" a b "));
/* No leading whitespace */
EXPECT_STRREF_EQ("c", drop_whitespace("c"));
/* Case with backslash, should be treated as whitespace */
EXPECT_STRREF_EQ("d", drop_whitespace(" \t d"));
}
TEST(io_common_string_utils, parse_int_valid)
{
std::string str = "1 -10 \t 1234 1234567890 +7 123a";
StringRef s = str;
int val;
s = parse_int(s, 0, val);
EXPECT_EQ(1, val);
s = parse_int(s, 0, val);
EXPECT_EQ(-10, val);
s = parse_int(s, 0, val);
EXPECT_EQ(1234, val);
s = parse_int(s, 0, val);
EXPECT_EQ(1234567890, val);
s = parse_int(s, 0, val);
EXPECT_EQ(7, val);
s = parse_int(s, 0, val);
EXPECT_EQ(123, val);
EXPECT_STRREF_EQ("a", s);
}
TEST(io_common_string_utils, parse_int_invalid)
{
int val;
/* Invalid syntax */
EXPECT_STRREF_EQ("--123", parse_int("--123", -1, val));
EXPECT_EQ(val, -1);
EXPECT_STRREF_EQ("foobar", parse_int("foobar", -2, val));
EXPECT_EQ(val, -2);
/* Out of integer range */
EXPECT_STRREF_EQ(" a", parse_int("1234567890123 a", -3, val));
EXPECT_EQ(val, -3);
/* Has leading white-space when we don't expect it */
EXPECT_STRREF_EQ(" 1", parse_int(" 1", -4, val, false));
EXPECT_EQ(val, -4);
}
TEST(io_common_string_utils, parse_float_valid)
{
std::string str = "1 -10 123.5 -17.125 0.1 1e6 50.0e-1";
StringRef s = str;
float val;
s = parse_float(s, 0, val);
EXPECT_EQ(1.0f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(-10.0f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(123.5f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(-17.125f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(0.1f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(1.0e6f, val);
s = parse_float(s, 0, val);
EXPECT_EQ(5.0f, val);
EXPECT_TRUE(s.is_empty());
}
TEST(io_common_string_utils, parse_float_invalid)
{
float val;
/* Invalid syntax */
EXPECT_STRREF_EQ("_0", parse_float("_0", -1.0f, val));
EXPECT_EQ(val, -1.0f);
EXPECT_STRREF_EQ("..5", parse_float("..5", -2.0f, val));
EXPECT_EQ(val, -2.0f);
/* Out of float range. */
EXPECT_STRREF_EQ(" a", parse_float("9.0e500 a", -3.0f, val));
EXPECT_EQ(val, -3.0f);
/* Has leading white-space when we don't expect it */
EXPECT_STRREF_EQ(" 1", parse_float(" 1", -4.0f, val, false));
EXPECT_EQ(val, -4.0f);
/* Has trailing non-number characters when we don't want them */
EXPECT_STRREF_EQ("123.5.png", parse_float(" 123.5.png", -5.0f, val, true, true));
EXPECT_EQ(val, -5.0f);
}
} // namespace blender::io

View File

@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_subdiv_disabler.hh"
#include "BLI_listbase.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_layer_types.h"
#include "DNA_mesh_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "BKE_layer.hh"
#include "BKE_modifier.hh"
namespace blender::io {
ModifierData *SubdivModifierDisabler::get_subdiv_modifier(Scene *scene,
const Object *ob,
ModifierMode mode)
{
/* Returns the last subdiv modifier associated with an object,
* if that modifier should be disabled.
* We do not disable the subdiv modifier if other modifiers are
* applied after it, with the sole exception of particle modifiers,
* which are allowed.
* Returns nullptr if there is not any subdiv modifier to disable.
*/
ModifierData *md = static_cast<ModifierData *>(ob->modifiers.last);
for (; md; md = md->prev) {
/* Ignore disabled modifiers. */
if (!BKE_modifier_is_enabled(scene, md, mode)) {
continue;
}
if (md->type == eModifierType_Subsurf) {
SubsurfModifierData *smd = reinterpret_cast<SubsurfModifierData *>(md);
if (smd->subdivType == ME_CC_SUBSURF) {
/* This is a Catmull-Clark modifier. */
return md;
}
/* Not Catmull-Clark, so ignore it. */
return nullptr;
}
/* If any modifier other than a particle system exists after the
* subdiv modifier, then abort. */
if (md->type != eModifierType_ParticleSystem) {
return nullptr;
}
}
return nullptr;
}
SubdivModifierDisabler::SubdivModifierDisabler(Depsgraph *depsgraph) : depsgraph_(depsgraph) {}
SubdivModifierDisabler::~SubdivModifierDisabler()
{
/* Enable previously disabled modifiers. */
for (ModifierData *modifier : disabled_modifiers_) {
modifier->mode &= ~eModifierMode_DisableTemporary;
}
/* Update object to render with restored modifiers in the viewport. */
for (Object *object : modified_objects_) {
DEG_id_tag_update(&object->id, ID_RECALC_GEOMETRY);
}
}
void SubdivModifierDisabler::disable_modifiers()
{
eEvaluationMode eval_mode = DEG_get_mode(depsgraph_);
const ModifierMode mode = eval_mode == DAG_EVAL_VIEWPORT ? eModifierMode_Realtime :
eModifierMode_Render;
Scene *scene = DEG_get_input_scene(depsgraph_);
ViewLayer *view_layer = DEG_get_input_view_layer(depsgraph_);
BKE_view_layer_synced_ensure(*DEG_get_bmain(depsgraph_), scene, view_layer);
for (Base &base : *BKE_view_layer_object_bases_get(view_layer)) {
Object *object = base.object;
if (object->type != OB_MESH) {
continue;
}
/* Check if a subdiv modifier exists, and should be disabled. */
ModifierData *mod = get_subdiv_modifier(scene, object, mode);
if (!mod) {
continue;
}
/* This might disable more modifiers than necessary, as it doesn't take restrictions like
* "export selected objects only" into account. However, with the subdivisions disabled,
* moving to a different frame is also going to be faster, so in the end this is probably
* a good thing to do. */
disable_modifier(mod);
modified_objects_.append(object);
DEG_id_tag_update(&object->id, ID_RECALC_GEOMETRY);
}
}
void SubdivModifierDisabler::disable_modifier(ModifierData *mod)
{
mod->mode |= eModifierMode_DisableTemporary;
disabled_modifiers_.append(mod);
}
} // namespace blender::io