arabago96's picture
Deploy Clean: Wine+SDK+LFS
8abcb4e
#define TINYGLTF_IMPLEMENTATION
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
// STB_IMAGE_WRITE is NOT implemented by tinygltf automatically, we must define
// it.
#include "vendor/tiny_gltf.h"
#include <iostream>
#include <string>
#include <vector>
#include <SketchUpAPI/color.h>
#include <SketchUpAPI/common.h>
#include <SketchUpAPI/geometry.h>
#include <SketchUpAPI/initialize.h>
#include <SketchUpAPI/model/component_instance.h>
#include <SketchUpAPI/model/edge.h>
#include <SketchUpAPI/model/entities.h>
#include <SketchUpAPI/model/face.h>
#include <SketchUpAPI/model/geometry_input.h>
#include <SketchUpAPI/model/group.h>
#include <SketchUpAPI/model/material.h>
#include <SketchUpAPI/model/model.h>
#include <SketchUpAPI/model/texture.h>
#include <SketchUpAPI/model/vertex.h>
// constant for meters to inches conversion
const double kMetersToInches = 39.3701;
// Helper to get data from accessor
template <typename T>
std::vector<T> GetData(const tinygltf::Model &model,
const tinygltf::Accessor &accessor) {
if (accessor.bufferView < 0) {
std::cerr << "GetData: ERROR - accessor " << accessor.name
<< " has no bufferView (index=" << accessor.bufferView << ")"
<< std::endl;
return std::vector<T>();
}
std::cout << "GetData: accessor=" << accessor.bufferView
<< " count=" << accessor.count << " stride="
<< accessor.ByteStride(model.bufferViews[accessor.bufferView])
<< std::endl;
const tinygltf::BufferView &bufferView =
model.bufferViews[accessor.bufferView];
const tinygltf::Buffer &buffer = model.buffers[bufferView.buffer];
// Check bounds
if (bufferView.byteOffset + accessor.byteOffset + accessor.count * sizeof(T) >
buffer.data.size()) {
// Only strict if stride is tight, but rough check
std::cerr << "WARNING: Accessor points outside buffer!" << std::endl;
}
const unsigned char *data =
buffer.data.data() + bufferView.byteOffset + accessor.byteOffset;
std::vector<T> output;
output.resize(accessor.count);
// Stride check
int stride = accessor.ByteStride(bufferView);
if (stride == sizeof(T)) {
memcpy(output.data(), data, accessor.count * sizeof(T));
} else {
for (size_t i = 0; i < accessor.count; ++i) {
memcpy(&output[i], data + i * stride, sizeof(T));
}
}
return output;
}
// Simple struct for vec2 and vec3
struct Vec2 {
float u, v;
};
struct Vec3 {
float x, y, z;
};
// Global stats for debug output (declared before CreateMaterialFromGLTF)
int g_totalFaces = 0;
int g_texturedFaces = 0;
int g_totalVertices = 0;
size_t g_totalTextureBytes = 0;
int g_textureWidth = 0;
int g_textureHeight = 0;
// Helper to create SketchUp material from GLTF material
SUMaterialRef
CreateMaterialFromGLTF(const tinygltf::Model &model, int materialIndex,
SUModelRef su_model,
std::vector<SUMaterialRef> &created_materials) {
if (materialIndex < 0 || materialIndex >= model.materials.size())
return SU_INVALID;
if (SUIsValid(created_materials[materialIndex]))
return created_materials[materialIndex];
const tinygltf::Material &gltf_mat = model.materials[materialIndex];
SUMaterialRef mat = SU_INVALID;
SUMaterialCreate(&mat);
// Set Name
std::string matName = gltf_mat.name;
if (matName.empty()) {
matName = "Material_" + std::to_string(materialIndex);
}
SUMaterialSetName(mat, matName.c_str());
// Set Color
if (gltf_mat.pbrMetallicRoughness.baseColorFactor.size() == 4) {
SUColor color;
color.red =
(SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[0] * 255);
color.green =
(SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[1] * 255);
color.blue =
(SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[2] * 255);
color.alpha =
(SUByte)(gltf_mat.pbrMetallicRoughness.baseColorFactor[3] * 255);
SUMaterialSetColor(mat, &color);
if (color.alpha < 255) {
SUMaterialSetUseOpacity(mat, true);
SUMaterialSetOpacity(mat,
gltf_mat.pbrMetallicRoughness.baseColorFactor[3]);
}
}
// Set Texture
int texIndex = gltf_mat.pbrMetallicRoughness.baseColorTexture.index;
if (texIndex >= 0 && texIndex < model.textures.size()) {
int imgIndex = model.textures[texIndex].source;
if (imgIndex >= 0 && imgIndex < model.images.size()) {
const tinygltf::Image &image = model.images[imgIndex];
// Check if image data is loaded
if (image.width > 0 && image.height > 0 && !image.image.empty()) {
// Track texture stats
g_textureWidth = image.width;
g_textureHeight = image.height;
// g_totalTextureBytes calculated after writing file
// Write to temp file
std::string tempFileName;
bool isJpeg =
(image.mimeType == "image/jpeg" || image.mimeType == "image/jpg");
if (isJpeg) {
tempFileName = "temp_" + std::to_string(imgIndex) + ".jpg";
// Convert RGBA to RGB if needed
if (image.component == 4) {
std::vector<unsigned char> rgb_data(image.width * image.height * 3);
for (size_t i = 0; i < image.width * image.height; ++i) {
rgb_data[i * 3 + 0] = image.image[i * 4 + 0];
rgb_data[i * 3 + 1] = image.image[i * 4 + 1];
rgb_data[i * 3 + 2] = image.image[i * 4 + 2];
}
stbi_write_jpg(tempFileName.c_str(), image.width, image.height, 3,
rgb_data.data(), 85);
} else {
stbi_write_jpg(tempFileName.c_str(), image.width, image.height,
image.component, image.image.data(), 85);
}
} else {
tempFileName = "temp_" + std::to_string(imgIndex) + ".png";
stbi_write_png(tempFileName.c_str(), image.width, image.height,
image.component, image.image.data(),
image.width * image.component);
}
// ACCURATE SIZE CALCULATION:
// Read the size of the generated file (JPEG/PNG) instead of the raw
// pixel buffer because SketchUp embeds the compressed file.
std::ifstream in(tempFileName,
std::ifstream::ate | std::ifstream::binary);
if (in.is_open()) {
g_totalTextureBytes += in.tellg();
}
SUTextureRef texture = SU_INVALID;
SUResult texRes =
SUTextureCreateFromFile(&texture, tempFileName.c_str(), 1.0, 1.0);
if (texRes == SU_ERROR_NONE) {
SUMaterialSetTexture(mat, texture);
} else {
std::cerr << "Failed to create texture from file: " << tempFileName
<< std::endl;
}
}
}
}
// Add material to model
SUModelAddMaterials(su_model, 1, &mat);
created_materials[materialIndex] = mat;
return mat;
}
void ProcessNode(const tinygltf::Model &model, const tinygltf::Node &node,
SUEntitiesRef entities, SUModelRef su_model,
std::vector<SUMaterialRef> &created_materials) {
if (node.mesh >= 0) {
if (node.mesh >= model.meshes.size()) {
std::cerr << "Error: Mesh index out of bounds: " << node.mesh
<< std::endl;
return;
}
const tinygltf::Mesh &mesh = model.meshes[node.mesh];
std::cout << "Processing Mesh: " << mesh.name << std::endl;
for (const auto &primitive : mesh.primitives) {
SUGeometryInputRef input = SU_INVALID;
SUGeometryInputCreate(&input);
// Vertices
std::vector<SUPoint3D> su_vertices;
if (primitive.attributes.find("POSITION") != primitive.attributes.end()) {
const tinygltf::Accessor &accessor =
model.accessors[primitive.attributes.at("POSITION")];
std::vector<Vec3> positions = GetData<Vec3>(model, accessor);
if (positions.empty()) {
std::cerr << "Error: No vertices retrieved for mesh " << mesh.name
<< std::endl;
SUGeometryInputRelease(&input);
continue;
}
for (const auto &p : positions) {
SUPoint3D pt;
// GLB uses Y-up coordinate system
// SketchUp uses Z-up coordinate system
// Transformation: GLB(x,y,z) -> SKU(x, -z, y)
// X stays X, GLB Y (up) becomes SKU Z (up), GLB Z (forward) becomes
// SKU -Y (forward)
pt.x = p.x * kMetersToInches;
pt.y = -p.z * kMetersToInches; // GLB Z (forward) becomes SKU -Y
pt.z =
p.y * kMetersToInches; // GLB Y (up) becomes SKU Z (NO negation)
su_vertices.push_back(pt);
}
g_totalVertices += su_vertices.size();
SUGeometryInputSetVertices(input, su_vertices.size(),
su_vertices.data());
}
// UVs
std::vector<SUPoint2D> su_uvs;
bool hasUVs = false;
if (primitive.attributes.find("TEXCOORD_0") !=
primitive.attributes.end()) {
const tinygltf::Accessor &accessor =
model.accessors[primitive.attributes.at("TEXCOORD_0")];
std::vector<Vec2> uvs = GetData<Vec2>(model, accessor);
for (const auto &uv : uvs) {
SUPoint2D pt;
pt.x = uv.u;
pt.y = 1.0f - uv.v; // Flip V for SketchUp
su_uvs.push_back(pt);
}
hasUVs = true;
}
std::cout << " - Accessor: " << primitive.indices
<< " Mode: " << primitive.mode << std::endl;
if (primitive.mode != TINYGLTF_MODE_TRIANGLES) {
std::cerr
<< "WARNING: Primitive mode " << primitive.mode
<< " is not TRIANGLES (4). SketchUp converter expects TRIANGLES."
<< std::endl;
}
// Indices (Faces)
std::vector<size_t> indices;
if (primitive.indices >= 0) {
const tinygltf::Accessor &indexAccessor =
model.accessors[primitive.indices];
if (indexAccessor.componentType ==
TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) {
std::vector<unsigned short> raw =
GetData<unsigned short>(model, indexAccessor);
for (auto v : raw)
indices.push_back(v);
} else if (indexAccessor.componentType ==
TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) {
std::vector<unsigned int> raw =
GetData<unsigned int>(model, indexAccessor);
for (auto v : raw)
indices.push_back(v);
} else if (indexAccessor.componentType ==
TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) {
std::vector<unsigned char> raw =
GetData<unsigned char>(model, indexAccessor);
for (auto v : raw)
indices.push_back(v);
}
} else {
// Handle non-indexed geometry (just vertices)
// Generate sequential indices: 0, 1, 2, ... based on vertex count
size_t vertexCount = su_vertices.size();
for (size_t i = 0; i < vertexCount; i++) {
indices.push_back(i);
}
}
// Validate indices are within range
size_t maxIndex = su_vertices.size();
bool validIndices = true;
for (size_t idx : indices) {
if (idx >= maxIndex) {
std::cerr << "Warning: Index " << idx << " out of range (max "
<< maxIndex << ")" << std::endl;
validIndices = false;
break;
}
}
if (!validIndices || indices.empty()) {
std::cerr << "Skipping primitive due to invalid indices" << std::endl;
SUGeometryInputRelease(&input);
continue;
}
// Material
SUMaterialRef material = SU_INVALID;
if (primitive.material >= 0) {
material = CreateMaterialFromGLTF(model, primitive.material, su_model,
created_materials);
}
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
size_t i0 = indices[i];
size_t i1 = indices[i + 1];
size_t i2 = indices[i + 2];
// Skip degenerate triangles (same vertex referenced multiple times)
if (i0 == i1 || i1 == i2 || i0 == i2) {
continue;
}
SULoopInputRef loop = SU_INVALID;
SULoopInputCreate(&loop);
// Add vertex indices for the triangle
SULoopInputAddVertexIndex(loop, i0);
SULoopInputAddVertexIndex(loop, i1);
SULoopInputAddVertexIndex(loop, i2);
// Set all edges as SOFT + SMOOTH so the model looks like smooth-shaded
// GLB Edge indices are 0, 1, 2 for the three edges of the triangle This
// makes the model render with smooth shading rather than faceted/boxy
SULoopInputEdgeSetSoft(loop, 0, true);
SULoopInputEdgeSetSmooth(loop, 0, true);
SULoopInputEdgeSetSoft(loop, 1, true);
SULoopInputEdgeSetSmooth(loop, 1, true);
SULoopInputEdgeSetSoft(loop, 2, true);
SULoopInputEdgeSetSmooth(loop, 2, true);
size_t face_index;
SUResult addResult = SUGeometryInputAddFace(input, &loop, &face_index);
if (addResult != SU_ERROR_NONE) {
// Face creation failed, skip
continue;
}
g_totalFaces++;
// Set Material and UVs
if (SUIsValid(material)) {
SUMaterialInput mat_input;
mat_input.material = material;
mat_input.num_uv_coords = 0;
if (hasUVs && i0 < su_uvs.size() && i1 < su_uvs.size() &&
i2 < su_uvs.size()) {
mat_input.num_uv_coords = 3;
mat_input.uv_coords[0] = su_uvs[i0];
mat_input.uv_coords[1] = su_uvs[i1];
mat_input.uv_coords[2] = su_uvs[i2];
mat_input.vertex_indices[0] = i0;
mat_input.vertex_indices[1] = i1;
mat_input.vertex_indices[2] = i2;
}
SUGeometryInputFaceSetFrontMaterial(input, face_index, &mat_input);
SUGeometryInputFaceSetBackMaterial(input, face_index, &mat_input);
g_texturedFaces++;
} else if (hasUVs) {
// Even without a material, we might want to apply UVs?
// SketchUp usually ties UVs to a material.
// If no material, no texture to map. So skipping.
}
}
// Fill entities (per primitive) - INSIDE the primitive loop
SUEntitiesFill(entities, input, true);
SUGeometryInputRelease(&input);
}
}
// Children - INSIDE ProcessNode function
for (int childIndex : node.children) {
ProcessNode(model, model.nodes[childIndex], entities, su_model,
created_materials);
}
}
int main(int argc, char **argv) {
if (argc < 3) {
std::cerr << "Usage: " << argv[0] << " input.glb output.skp" << std::endl;
return 1;
}
SUInitialize();
std::string inputPath = argv[1];
std::string outputPath = argv[2];
tinygltf::Model model;
tinygltf::TinyGLTF loader;
// Set tolerent image loader
loader.SetImageLoader(
[](tinygltf::Image *image, const int image_idx, std::string *err,
std::string *warn, int req_width, int req_height,
const unsigned char *bytes, int size, void *user_data) {
bool res =
tinygltf::LoadImageData(image, image_idx, err, warn, req_width,
req_height, bytes, size, user_data);
if (!res) {
// If default loader fails, consume the error and return true to
// proceed without this image
if (warn) {
*warn += "Failed to load image: " + (err ? *err : "Unknown error") +
". Skipping.\n";
}
if (err) {
*err = ""; // Clear error
}
return true;
}
return true;
},
nullptr);
std::string err;
std::string warn;
bool ret = loader.LoadBinaryFromFile(&model, &err, &warn, inputPath);
if (!warn.empty()) {
std::cerr << "Warn: " << warn << std::endl;
}
if (!err.empty()) {
std::cerr << "Err: " << err << std::endl;
}
if (!ret) {
std::cerr << "Failed to load GLTF" << std::endl;
return 1;
}
SUModelRef su_model = SU_INVALID;
SUResult res = SUModelCreate(&su_model);
if (res != SU_ERROR_NONE)
return 1;
// Get model's root entities first (we need it to add the group)
SUEntitiesRef model_entities = SU_INVALID;
SUModelGetEntities(su_model, &model_entities);
// Create a group to hold all geometry (makes it a single unit in SketchUp)
SUGroupRef group = SU_INVALID;
SUGroupCreate(&group);
// Add the group to the model FIRST (required before filling its entities)
SUEntitiesAddGroup(model_entities, group);
// Now get the group's internal entities to add geometry
SUEntitiesRef group_entities = SU_INVALID;
SUGroupGetEntities(group, &group_entities);
// Cache to store created SU materials to reuse them
std::vector<SUMaterialRef> created_materials(model.materials.size(),
SU_INVALID);
// Iterate over scenes
// Iterate over scenes
int sceneindex = (model.defaultScene >= 0) ? model.defaultScene : 0;
if (sceneindex >= 0 && sceneindex < model.scenes.size()) {
const tinygltf::Scene &scene = model.scenes[sceneindex];
for (size_t i = 0; i < scene.nodes.size(); i++) {
ProcessNode(model, model.nodes[scene.nodes[i]], model_entities, su_model,
created_materials);
}
} else {
// If no valid scene found, try processing all nodes (fallback for flat
// GLBs)
for (size_t i = 0; i < model.nodes.size(); i++) {
ProcessNode(model, model.nodes[i], model_entities, su_model,
created_materials);
}
}
// POST-PROCESSING: Soften ALL edges to make the model
// appear smooth
// This is crucial for imported meshes - it hides all triangle edges
// and makes SketchUp render the model with smooth shading
size_t edge_count = 0;
SUEntitiesGetNumEdges(group_entities, false, &edge_count);
if (edge_count > 0) {
std::vector<SUEdgeRef> edges(edge_count);
size_t actual_count = 0;
SUEntitiesGetEdges(group_entities, false, edge_count, edges.data(),
&actual_count);
// Soften and smooth ALL edges - this makes curved surfaces look smooth
for (size_t i = 0; i < actual_count; i++) {
SUEdgeSetSoft(edges[i], true);
SUEdgeSetSmooth(edges[i], true);
}
std::cerr << "Softened " << actual_count << " edges for smooth appearance"
<< std::endl;
}
// Lock the group to prevent accidental exploding
SUComponentInstanceRef group_instance = SUGroupToComponentInstance(group);
if (group_instance.ptr !=
0) { // Check if valid (though SUGroupRef is basically a subclass)
SUComponentInstanceSetLocked(group_instance, true);
}
res = SUModelSaveToFile(su_model, outputPath.c_str());
if (res != SU_ERROR_NONE) {
std::cerr << "Failed to save SKP file" << std::endl;
} else {
std::cout << "Saved " << outputPath << std::endl;
}
// Cleanup images (optional, if we track temp files)
SUModelRelease(&su_model);
SUTerminate();
// Calculate size breakdown estimates (in bytes)
// Geometry: ~24 bytes per vertex (3x float position + 3x float normal + 2x
// float UV) Face index: ~12 bytes per face (3x int32 indices)
size_t estimatedGeometryBytes = (g_totalVertices * 24) + (g_totalFaces * 12);
// Calculate SKP Overhead
std::ifstream f(outputPath, std::ifstream::ate | std::ifstream::binary);
size_t actualFileSize = f.tellg();
f.close();
long long overhead = (long long)actualFileSize -
(long long)estimatedGeometryBytes -
(long long)g_totalTextureBytes;
if (overhead < 0)
overhead = 0; // Should not happen unless estimates are way off
// Output JSON stats for frontend to parse
std::cout << "{" << std::endl;
std::cout << " \"status\": \"success\"," << std::endl;
std::cout << " \"vertices\": " << g_totalVertices << "," << std::endl;
std::cout << " \"faces\": " << g_totalFaces << "," << std::endl;
std::cout << " \"textured_faces\": " << g_texturedFaces << "," << std::endl;
std::cout << " \"materials\": " << model.materials.size() << ","
<< std::endl;
std::cout << " \"textures\": " << model.textures.size() << "," << std::endl;
std::cout << " \"texture_width\": " << g_textureWidth << "," << std::endl;
std::cout << " \"texture_height\": " << g_textureHeight << "," << std::endl;
std::cout << " \"texture_bytes\": " << g_totalTextureBytes << ","
<< std::endl;
std::cout << " \"estimated_geometry_bytes\": " << estimatedGeometryBytes
<< "," << std::endl;
std::cout << " \"skp_overhead_bytes\": " << overhead << std::endl;
std::cout << "}" << std::endl;
return 0;
}