mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
BBL Port Color Mix Base
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
#include "../libslic3r.h"
|
||||
#include "../Model.hpp"
|
||||
#include "../TriangleMesh.hpp"
|
||||
#include "../TexturePainting.hpp"
|
||||
#include "ResourcePathUtils.hpp"
|
||||
|
||||
#include "OBJ.hpp"
|
||||
#include "objparser.hpp"
|
||||
@@ -21,7 +23,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message)
|
||||
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
|
||||
{
|
||||
if (meshptr == nullptr)
|
||||
return false;
|
||||
@@ -98,6 +100,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
its.indices.reserve(num_faces + num_quads);
|
||||
if (exist_mtl) {
|
||||
obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1;
|
||||
obj_info.usemtls = data.usemtls;
|
||||
obj_info.face_colors.reserve(num_faces + num_quads);
|
||||
}
|
||||
bool has_color = data.has_vertex_color;
|
||||
@@ -210,14 +213,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
||||
}
|
||||
if (meshptr->volume() < 0)
|
||||
meshptr->flip_triangles();
|
||||
// Hand the parsed material table back so callers can build a TexturedMesh from it.
|
||||
if (out_mtl)
|
||||
*out_mtl = mtl_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in)
|
||||
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
|
||||
{
|
||||
TriangleMesh mesh;
|
||||
|
||||
bool ret = load_obj(path, &mesh, obj_info, message);
|
||||
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
|
||||
|
||||
if (ret) {
|
||||
std::string object_name;
|
||||
@@ -232,6 +238,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool obj_to_textured_mesh(
|
||||
const ObjInfo& obj_info,
|
||||
const indexed_triangle_set& its,
|
||||
const ObjParser::MtlData& mtl_data,
|
||||
const std::string& obj_directory,
|
||||
TexturedMesh& out)
|
||||
{
|
||||
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
|
||||
return false;
|
||||
|
||||
const size_t nv = its.vertices.size();
|
||||
const size_t nf = its.indices.size();
|
||||
|
||||
// 1. Copy vertices
|
||||
out.vertices.resize(nv);
|
||||
for (size_t i = 0; i < nv; ++i)
|
||||
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
|
||||
|
||||
// 2. Copy face indices
|
||||
out.indices.resize(nf);
|
||||
for (size_t i = 0; i < nf; ++i)
|
||||
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
|
||||
|
||||
// 3. Build per-face UV (uv_coords + uv_indices)
|
||||
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
|
||||
// Flip V here so downstream code works uniformly.
|
||||
if (!obj_info.uvs.empty()) {
|
||||
const size_t uv_face_count = obj_info.uvs.size();
|
||||
out.uv_coords.resize(uv_face_count * 3);
|
||||
out.uv_indices.resize(nf);
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
if (fi < uv_face_count) {
|
||||
int base = static_cast<int>(fi * 3);
|
||||
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
|
||||
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
|
||||
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
|
||||
out.uv_indices[fi] = {base, base + 1, base + 2};
|
||||
} else {
|
||||
out.uv_indices[fi] = {0, 0, 0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Build material list and load textures from disk
|
||||
// Map: material name -> material index
|
||||
std::map<std::string, int> mtl_name_to_idx;
|
||||
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
|
||||
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
|
||||
|
||||
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
|
||||
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
|
||||
out.material_texture_map.resize(num_materials, -1);
|
||||
|
||||
// Map: texture filename -> index in out.textures
|
||||
std::map<std::string, int> png_to_tex_idx;
|
||||
|
||||
for (int mi = 0; mi < num_materials; ++mi) {
|
||||
const std::string& name = mtl_data.mtl_orders[mi];
|
||||
auto it = mtl_data.new_mtl_unmap.find(name);
|
||||
if (it == mtl_data.new_mtl_unmap.end())
|
||||
continue;
|
||||
const auto& mtl = *(it->second);
|
||||
|
||||
// Material color from Kd
|
||||
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
|
||||
|
||||
// Texture from map_Kd
|
||||
if (mtl.map_Kd.empty())
|
||||
continue;
|
||||
|
||||
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
|
||||
if (tex_it != png_to_tex_idx.end()) {
|
||||
out.material_texture_map[mi] = tex_it->second;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve texture file path.
|
||||
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
|
||||
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
|
||||
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
|
||||
resource_path::resolve_existing_relative_path_case_insensitive(
|
||||
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
|
||||
|
||||
if (tex_path.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read raw file bytes
|
||||
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open())
|
||||
continue;
|
||||
auto file_size = file.tellg();
|
||||
if (file_size <= 0)
|
||||
continue;
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
TextureImage ti;
|
||||
ti.data.resize(static_cast<size_t>(file_size));
|
||||
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
|
||||
ti.width = -1;
|
||||
ti.height = -1;
|
||||
ti.channels = 0;
|
||||
|
||||
int new_idx = static_cast<int>(out.textures.size());
|
||||
out.textures.push_back(std::move(ti));
|
||||
png_to_tex_idx[mtl.map_Kd] = new_idx;
|
||||
out.material_texture_map[mi] = new_idx;
|
||||
}
|
||||
|
||||
// 5. Build per-face material_ids from usemtls ranges
|
||||
out.material_ids.resize(nf, -1);
|
||||
if (!obj_info.usemtls.empty()) {
|
||||
for (size_t fi = 0; fi < nf; ++fi) {
|
||||
int face_idx = static_cast<int>(fi);
|
||||
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
|
||||
const auto& um = obj_info.usemtls[k];
|
||||
if (face_idx >= um.face_start && face_idx <= um.face_end) {
|
||||
auto name_it = mtl_name_to_idx.find(um.name);
|
||||
if (name_it != mtl_name_to_idx.end())
|
||||
out.material_ids[fi] = name_it->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (out.textures.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
|
||||
<< out.textures.size() << " textures, "
|
||||
<< num_materials << " materials";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool store_obj(const char *path, TriangleMesh *mesh)
|
||||
{
|
||||
//FIXME returning false even if write failed.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_Format_OBJ_hpp_
|
||||
#define slic3r_Format_OBJ_hpp_
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "objparser.hpp"
|
||||
#include <unordered_map>
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -18,6 +19,7 @@ struct ObjInfo {
|
||||
std::map<std::string,bool> pngs;
|
||||
std::unordered_map<int, std::string> uv_map_pngs;
|
||||
bool has_uv_png{false};
|
||||
std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import
|
||||
|
||||
};
|
||||
struct ObjDialogInOut
|
||||
@@ -32,8 +34,18 @@ struct ObjDialogInOut
|
||||
std::string lost_material_name{""};
|
||||
};
|
||||
typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn;
|
||||
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message);
|
||||
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr);
|
||||
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr);
|
||||
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr);
|
||||
|
||||
struct TexturedMesh;
|
||||
// Build a TexturedMesh (vertices + per-face UVs + decoded texture images) from a parsed OBJ
|
||||
// plus its material table, so the texture-to-color importer can sample face colours.
|
||||
extern bool obj_to_textured_mesh(
|
||||
const ObjInfo& obj_info,
|
||||
const indexed_triangle_set& its,
|
||||
const ObjParser::MtlData& mtl_data,
|
||||
const std::string& obj_directory,
|
||||
TexturedMesh& out);
|
||||
|
||||
extern bool store_obj(const char *path, TriangleMesh *mesh);
|
||||
extern bool store_obj(const char *path, ModelObject *model);
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#ifndef slic3r_Format_ResourcePathUtils_hpp_
|
||||
#define slic3r_Format_ResourcePathUtils_hpp_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace resource_path {
|
||||
|
||||
inline std::string ascii_lower_copy(const std::string& value)
|
||||
{
|
||||
std::string lowered;
|
||||
lowered.reserve(value.size());
|
||||
for (unsigned char ch : value)
|
||||
lowered.push_back(static_cast<char>(std::tolower(ch)));
|
||||
return lowered;
|
||||
}
|
||||
|
||||
inline boost::filesystem::path portable_path_copy(const boost::filesystem::path& value)
|
||||
{
|
||||
std::string portable = value.string();
|
||||
std::replace(portable.begin(), portable.end(), '\\', '/');
|
||||
return boost::filesystem::path(portable);
|
||||
}
|
||||
|
||||
inline int hex_digit_value(char ch)
|
||||
{
|
||||
if (ch >= '0' && ch <= '9') return ch - '0';
|
||||
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
|
||||
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Byte-level percent decoding. Per RFC 3986 the %XX byte stream is expected to be
|
||||
// UTF-8 when produced from URIs / Assimp aiString; this function performs no
|
||||
// transcoding, so callers must treat both input and output as raw UTF-8 bytes.
|
||||
inline std::string percent_decode_copy(const std::string& value)
|
||||
{
|
||||
std::string decoded;
|
||||
decoded.reserve(value.size());
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (value[i] == '%' && i + 2 < value.size()) {
|
||||
const int hi = hex_digit_value(value[i + 1]);
|
||||
const int lo = hex_digit_value(value[i + 2]);
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
decoded.push_back(static_cast<char>((hi << 4) | lo));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded.push_back(value[i]);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
inline std::string strip_file_uri_prefix_copy(const std::string& value)
|
||||
{
|
||||
const std::string lower = ascii_lower_copy(value);
|
||||
if (lower.rfind("file://", 0) != 0)
|
||||
return value;
|
||||
|
||||
std::string path = value.substr(7);
|
||||
if (ascii_lower_copy(path).rfind("localhost/", 0) == 0)
|
||||
path.erase(0, std::string("localhost").size());
|
||||
else if (!path.empty() && path.front() != '/')
|
||||
path = "//" + path;
|
||||
|
||||
// file:///C:/... should become C:/..., while file:///tmp/... keeps /tmp/...
|
||||
if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':')
|
||||
path.erase(path.begin());
|
||||
return path;
|
||||
}
|
||||
|
||||
inline bool file_uri_has_remote_authority(const std::string& value)
|
||||
{
|
||||
const std::string lower = ascii_lower_copy(value);
|
||||
if (lower.rfind("file://", 0) != 0)
|
||||
return false;
|
||||
|
||||
const std::string path = value.substr(7);
|
||||
if (path.empty() || path.front() == '/')
|
||||
return false;
|
||||
|
||||
const std::size_t slash = path.find('/');
|
||||
const std::string authority = path.substr(0, slash);
|
||||
return ascii_lower_copy(authority) != "localhost";
|
||||
}
|
||||
|
||||
inline bool looks_like_windows_absolute_path(const boost::filesystem::path& path)
|
||||
{
|
||||
const std::string portable = portable_path_copy(path).string();
|
||||
return portable.size() >= 3
|
||||
&& std::isalpha(static_cast<unsigned char>(portable[0]))
|
||||
&& portable[1] == ':'
|
||||
&& portable[2] == '/';
|
||||
}
|
||||
|
||||
inline boost::filesystem::path filename_from_portable_path(const boost::filesystem::path& value)
|
||||
{
|
||||
const boost::filesystem::path portable = portable_path_copy(value);
|
||||
return portable.filename();
|
||||
}
|
||||
|
||||
inline boost::filesystem::path find_child_case_insensitive(
|
||||
const boost::filesystem::path& directory,
|
||||
const boost::filesystem::path& requested_name,
|
||||
const char* context)
|
||||
{
|
||||
if (!boost::filesystem::exists(directory) || !boost::filesystem::is_directory(directory))
|
||||
return {};
|
||||
|
||||
const std::string requested_lower = ascii_lower_copy(requested_name.filename().string());
|
||||
std::vector<boost::filesystem::path> matches;
|
||||
|
||||
boost::system::error_code ec;
|
||||
for (boost::filesystem::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) {
|
||||
if (ascii_lower_copy(it->path().filename().string()) == requested_lower)
|
||||
matches.push_back(it->path());
|
||||
}
|
||||
|
||||
if (matches.size() == 1)
|
||||
return matches.front();
|
||||
|
||||
if (matches.size() > 1) {
|
||||
BOOST_LOG_TRIVIAL(warning) << context << ": ambiguous case-insensitive resource match for "
|
||||
<< requested_name << " in " << directory;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
inline boost::filesystem::path resolve_existing_path_case_insensitive(
|
||||
const boost::filesystem::path& requested_path,
|
||||
const char* context = "resource_path")
|
||||
{
|
||||
const boost::filesystem::path normalized_path = portable_path_copy(requested_path);
|
||||
|
||||
if (normalized_path.empty())
|
||||
return {};
|
||||
|
||||
if (boost::filesystem::exists(normalized_path))
|
||||
return normalized_path;
|
||||
|
||||
boost::filesystem::path current;
|
||||
bool initialized = false;
|
||||
|
||||
for (const boost::filesystem::path& part : normalized_path) {
|
||||
if (part == normalized_path.root_name() || part == normalized_path.root_directory()) {
|
||||
current /= part;
|
||||
initialized = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
current = boost::filesystem::current_path();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
boost::filesystem::path exact = current / part;
|
||||
if (boost::filesystem::exists(exact)) {
|
||||
current = exact;
|
||||
continue;
|
||||
}
|
||||
|
||||
boost::filesystem::path matched = find_child_case_insensitive(current, part, context);
|
||||
if (matched.empty())
|
||||
return {};
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource path case-insensitively from "
|
||||
<< exact << " to " << matched;
|
||||
current = matched;
|
||||
}
|
||||
|
||||
return boost::filesystem::exists(current) ? current : boost::filesystem::path();
|
||||
}
|
||||
|
||||
inline boost::filesystem::path resolve_existing_relative_path_case_insensitive(
|
||||
const boost::filesystem::path& base_dir,
|
||||
const boost::filesystem::path& resource_path,
|
||||
const char* context = "resource_path")
|
||||
{
|
||||
const boost::filesystem::path requested = resource_path.is_absolute() ? resource_path : base_dir / resource_path;
|
||||
return resolve_existing_path_case_insensitive(requested, context);
|
||||
}
|
||||
|
||||
// Resolve a resource path that originated outside our own code (e.g. a glTF/FBX
|
||||
// material texture reference or a file:// URI inside a 3MF descriptor).
|
||||
//
|
||||
// `raw_path` is expected to be UTF-8 regardless of host platform: file URIs are
|
||||
// UTF-8 by spec, and Assimp aiString uses UTF-8 internally. Cross-platform
|
||||
// correctness on Windows additionally relies on the process having called
|
||||
// boost::nowide::nowide_filesystem() during startup (see src/BambuStudio.cpp),
|
||||
// which imbues boost::filesystem::path with a UTF-8 codecvt so that
|
||||
// `path(std::string)` constructs from UTF-8 byte sequences. Callers that bypass
|
||||
// the main entry point (standalone CLI tools, unit tests) must reproduce that
|
||||
// setup themselves before invoking this helper.
|
||||
inline boost::filesystem::path resolve_external_resource_path(
|
||||
const boost::filesystem::path& base_dir,
|
||||
const std::string& raw_path,
|
||||
const char* context = "resource_path",
|
||||
bool allow_basename_fallback = true)
|
||||
{
|
||||
if (raw_path.empty())
|
||||
return {};
|
||||
|
||||
const bool remote_file_uri = file_uri_has_remote_authority(raw_path);
|
||||
const std::string decoded_path = percent_decode_copy(strip_file_uri_prefix_copy(raw_path));
|
||||
const boost::filesystem::path requested = portable_path_copy(boost::filesystem::path(decoded_path));
|
||||
|
||||
boost::filesystem::path resolved = (requested.is_absolute() || looks_like_windows_absolute_path(requested)) ?
|
||||
resolve_existing_path_case_insensitive(requested, context) :
|
||||
resolve_existing_relative_path_case_insensitive(base_dir, requested, context);
|
||||
if (!resolved.empty())
|
||||
return resolved;
|
||||
|
||||
if (!allow_basename_fallback || remote_file_uri)
|
||||
return {};
|
||||
|
||||
const boost::filesystem::path basename = filename_from_portable_path(requested);
|
||||
if (basename.empty())
|
||||
return {};
|
||||
|
||||
resolved = resolve_existing_relative_path_case_insensitive(base_dir, basename, context);
|
||||
if (!resolved.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource by basename from "
|
||||
<< requested << " to " << resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace resource_path
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_Format_ResourcePathUtils_hpp_ */
|
||||
@@ -394,6 +394,7 @@ static bool mtl_parseline(const char *line, MtlData &data)
|
||||
ObjNewMtl new_mtl;
|
||||
cur_mtl_name = line;
|
||||
data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>();
|
||||
data.mtl_orders.emplace_back(cur_mtl_name);
|
||||
break;
|
||||
}
|
||||
case 'm': {
|
||||
|
||||
@@ -122,6 +122,9 @@ struct MtlData
|
||||
// Version of the data structure for load / store in the private binary format.
|
||||
int version;
|
||||
std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap;
|
||||
// Material names in declaration order. new_mtl_unmap is unordered, but OBJ material
|
||||
// indices are positional, so texture import needs the original order.
|
||||
std::vector<std::string> mtl_orders;
|
||||
};
|
||||
extern bool objparse(const char *path, ObjData &data);
|
||||
extern bool mtlparse(const char *path, MtlData &data);
|
||||
|
||||
Reference in New Issue
Block a user