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,73 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
exporter
importer
intern
../common
../../editors/include
../../makesrna
)
set(INC_SYS
)
set(SRC
IO_stl.cc
importer/stl_import.cc
importer/stl_import_ascii_reader.cc
importer/stl_import_binary_reader.cc
importer/stl_import_mesh.cc
exporter/stl_export.cc
exporter/stl_export_writer.cc
intern/stl_data.hh
IO_stl.hh
importer/stl_import.hh
importer/stl_import_ascii_reader.hh
importer/stl_import_binary_reader.hh
importer/stl_import_mesh.hh
exporter/stl_export.hh
exporter/stl_export_writer.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
bf_io_common
PRIVATE bf::extern::fast_float
PRIVATE bf::windowmanager
)
blender_add_lib(bf_io_stl "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/stl_exporter_tests.cc
)
set(TEST_INC
${INC}
../../blenloader
../../../../tests/gtests
)
set(TEST_LIB
${LIB}
bf_blenloader_test_util
bf_io_stl
)
blender_add_test_suite_lib(io_stl "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}")
endif()

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include "BLI_timeit.hh"
#include "IO_stl.hh"
#include "stl_export.hh"
#include "stl_import.hh"
namespace blender {
void STL_import(bContext *C, const STLImportParams *import_params)
{
SCOPED_TIMER("STL Import");
io::stl::importer_main(C, *import_params);
}
void STL_export(bContext *C, const STLExportParams *export_params)
{
SCOPED_TIMER("STL Export");
io::stl::exporter_main(C, *export_params);
}
Mesh *STL_import_mesh(const STLImportParams *import_params)
{
return io::stl::read_stl_file(*import_params);
}
} // namespace blender

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include "BLI_path_utils.hh"
#include "DEG_depsgraph.hh"
#include "DNA_ID.h"
#include "IO_orientation.hh"
namespace blender {
struct Mesh;
struct bContext;
struct ReportList;
struct STLImportParams {
/** Full path to the source STL file to import. */
char filepath[FILE_MAX] = "";
eIOAxis forward_axis = IO_AXIS_Y;
eIOAxis up_axis = IO_AXIS_Z;
bool use_facet_normal = false;
bool use_scene_unit = false;
float global_scale = 1.0f;
bool use_mesh_validate = true;
ReportList *reports = nullptr;
};
struct STLExportParams {
/** Full path to the to-be-saved STL file. */
char filepath[FILE_MAX] = "";
eIOAxis forward_axis = IO_AXIS_Y;
eIOAxis up_axis = IO_AXIS_Z;
float global_scale = 1.0f;
bool export_selected_objects = false;
bool use_scene_unit = false;
bool apply_modifiers = true;
eEvaluationMode evaluation_mode = DAG_EVAL_RENDER;
bool ascii_format = false;
bool use_batch = false;
char collection[MAX_ID_NAME - 2] = "";
ReportList *reports = nullptr;
};
void STL_import(bContext *C, const STLImportParams *import_params);
void STL_export(bContext *C, const STLExportParams *export_params);
Mesh *STL_import_mesh(const STLImportParams *import_params);
} // namespace blender

View File

@@ -0,0 +1,207 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <memory>
#include "BKE_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_mesh_wrapper.hh"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_layer_types.h"
#include "DNA_mesh_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "ED_util.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "IO_mesh_utils.hh"
#include "IO_stl.hh"
#include "stl_data.hh"
#include "stl_export.hh"
#include "stl_export_writer.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.stl"};
namespace io::stl {
void export_frame(Depsgraph *depsgraph,
float scene_unit_scale,
const STLExportParams &export_params)
{
std::unique_ptr<FileWriter> writer;
/* If not exporting in batch, create single writer for all objects. */
if (!export_params.use_batch) {
try {
writer = std::make_unique<FileWriter>(export_params.filepath, export_params.ascii_format);
}
catch (const std::runtime_error &ex) {
CLOG_ERROR(&LOG, "Error: %s", ex.what());
BKE_reportf(export_params.reports,
RPT_ERROR,
"STL Export: Cannot open file '%s'",
export_params.filepath);
return;
}
}
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 | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (object->type != OB_MESH) {
continue;
}
if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
continue;
}
/* If exporting in batch, create writer for each iteration over objects. */
if (export_params.use_batch) {
/* Get object name by skipping initial "OB" prefix. */
char object_name[sizeof(object->id.name) - 2];
STRNCPY(object_name, object->id.name + 2);
BLI_path_make_safe_filename(object_name);
/* Replace spaces with underscores. */
BLI_string_replace_char(object_name, ' ', '_');
/* Include object name in the exported file name. */
char filepath[FILE_MAX];
STRNCPY(filepath, export_params.filepath);
/* When basename is just ".stl", regular path functions would
* treat it as a hidden file called ".stl". Remove the extension
* before trying to add a suffix. */
const char *basename = BLI_path_basename(filepath);
if (basename != nullptr && BLI_strcasecmp(basename, ".stl") == 0) {
*const_cast<char *>(basename) = '\0';
}
BLI_path_suffix(filepath, FILE_MAX, object_name, "");
/* Make sure we have `.stl` extension (case insensitive). */
if (!BLI_path_extension_check(filepath, ".stl")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".stl");
}
try {
writer = std::make_unique<FileWriter>(filepath, export_params.ascii_format);
}
catch (const std::runtime_error &ex) {
CLOG_ERROR(&LOG, "Error: %s", ex.what());
BKE_reportf(
export_params.reports, RPT_ERROR, "STL Export: Cannot open file '%s'", filepath);
return;
}
}
Object *obj_eval = DEG_get_evaluated(depsgraph, object);
MeshCoerceForExport coerce;
const Mesh *mesh = mesh_coerce_for_export_setup(
coerce, depsgraph, obj_eval, export_params.apply_modifiers);
/* Ensure data exists if currently in edit mode. */
BKE_mesh_wrapper_ensure_mdata(const_cast<Mesh *>(mesh));
/* Calculate transform. */
float global_scale = export_params.global_scale * scene_unit_scale;
float axes_transform[3][3];
unit_m3(axes_transform);
float xform[4][4];
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(
export_params.forward_axis, export_params.up_axis, IO_AXIS_Y, IO_AXIS_Z, axes_transform);
mul_m4_m3m4(xform, axes_transform, obj_eval->object_to_world().ptr());
/* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */
mul_v3_m3v3(xform[3], axes_transform, obj_eval->object_to_world().location());
xform[3][3] = obj_eval->object_to_world()[3][3];
const bool mirrored = is_negative_m4(xform);
/* Write triangles. */
const Span<float3> positions = mesh->vert_positions();
const Span<int> corner_verts = mesh->corner_verts();
for (const int3 &tri : mesh->corner_tris()) {
PackedTriangle data{};
for (int i = 0; i < 3; i++) {
/* Reverse face order for mirrored objects. */
int idx = mirrored ? 2 - i : i;
float3 pos = positions[corner_verts[tri[idx]]];
mul_m4_v3(xform, pos);
pos *= global_scale;
data.vertices[i] = pos;
}
data.normal = math::normal_tri(data.vertices[0], data.vertices[1], data.vertices[2]);
writer->write_triangle(data);
}
}
DEG_OBJECT_ITER_END;
}
void exporter_main(const bContext *C, const STLExportParams &export_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
ED_editors_flush_edits(bmain);
Depsgraph *depsgraph = DEG_graph_new(bmain, scene, view_layer, export_params.evaluation_mode);
if (export_params.collection[0]) {
Collection *collection = reinterpret_cast<Collection *>(
BKE_libblock_find_name(bmain, ID_GR, export_params.collection));
if (!collection) {
BKE_reportf(export_params.reports,
RPT_ERROR,
"STL Export: Unable to find collection '%s'",
export_params.collection);
DEG_graph_free(depsgraph);
return;
}
DEG_graph_build_from_collection(depsgraph, collection);
}
else {
DEG_graph_build_from_view_layer(depsgraph);
}
BKE_scene_graph_update_tagged(depsgraph, bmain);
float scene_unit_scale = 1.0f;
if ((scene->unit.system != USER_UNIT_NONE) && export_params.use_scene_unit) {
scene_unit_scale = scene->unit.scale_length;
}
export_frame(depsgraph, scene_unit_scale, export_params);
DEG_graph_free(depsgraph);
}
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include "IO_stl.hh"
namespace blender {
struct Depsgraph;
struct bContext;
namespace io::stl {
void exporter_main(const bContext *C, const STLExportParams &export_params);
void export_frame(Depsgraph *depsgraph,
float scene_unit_scale,
const STLExportParams &export_params);
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,88 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdint>
#include <cstdio>
#include <stdexcept>
#include <fmt/format.h>
#include "stl_data.hh"
#include "stl_export_writer.hh"
#include "BLI_fileops.h"
namespace blender::io::stl {
FileWriter::FileWriter(const char *filepath, bool ascii) : tris_num_(0), ascii_(ascii)
{
file_ = BLI_fopen(filepath, "wb");
if (file_ == nullptr) {
throw std::runtime_error("STL export: failed to open file");
}
/* Write header */
if (ascii_) {
fmt::print(file_, "solid \n");
}
else {
const char header[BINARY_HEADER_SIZE] = {};
fwrite(header, 1, BINARY_HEADER_SIZE, file_);
/* Write placeholder for number of triangles, so that it can be updated later (after all
* triangles have been written). */
fwrite(&tris_num_, sizeof(uint32_t), 1, file_);
}
}
FileWriter::~FileWriter()
{
if (file_ == nullptr) {
return;
}
if (ascii_) {
fmt::print(file_, "endsolid \n");
}
else {
fseek(file_, BINARY_HEADER_SIZE, SEEK_SET);
fwrite(&tris_num_, sizeof(uint32_t), 1, file_);
}
fclose(file_);
}
void FileWriter::write_triangle(const PackedTriangle &data)
{
tris_num_++;
if (ascii_) {
fmt::print(file_,
"facet normal {} {} {}\n"
" outer loop\n"
" vertex {} {} {}\n"
" vertex {} {} {}\n"
" vertex {} {} {}\n"
" endloop\n"
"endfacet\n",
data.normal.x,
data.normal.y,
data.normal.z,
data.vertices[0].x,
data.vertices[0].y,
data.vertices[0].z,
data.vertices[1].x,
data.vertices[1].y,
data.vertices[1].z,
data.vertices[2].x,
data.vertices[2].y,
data.vertices[2].z);
}
else {
fwrite(&data, sizeof(data), 1, file_);
}
}
} // namespace blender::io::stl

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdint>
#include <cstdio>
namespace blender::io::stl {
struct PackedTriangle;
class FileWriter {
public:
FileWriter(const char *filepath, bool ascii);
~FileWriter();
void write_triangle(const PackedTriangle &data);
private:
FILE *file_;
uint32_t tris_num_;
bool ascii_;
};
} // namespace blender::io::stl

View File

@@ -0,0 +1,177 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdio>
#include "BKE_context.hh"
#include "BKE_layer.hh"
#include "BKE_library.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "DNA_collection_types.h"
#include "DNA_layer_types.h"
#include "DNA_scene_types.h"
#include "BLI_fileops.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_memory_utils.hh"
#include "BLI_string.h"
#include "DNA_object_types.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "stl_data.hh"
#include "stl_import.hh"
#include "stl_import_ascii_reader.hh"
#include "stl_import_binary_reader.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.stl"};
namespace io::stl {
void stl_import_report_error(FILE *file)
{
CLOG_ERROR(&LOG, "STL Importer: failed to read file");
if (feof(file)) {
CLOG_ERROR(&LOG, "End of file reached");
}
else if (ferror(file)) {
perror("Error");
}
}
Mesh *read_stl_file(const STLImportParams &import_params)
{
FILE *file = BLI_fopen(import_params.filepath, "rb");
if (!file) {
CLOG_ERROR(&LOG, "Failed to open STL file:'%s'.", import_params.filepath);
BKE_reportf(import_params.reports,
RPT_ERROR,
"STL Import: Cannot open file '%s'",
import_params.filepath);
return nullptr;
}
BLI_SCOPED_DEFER([&]() { fclose(file); });
/* Detect STL file type by comparing file size with expected file size,
* could check if file starts with "solid", but some files do not adhere,
* this is the same as the old Python importer.
*/
uint32_t num_tri = 0;
size_t file_size = BLI_file_size(import_params.filepath);
fseek(file, BINARY_HEADER_SIZE, SEEK_SET);
if (fread(&num_tri, sizeof(uint32_t), 1, file) != 1) {
stl_import_report_error(file);
BKE_reportf(import_params.reports,
RPT_ERROR,
"STL Import: Failed to read file '%s'",
import_params.filepath);
return nullptr;
}
bool is_ascii_stl = (file_size != (BINARY_HEADER_SIZE + 4 + BINARY_STRIDE * num_tri));
Mesh *mesh = is_ascii_stl ?
read_stl_ascii(import_params.filepath, import_params.use_facet_normal) :
read_stl_binary(file, import_params.use_facet_normal);
if (mesh == nullptr) {
CLOG_ERROR(&LOG, "STL Importer: Failed to import mesh '%s'", import_params.filepath);
BKE_reportf(import_params.reports,
RPT_ERROR,
"STL Import: Failed to import mesh from file '%s'",
import_params.filepath);
return nullptr;
}
if (import_params.use_mesh_validate) {
bool verbose_validate = false;
#ifndef NDEBUG
verbose_validate = true;
#endif
bke::mesh_validate(*mesh, verbose_validate);
}
return mesh;
}
void importer_main(const bContext *C, const STLImportParams &import_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
importer_main(bmain, scene, view_layer, import_params);
}
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const STLImportParams &import_params)
{
/* Name used for both mesh and object. */
char ob_name[FILE_MAX];
STRNCPY(ob_name, BLI_path_basename(import_params.filepath));
BLI_path_extension_strip(ob_name);
Mesh *mesh = read_stl_file(import_params);
if (!mesh) {
return;
}
Mesh *mesh_in_main = BKE_mesh_add(bmain, ob_name);
BKE_mesh_nomain_to_mesh(mesh, mesh_in_main, nullptr);
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
if (!ID_IS_EDITABLE(lc->collection)) {
BKE_report(import_params.reports,
RPT_WARNING,
"Could not find an editable collection in current scene, imported data will not be "
"instantiated");
}
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name);
obj->data = id_cast<ID *>(mesh_in_main);
BKE_collection_object_add(bmain, lc->collection, obj);
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
if (Base *base = BKE_view_layer_base_find(view_layer, obj)) {
/* `base` will be nullptr if the Object could not be instantiated in the current viewlayer. */
BKE_view_layer_base_select_and_set_active(view_layer, base);
}
float global_scale = import_params.global_scale;
if ((scene->unit.system != USER_UNIT_NONE) && import_params.use_scene_unit) {
global_scale /= scene->unit.scale_length;
}
float scale_vec[3] = {global_scale, global_scale, global_scale};
float obmat3x3[3][3];
unit_m3(obmat3x3);
float obmat4x4[4][4];
unit_m4(obmat4x4);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, obmat3x3);
copy_m4_m3(obmat4x4, obmat3x3);
rescale_m4(obmat4x4, scale_vec);
BKE_object_apply_mat4(obj, obmat4x4, true, false);
DEG_id_tag_update(&lc->collection->id, ID_RECALC_SYNC_TO_EVAL);
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS;
DEG_id_tag_update_ex(bmain, &obj->id, flags);
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
}
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdio>
#include "IO_stl.hh"
namespace blender {
struct bContext;
struct Main;
struct Mesh;
struct Scene;
struct ViewLayer;
namespace io::stl {
void stl_import_report_error(FILE *file);
/* Used from Geo nodes import for Mesh* access */
Mesh *read_stl_file(const STLImportParams &import_params);
/* Main import function used from within Blender. */
void importer_main(const bContext *C, const STLImportParams &import_params);
/* Used from tests, where full bContext does not exist. */
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const STLImportParams &import_params);
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <system_error>
#include "BLI_fileops.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "DNA_mesh_types.h"
/* 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 "stl_data.hh"
#include "stl_import_ascii_reader.hh"
#include "stl_import_mesh.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.stl"};
namespace io::stl {
class StringBuffer {
private:
char *start;
const char *end;
public:
StringBuffer(char *buf, size_t len)
{
start = buf;
end = start + len;
}
bool is_empty() const
{
return start == end;
}
void drop_leading_control_chars()
{
while ((start < end) && (*start) <= ' ') {
start++;
}
}
void drop_leading_non_control_chars()
{
while ((start < end) && (*start) > ' ') {
start++;
}
}
void drop_line()
{
while (start < end && *start != '\n') {
start++;
}
}
bool parse_token(const char *token, size_t token_length)
{
drop_leading_control_chars();
if (end - start < token_length + 1) {
return false;
}
if (memcmp(start, token, token_length) != 0) {
return false;
}
if (start[token_length] > ' ') {
return false;
}
start += token_length + 1;
return true;
}
void drop_token()
{
drop_leading_non_control_chars();
drop_leading_control_chars();
}
void parse_float(float &out)
{
drop_leading_control_chars();
/* Skip '+' */
if (start < end && *start == '+') {
start++;
}
fast_float::from_chars_result res = fast_float::from_chars(start, end, out);
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
out = 0.0f;
}
start = const_cast<char *>(res.ptr);
}
};
static inline void parse_float3(StringBuffer &buf, float3 &out)
{
for (int i = 0; i < 3; i++) {
buf.parse_float(out[i]);
}
}
Mesh *read_stl_ascii(const char *filepath, const bool use_custom_normals)
{
size_t buffer_len;
char *buffer = BLI_file_read_text_as_mem(filepath, 0, &buffer_len);
if (buffer == nullptr) {
CLOG_ERROR(&LOG, "STL Importer: cannot read from ASCII STL file: '%s'", filepath);
return nullptr;
}
BLI_SCOPED_DEFER([&]() { MEM_delete(buffer); });
constexpr int num_reserved_tris = 1024;
StringBuffer str_buf(buffer, buffer_len);
STLMeshHelper stl_mesh(num_reserved_tris, use_custom_normals);
PackedTriangle data{};
str_buf.drop_line(); /* Skip header line */
while (!str_buf.is_empty()) {
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, data.vertices[0]);
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, data.vertices[1]);
}
if (str_buf.parse_token("vertex", 6)) {
parse_float3(str_buf, data.vertices[2]);
}
stl_mesh.add_triangle(data);
}
else if (str_buf.parse_token("facet", 5)) {
str_buf.drop_token(); /* Expecting "normal" */
parse_float3(str_buf, data.normal);
}
else {
str_buf.drop_token();
}
}
return stl_mesh.to_mesh();
}
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,36 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
namespace blender {
struct Mesh;
/**
* ASCII STL spec:
* <pre>
* solid name
* facet normal ni nj nk
* outer loop
* vertex v1x v1y v1z
* vertex v2x v2y v2z
* vertex v3x v3y v3z
* endloop
* endfacet
* ...
* endsolid name
* </pre>
*/
namespace io::stl {
Mesh *read_stl_ascii(const char *filepath, bool use_custom_normals);
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,62 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include <cstdint>
#include <cstdio>
#include "BKE_mesh.hh"
#include "BLI_array.hh"
#include "DNA_mesh_types.h"
#include "IO_validate.hh"
#include "CLG_log.h"
#include "stl_data.hh"
#include "stl_import.hh"
#include "stl_import_binary_reader.hh"
#include "stl_import_mesh.hh"
namespace blender::io::stl {
static CLG_LogRef LOG = {"io.stl"};
Mesh *read_stl_binary(FILE *file, const bool use_custom_normals)
{
const int chunk_size = 1024;
uint32_t num_tris = 0;
fseek(file, BINARY_HEADER_SIZE, SEEK_SET);
if (fread(&num_tris, sizeof(uint32_t), 1, file) != 1) {
stl_import_report_error(file);
return nullptr;
}
if (num_tris == 0) {
return BKE_mesh_new_nomain(0, 0, 0, 0);
}
if (!validate::size_fits_in_int(int64_t(num_tris) * 3)) {
CLOG_WARN(&LOG, "STL mesh too large to import, exceeds max int size");
return nullptr;
}
Array<PackedTriangle> tris_buf(chunk_size);
STLMeshHelper stl_mesh(num_tris, use_custom_normals);
size_t num_read_tris;
while ((num_read_tris = fread(tris_buf.data(), sizeof(PackedTriangle), chunk_size, file))) {
for (size_t i = 0; i < num_read_tris; i++) {
stl_mesh.add_triangle(tris_buf[i]);
}
}
return stl_mesh.to_mesh();
}
} // namespace blender::io::stl

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdio>
namespace blender {
struct Mesh;
/* Binary STL spec.:
* - UINT8[80] - Header - 80 bytes
* - UINT32 - Number of triangles - 4 bytes
* For each triangle - 50 bytes:
* - REAL32[3] - Normal vector - 12 bytes
* - REAL32[3] - Vertex 1 - 12 bytes
* - REAL32[3] - Vertex 2 - 12 bytes
* - REAL32[3] - Vertex 3 - 12 bytes
* - UINT16 - Attribute byte count - 2 bytes
*/
namespace io::stl {
Mesh *read_stl_binary(FILE *file, bool use_custom_normals);
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#include "BKE_mesh.hh"
#include "BLI_array_utils.hh"
#include "BLI_span.hh"
#include "DNA_mesh_types.h"
#include "stl_data.hh"
#include "stl_import_mesh.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.stl"};
namespace io::stl {
STLMeshHelper::STLMeshHelper(int tris_num, bool use_custom_normals)
: use_custom_normals_(use_custom_normals)
{
degenerate_tris_num_ = 0;
duplicate_tris_num_ = 0;
tris_.reserve(tris_num);
/* Upper bound (all vertices are unique). */
verts_.reserve(int64_t(tris_num) * 3);
if (use_custom_normals) {
loop_normals_.reserve(int64_t(tris_num) * 3);
}
}
bool STLMeshHelper::add_triangle(const PackedTriangle &data)
{
int v1_id = verts_.index_of_or_add(data.vertices[0]);
int v2_id = verts_.index_of_or_add(data.vertices[1]);
int v3_id = verts_.index_of_or_add(data.vertices[2]);
if ((v1_id == v2_id) || (v1_id == v3_id) || (v2_id == v3_id)) {
degenerate_tris_num_++;
return false;
}
if (!tris_.add({v1_id, v2_id, v3_id})) {
duplicate_tris_num_++;
return false;
}
if (use_custom_normals_) {
loop_normals_.append_n_times(data.normal, 3);
}
return true;
}
Mesh *STLMeshHelper::to_mesh()
{
if (degenerate_tris_num_ > 0) {
CLOG_WARN(&LOG, "Removed %d degenerate triangles during import", degenerate_tris_num_);
}
if (duplicate_tris_num_ > 0) {
CLOG_WARN(&LOG, "Removed %d duplicate triangles during import", duplicate_tris_num_);
}
Mesh *mesh = BKE_mesh_new_nomain(verts_.size(), 0, tris_.size(), tris_.size() * 3);
mesh->vert_positions_for_write().copy_from(verts_);
offset_indices::fill_constant_group_size(3, 0, mesh->face_offsets_for_write());
array_utils::copy(tris_.as_span().cast<int>(), mesh->corner_verts_for_write());
bke::mesh_smooth_set(*mesh, false);
/* NOTE: edges must be calculated first before setting custom normals. */
bke::mesh_calc_edges(*mesh, false, false);
if (use_custom_normals_ && loop_normals_.size() == mesh->corners_num) {
bke::mesh_set_custom_normals(*mesh, loop_normals_);
}
return mesh;
}
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup stl
*/
#pragma once
#include <cstdint>
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "stl_data.hh"
namespace blender {
struct Mesh;
namespace io::stl {
class Triangle {
public:
int v1, v2, v3;
/* Based on an old version of Python's frozen-set hash
* https://web.archive.org/web/20220520211017/https://stackoverflow.com/questions/20832279/python-frozenset-hashing-algorithm-implementation
*/
uint64_t hash() const
{
uint64_t res = 1927868237UL;
res *= 4;
res ^= (v1 ^ (v1 << 16) ^ 89869747UL) * 3644798167UL;
res ^= (v2 ^ (v2 << 16) ^ 89869747UL) * 3644798167UL;
res ^= (v3 ^ (v3 << 16) ^ 89869747UL) * 3644798167UL;
return res * 69069U + 907133923UL;
}
friend bool operator==(const Triangle &a, const Triangle &b)
{
bool i = (a.v1 == b.v1) && (a.v2 == b.v2) && (a.v3 == b.v3);
bool j = (a.v1 == b.v1) && (a.v3 == b.v2) && (a.v2 == b.v3);
bool k = (a.v2 == b.v1) && (a.v1 == b.v2) && (a.v3 == b.v3);
bool l = (a.v2 == b.v1) && (a.v3 == b.v2) && (a.v1 == b.v3);
bool m = (a.v3 == b.v1) && (a.v1 == b.v2) && (a.v2 == b.v3);
bool n = (a.v3 == b.v1) && (a.v2 == b.v2) && (a.v1 == b.v3);
return i || j || k || l || m || n;
}
};
class STLMeshHelper {
private:
VectorSet<float3> verts_;
VectorSet<Triangle> tris_;
Vector<float3> loop_normals_;
int degenerate_tris_num_;
int duplicate_tris_num_;
const bool use_custom_normals_;
public:
STLMeshHelper(int tris_num, bool use_custom_normals);
/* Creates a new triangle from specified vertex locations,
* duplicate vertices and triangles are merged.
*/
bool add_triangle(const PackedTriangle &data);
Mesh *to_mesh();
};
} // namespace io::stl
} // namespace blender

View File

@@ -0,0 +1,27 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include <cstdint>
namespace blender::io::stl {
#pragma pack(push, 1)
struct PackedTriangle {
float3 normal;
float3 vertices[3];
uint16_t attribute_byte_count;
};
#pragma pack(pop)
inline constexpr size_t BINARY_HEADER_SIZE = 80;
inline constexpr size_t BINARY_STRIDE = sizeof(PackedTriangle);
static_assert(sizeof(PackedTriangle) == 12 + (12 * 3) + 2,
"PackedTriangle expected size mismatch");
} // namespace blender::io::stl

View File

@@ -0,0 +1,125 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "tests/blendfile_loading_base_test.h"
#include "BKE_appdir.hh"
#include "BLI_fileops.h"
#include "BLI_string.h"
#include "DEG_depsgraph.hh"
#include "MEM_guardedalloc.h"
#include "IO_stl.hh"
#include "stl_export.hh"
namespace blender::io::stl {
/* Set this true to keep comparison-failing test output in temp file directory. */
constexpr bool save_failing_test_output = false;
static std::string read_temp_file_in_string(const std::string &file_path)
{
std::string res;
size_t buffer_len;
char *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len);
if (buffer != nullptr) {
res.assign(buffer, buffer_len);
MEM_delete(buffer);
}
return res;
}
class STLExportTest : public BlendfileLoadingBaseTest {
public:
bool load_file_and_depsgraph(const std::string &filepath)
{
if (!blendfile_load(filepath.c_str())) {
return false;
}
depsgraph_create(DAG_EVAL_VIEWPORT);
return true;
}
protected:
STLExportTest()
{
_params.ascii_format = true;
}
void SetUp() override
{
BlendfileLoadingBaseTest::SetUp();
BKE_tempdir_init(nullptr);
}
void TearDown() override
{
BlendfileLoadingBaseTest::TearDown();
BKE_tempdir_session_purge();
}
static std::string get_temp_filename(const std::string &filename)
{
return std::string(BKE_tempdir_base()) + SEP_STR + filename;
}
/**
* Export the given blend file with the given parameters and
* test to see if it matches a golden file (ignoring any difference in Blender version number).
* \param blendfile: input, relative to "tests" directory.
* \param golden_stl: expected output, relative to "tests" directory.
*/
void compare_to_golden(const std::string &blendfile, const std::string &golden_stl)
{
if (!load_file_and_depsgraph(blendfile)) {
return;
}
std::string out_file_path = get_temp_filename(BLI_path_basename(golden_stl.c_str()));
STRNCPY(_params.filepath, out_file_path.c_str());
std::string golden_file_path = tests::flags_test_asset_dir() + SEP_STR + golden_stl;
export_frame(depsgraph, 1.0f, _params);
std::string output_str = read_temp_file_in_string(out_file_path);
std::string golden_str = read_temp_file_in_string(golden_file_path);
bool are_equal = output_str == golden_str;
if (save_failing_test_output && !are_equal) {
printf("failing test output in %s\n", out_file_path.c_str());
}
ASSERT_TRUE(are_equal);
if (!save_failing_test_output || are_equal) {
BLI_delete(out_file_path.c_str(), false, false);
}
}
STLExportParams _params;
};
TEST_F(STLExportTest, all_tris)
{
compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_tris.blend",
"io_tests" SEP_STR "stl" SEP_STR "all_tris.stl");
}
TEST_F(STLExportTest, all_quads)
{
compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_quads.blend",
"io_tests" SEP_STR "stl" SEP_STR "all_quads.stl");
}
TEST_F(STLExportTest, non_uniform_scale)
{
compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "non_uniform_scale.blend",
"io_tests" SEP_STR "stl" SEP_STR "non_uniform_scale.stl");
}
TEST_F(STLExportTest, cubes_positioned)
{
compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "cubes_positioned.blend",
"io_tests" SEP_STR "stl" SEP_STR "cubes_positioned.stl");
}
} // namespace blender::io::stl