Compare commits

...
3 Commits
17 changed files with 6842 additions and 0 deletions
+1
View File
@@ -34,6 +34,7 @@ model/gltf gltf glb
model/jt jt
model/obj obj
model/ply ply
model/vnd.freecad fcstd FCStd
model/shp shp
model/step stp step
model/stl stl
+1
View File
@@ -189,6 +189,7 @@ brlcad_add_test(NAME bu_file COMMAND bu_test test_file)
# NOTE: these test numbers correspond with the bu_mime_t enum values in include/bu/mime.h
brlcad_add_test(NAME bu_file_mime_dxf COMMAND bu_test test_file_mime dxf 5 BU_MIME_MODEL_DXF) # tests dxf extension in model context
brlcad_add_test(NAME bu_file_mime_fcstd COMMAND bu_test test_file_mime fcstd 5 BU_MIME_MODEL_VND_FREECAD) # tests fcstd extension in model context
brlcad_add_test(NAME bu_file_mime_stp COMMAND bu_test test_file_mime stp 5 BU_MIME_MODEL_STEP) # tests stp extension in model context
brlcad_add_test(NAME bu_file_mime_png COMMAND bu_test test_file_mime png 3 BU_MIME_IMAGE_PNG) # tests png extension in image context
+1
View File
@@ -23,6 +23,7 @@ add_subdirectory(asc)
add_subdirectory(assetimport)
add_subdirectory(decimate)
add_subdirectory(fastgen4)
add_subdirectory(freecad)
add_subdirectory(gdal)
add_subdirectory(gltf)
add_subdirectory(iges)
+161
View File
@@ -0,0 +1,161 @@
# FreeCAD FCStd importer/exporter plugin for libgcv.
#
# This plugin intentionally does not bundle OpenCASCADE. It is compiled only
# when an external OpenCASCADE package, BRL-CAD's openNURBS, and zlib are
# available. FCStd ZIP archive I/O uses the local zlib-backed helper in this
# directory.
set(FREECAD_SRCS freecad_read.cpp freecad_write.cpp)
set(FREECAD_EXTRA_SRCS
fcstd_zip.cpp
fcstd_zip.h
open2open/include/open2open/brep_convert.h
open2open/include/open2open/fcstd_convert.h
open2open/include/open2open/geom_convert.h
open2open/include/open2open/mesh_convert.h
open2open/src/brep_convert.cpp
open2open/src/fcstd_convert.cpp
open2open/src/geom_convert.cpp
open2open/src/mesh_convert.cpp
)
set(_OPEN2OPEN_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/open2open")
if(NOT EXISTS "${_OPEN2OPEN_SOURCE_DIR}/include/open2open/fcstd_convert.h" OR NOT EXISTS "${_OPEN2OPEN_SOURCE_DIR}/src/fcstd_convert.cpp")
message(STATUS "gcv-freecad: local open2open source copy is incomplete - FreeCAD plugin disabled")
cmakefiles(${FREECAD_SRCS} CMakeLists.txt)
return()
endif()
set(_FREECAD_OCCT_BUILD_TREE FALSE)
if(OpenCASCADE_DIR)
get_filename_component(_FREECAD_OCCT_CONFIG_DIR "${OpenCASCADE_DIR}" ABSOLUTE)
get_filename_component(_FREECAD_OCCT_SOURCE_DIR "${_FREECAD_OCCT_CONFIG_DIR}" DIRECTORY)
if(EXISTS "${_FREECAD_OCCT_CONFIG_DIR}/OpenCASCADEConfig.cmake"
AND EXISTS "${_FREECAD_OCCT_CONFIG_DIR}/lin64/clang/lib"
AND EXISTS "${_FREECAD_OCCT_SOURCE_DIR}/src/Standard/Standard.hxx")
set(OpenCASCADE_FOUND TRUE)
set(_FREECAD_OCCT_BUILD_TREE TRUE)
set(OpenCASCADE_LIBRARY_DIR "${_FREECAD_OCCT_CONFIG_DIR}/lin64/clang/lib")
endif()
endif()
if(NOT OpenCASCADE_FOUND)
find_package(OpenCASCADE CONFIG QUIET)
if(NOT OpenCASCADE_FOUND)
find_package(OpenCASCADE QUIET)
endif()
endif()
if(NOT OpenCASCADE_FOUND)
message(STATUS "gcv-freecad: OpenCASCADE not found - FreeCAD plugin disabled")
cmakefiles(${FREECAD_SRCS} CMakeLists.txt)
return()
endif()
find_package(ZLIB REQUIRED)
if(TARGET ZLIB::ZLIB)
set(_FREECAD_ZLIB_LIB ZLIB::ZLIB)
else()
set(_FREECAD_ZLIB_LIB ${ZLIB_LIBRARIES})
endif()
brlcad_find_package(OPENNURBS REQUIRED SYSPATTERN openNURBS)
if(NOT TARGET OPENNURBS::OPENNURBS)
message(STATUS "gcv-freecad: BRL-CAD openNURBS target not found - FreeCAD plugin disabled")
cmakefiles(${FREECAD_SRCS} CMakeLists.txt)
return()
endif()
set(_FREECAD_OCCT_LIBS
TKernel
TKMath
TKG2d
TKG3d
TKGeomBase
TKBRep
TKGeomAlgo
TKTopAlgo
TKCDF
)
set(_FREECAD_OCCT_INCLUDE_DIRS ${OpenCASCADE_INCLUDE_DIR})
set(_FREECAD_OCCT_LIBRARY_DIRS ${OpenCASCADE_LIBRARY_DIR})
if(OpenCASCADE_DIR)
get_filename_component(_FREECAD_OCCT_CONFIG_DIR "${OpenCASCADE_DIR}" ABSOLUTE)
if(EXISTS "${_FREECAD_OCCT_CONFIG_DIR}/lin64/clang/lib")
list(APPEND _FREECAD_OCCT_LIBRARY_DIRS "${_FREECAD_OCCT_CONFIG_DIR}/lin64/clang/lib")
endif()
if(EXISTS "${_FREECAD_OCCT_CONFIG_DIR}/inc")
list(APPEND _FREECAD_OCCT_INCLUDE_DIRS "${_FREECAD_OCCT_CONFIG_DIR}/inc")
endif()
get_filename_component(_FREECAD_OCCT_SOURCE_DIR "${_FREECAD_OCCT_CONFIG_DIR}" DIRECTORY)
if(EXISTS "${_FREECAD_OCCT_SOURCE_DIR}/src/Standard/Standard.hxx")
file(GLOB _FREECAD_OCCT_SRC_INCLUDE_DIRS LIST_DIRECTORIES true "${_FREECAD_OCCT_SOURCE_DIR}/src/*")
list(APPEND _FREECAD_OCCT_INCLUDE_DIRS ${_FREECAD_OCCT_SRC_INCLUDE_DIRS})
endif()
endif()
list(REMOVE_DUPLICATES _FREECAD_OCCT_INCLUDE_DIRS)
list(REMOVE_DUPLICATES _FREECAD_OCCT_LIBRARY_DIRS)
set(_FREECAD_OPEN2OPEN_SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/fcstd_zip.cpp"
"${_OPEN2OPEN_SOURCE_DIR}/src/geom_convert.cpp"
"${_OPEN2OPEN_SOURCE_DIR}/src/brep_convert.cpp"
"${_OPEN2OPEN_SOURCE_DIR}/src/mesh_convert.cpp"
"${_OPEN2OPEN_SOURCE_DIR}/src/fcstd_convert.cpp"
)
add_library(gcv_freecad_open2open OBJECT ${_FREECAD_OPEN2OPEN_SRCS})
target_include_directories(gcv_freecad_open2open
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}"
"${_OPEN2OPEN_SOURCE_DIR}/include"
"${_OPEN2OPEN_SOURCE_DIR}/compat"
${_FREECAD_OCCT_INCLUDE_DIRS}
)
target_compile_definitions(gcv_freecad_open2open PUBLIC OPEN2OPEN_HAVE_LIBZIP)
target_link_libraries(gcv_freecad_open2open PUBLIC OPENNURBS::OPENNURBS ${_FREECAD_ZLIB_LIB})
target_compile_options(gcv_freecad_open2open PRIVATE $<$<CXX_COMPILER_ID:GNU,Clang>:-w>)
message(STATUS "gcv-freecad: FreeCAD plugin enabled using open2open at ${_OPEN2OPEN_SOURCE_DIR}")
gcv_plugin_library(gcv-freecad SHARED ${FREECAD_SRCS})
target_include_directories(gcv-freecad PRIVATE
$<TARGET_PROPERTY:gcv_freecad_open2open,INTERFACE_INCLUDE_DIRECTORIES>
)
target_compile_definitions(gcv-freecad PRIVATE BRLCADBUILD HAVE_CONFIG_H OPEN2OPEN_HAVE_LIBZIP)
if(_FREECAD_OCCT_LIBRARY_DIRS)
target_link_directories(gcv-freecad PRIVATE ${_FREECAD_OCCT_LIBRARY_DIRS})
endif()
target_link_libraries(gcv-freecad
libgcv
librt
libwdb
libbg
libbu
OPENNURBS::OPENNURBS
$<TARGET_OBJECTS:gcv_freecad_open2open>
${_FREECAD_OCCT_LIBS}
${_FREECAD_ZLIB_LIB}
)
validate_style(gcv-freecad "${FREECAD_SRCS}")
plugin_setup(gcv-freecad gcv)
cmakefiles(
${FREECAD_SRCS}
${FREECAD_EXTRA_SRCS}
CMakeLists.txt
)
# Local Variables:
# tab-width: 8
# mode: cmake
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
+427
View File
@@ -0,0 +1,427 @@
#include "fcstd_zip.h"
#include <zlib.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
#include <vector>
struct zip_entry {
std::string name;
std::uint16_t method = 0;
std::uint32_t crc = 0;
std::uint32_t compressed_size = 0;
std::uint32_t uncompressed_size = 0;
std::uint32_t local_header_offset = 0;
std::vector<unsigned char> compressed;
std::vector<unsigned char> uncompressed;
};
struct zip {
std::string path;
bool write_mode = false;
std::map<std::string, zip_entry> entries;
};
struct zip_file {
std::vector<unsigned char> data;
std::size_t offset = 0;
};
struct zip_source {
std::vector<unsigned char> data;
};
static std::uint16_t
read_le16(const unsigned char *p)
{
return (std::uint16_t)p[0] | ((std::uint16_t)p[1] << 8);
}
static std::uint32_t
read_le32(const unsigned char *p)
{
return (std::uint32_t)p[0] |
((std::uint32_t)p[1] << 8) |
((std::uint32_t)p[2] << 16) |
((std::uint32_t)p[3] << 24);
}
static void
append_le16(std::vector<unsigned char> &out, std::uint16_t v)
{
out.push_back((unsigned char)(v & 0xff));
out.push_back((unsigned char)((v >> 8) & 0xff));
}
static void
append_le32(std::vector<unsigned char> &out, std::uint32_t v)
{
out.push_back((unsigned char)(v & 0xff));
out.push_back((unsigned char)((v >> 8) & 0xff));
out.push_back((unsigned char)((v >> 16) & 0xff));
out.push_back((unsigned char)((v >> 24) & 0xff));
}
static bool
read_file(const std::string &path, std::vector<unsigned char> &data)
{
std::ifstream in(path, std::ios::binary);
if (!in)
return false;
in.seekg(0, std::ios::end);
std::streamoff sz = in.tellg();
if (sz < 0)
return false;
in.seekg(0, std::ios::beg);
data.resize((std::size_t)sz);
if (!data.empty())
in.read((char *)data.data(), (std::streamsize)data.size());
return in.good() || in.eof();
}
static bool
inflate_raw(const std::vector<unsigned char> &input, std::uint32_t expected_size, std::vector<unsigned char> &output)
{
output.assign(expected_size, 0);
unsigned char empty_output = 0;
z_stream zs;
std::memset(&zs, 0, sizeof(zs));
zs.next_in = const_cast<Bytef *>(input.data());
zs.avail_in = (uInt)input.size();
zs.next_out = expected_size ? output.data() : &empty_output;
zs.avail_out = expected_size ? (uInt)output.size() : 1;
if (inflateInit2(&zs, -MAX_WBITS) != Z_OK)
return false;
int ret = inflate(&zs, Z_FINISH);
inflateEnd(&zs);
return ret == Z_STREAM_END && zs.total_out == expected_size;
}
static bool
deflate_raw(const std::vector<unsigned char> &input, std::vector<unsigned char> &output)
{
z_stream zs;
std::memset(&zs, 0, sizeof(zs));
if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK)
return false;
uLong bound = deflateBound(&zs, (uLong)input.size());
output.assign((std::size_t)bound, 0);
zs.next_in = const_cast<Bytef *>(input.data());
zs.avail_in = (uInt)input.size();
zs.next_out = output.data();
zs.avail_out = (uInt)output.size();
int ret = deflate(&zs, Z_FINISH);
if (ret != Z_STREAM_END) {
deflateEnd(&zs);
return false;
}
output.resize((std::size_t)zs.total_out);
deflateEnd(&zs);
return true;
}
static bool
parse_zip(zip_t *archive)
{
std::vector<unsigned char> file_data;
if (!read_file(archive->path, file_data) || file_data.size() < 22)
return false;
std::size_t scan_start = file_data.size() > 66000 ? file_data.size() - 66000 : 0;
std::size_t eocd = std::string::npos;
for (std::size_t i = file_data.size() - 22; i + 1 > scan_start; --i) {
if (read_le32(&file_data[i]) == 0x06054b50u) {
eocd = i;
break;
}
if (i == 0)
break;
}
if (eocd == std::string::npos)
return false;
std::uint16_t total_entries = read_le16(&file_data[eocd + 10]);
std::uint32_t cd_size = read_le32(&file_data[eocd + 12]);
std::uint32_t cd_offset = read_le32(&file_data[eocd + 16]);
if ((std::uint64_t)cd_offset + cd_size > file_data.size())
return false;
std::size_t pos = cd_offset;
for (std::uint16_t i = 0; i < total_entries; ++i) {
if (pos + 46 > file_data.size() || read_le32(&file_data[pos]) != 0x02014b50u)
return false;
std::uint16_t flags = read_le16(&file_data[pos + 8]);
std::uint16_t method = read_le16(&file_data[pos + 10]);
std::uint32_t crc = read_le32(&file_data[pos + 16]);
std::uint32_t csize = read_le32(&file_data[pos + 20]);
std::uint32_t usize = read_le32(&file_data[pos + 24]);
std::uint16_t name_len = read_le16(&file_data[pos + 28]);
std::uint16_t extra_len = read_le16(&file_data[pos + 30]);
std::uint16_t comment_len = read_le16(&file_data[pos + 32]);
std::uint32_t local_offset = read_le32(&file_data[pos + 42]);
if ((flags & 1) || (method != 0 && method != 8))
return false;
if (pos + 46u + name_len + extra_len + comment_len > file_data.size())
return false;
std::string name((const char *)&file_data[pos + 46], name_len);
if ((std::uint64_t)local_offset + 30 > file_data.size() ||
read_le32(&file_data[local_offset]) != 0x04034b50u)
return false;
std::uint16_t lname_len = read_le16(&file_data[local_offset + 26]);
std::uint16_t lextra_len = read_le16(&file_data[local_offset + 28]);
std::uint64_t data_offset = (std::uint64_t)local_offset + 30 + lname_len + lextra_len;
if (data_offset + csize > file_data.size())
return false;
zip_entry e;
e.name = name;
e.method = method;
e.crc = crc;
e.compressed_size = csize;
e.uncompressed_size = usize;
e.local_header_offset = local_offset;
e.compressed.assign(file_data.begin() + (std::ptrdiff_t)data_offset,
file_data.begin() + (std::ptrdiff_t)(data_offset + csize));
archive->entries[name] = std::move(e);
pos += 46u + name_len + extra_len + comment_len;
}
return true;
}
extern "C" zip_t *
zip_open(const char *path, int flags, int *errorp)
{
if (errorp)
*errorp = 0;
if (!path)
return nullptr;
zip_t *archive = new zip;
archive->path = path;
archive->write_mode = (flags & ZIP_CREATE) || (flags & ZIP_TRUNCATE);
if (!archive->write_mode) {
if (!parse_zip(archive)) {
delete archive;
if (errorp)
*errorp = 1;
return nullptr;
}
}
return archive;
}
extern "C" void
zip_discard(zip_t *archive)
{
delete archive;
}
extern "C" zip_file_t *
zip_fopen(zip_t *archive, const char *fname, int)
{
if (!archive || !fname)
return nullptr;
auto it = archive->entries.find(fname);
if (it == archive->entries.end())
return nullptr;
zip_file_t *f = new zip_file;
const zip_entry &entry = it->second;
if (!entry.uncompressed.empty() || entry.uncompressed_size == 0) {
f->data = entry.uncompressed;
} else if (entry.method == 0) {
f->data = entry.compressed;
if (f->data.size() != entry.uncompressed_size) {
delete f;
return nullptr;
}
} else if (!inflate_raw(entry.compressed, entry.uncompressed_size, f->data)) {
delete f;
return nullptr;
}
return f;
}
extern "C" zip_int64_t
zip_fread(zip_file_t *file, void *buf, zip_uint64_t nbytes)
{
if (!file || !buf)
return -1;
std::size_t remaining = file->data.size() - file->offset;
std::size_t n = std::min<std::size_t>((std::size_t)nbytes, remaining);
if (n)
std::memcpy(buf, file->data.data() + file->offset, n);
file->offset += n;
return (zip_int64_t)n;
}
extern "C" int
zip_fclose(zip_file_t *file)
{
delete file;
return 0;
}
extern "C" void
zip_stat_init(zip_stat_t *st)
{
if (st)
std::memset(st, 0, sizeof(*st));
}
extern "C" int
zip_stat(zip_t *archive, const char *fname, int, zip_stat_t *st)
{
if (!archive || !fname || !st)
return -1;
auto it = archive->entries.find(fname);
if (it == archive->entries.end())
return -1;
zip_stat_init(st);
st->valid = ZIP_STAT_SIZE;
st->size = it->second.uncompressed_size;
return 0;
}
extern "C" zip_source_t *
zip_source_buffer(zip_t *, const void *data, zip_uint64_t len, int)
{
if (!data && len)
return nullptr;
zip_source_t *src = new zip_source;
const unsigned char *bytes = (const unsigned char *)data;
src->data.assign(bytes, bytes + len);
return src;
}
extern "C" void
zip_source_free(zip_source_t *source)
{
delete source;
}
extern "C" zip_int64_t
zip_file_add(zip_t *archive, const char *name, zip_source_t *source, int)
{
if (!archive || !archive->write_mode || !name || !source)
return -1;
zip_entry e;
e.name = name;
e.uncompressed = source->data;
e.uncompressed_size = (std::uint32_t)e.uncompressed.size();
e.crc = crc32(0L, Z_NULL, 0);
if (!e.uncompressed.empty())
e.crc = crc32(e.crc, e.uncompressed.data(), (uInt)e.uncompressed.size());
std::vector<unsigned char> compressed;
if (deflate_raw(e.uncompressed, compressed) && compressed.size() < e.uncompressed.size()) {
e.method = 8;
e.compressed = std::move(compressed);
} else {
e.method = 0;
e.compressed = e.uncompressed;
}
e.compressed_size = (std::uint32_t)e.compressed.size();
archive->entries[e.name] = std::move(e);
delete source;
return (zip_int64_t)archive->entries.size() - 1;
}
static bool
write_all(std::ofstream &out, const std::vector<unsigned char> &data)
{
if (!data.empty())
out.write((const char *)data.data(), (std::streamsize)data.size());
return out.good();
}
extern "C" int
zip_close(zip_t *archive)
{
if (!archive)
return -1;
if (!archive->write_mode) {
delete archive;
return 0;
}
std::vector<unsigned char> file_data;
std::vector<unsigned char> central_dir;
for (auto &kv : archive->entries) {
zip_entry &e = kv.second;
e.local_header_offset = (std::uint32_t)file_data.size();
append_le32(file_data, 0x04034b50u);
append_le16(file_data, 20);
append_le16(file_data, 0x0800u);
append_le16(file_data, e.method);
append_le16(file_data, 0);
append_le16(file_data, 0);
append_le32(file_data, e.crc);
append_le32(file_data, e.compressed_size);
append_le32(file_data, e.uncompressed_size);
append_le16(file_data, (std::uint16_t)e.name.size());
append_le16(file_data, 0);
file_data.insert(file_data.end(), e.name.begin(), e.name.end());
file_data.insert(file_data.end(), e.compressed.begin(), e.compressed.end());
append_le32(central_dir, 0x02014b50u);
append_le16(central_dir, 20);
append_le16(central_dir, 20);
append_le16(central_dir, 0x0800u);
append_le16(central_dir, e.method);
append_le16(central_dir, 0);
append_le16(central_dir, 0);
append_le32(central_dir, e.crc);
append_le32(central_dir, e.compressed_size);
append_le32(central_dir, e.uncompressed_size);
append_le16(central_dir, (std::uint16_t)e.name.size());
append_le16(central_dir, 0);
append_le16(central_dir, 0);
append_le16(central_dir, 0);
append_le16(central_dir, 0);
append_le32(central_dir, 0);
append_le32(central_dir, e.local_header_offset);
central_dir.insert(central_dir.end(), e.name.begin(), e.name.end());
}
std::uint32_t cd_offset = (std::uint32_t)file_data.size();
std::uint32_t cd_size = (std::uint32_t)central_dir.size();
file_data.insert(file_data.end(), central_dir.begin(), central_dir.end());
append_le32(file_data, 0x06054b50u);
append_le16(file_data, 0);
append_le16(file_data, 0);
append_le16(file_data, (std::uint16_t)archive->entries.size());
append_le16(file_data, (std::uint16_t)archive->entries.size());
append_le32(file_data, cd_size);
append_le32(file_data, cd_offset);
append_le16(file_data, 0);
std::ofstream out(archive->path, std::ios::binary | std::ios::trunc);
bool ok = out && write_all(out, file_data);
out.close();
delete archive;
return ok ? 0 : -1;
}
+54
View File
@@ -0,0 +1,54 @@
/* Minimal FCStd ZIP archive interface for the local open2open copy.
*
* This intentionally implements only the archive operations used by
* open2open/src/fcstd_convert.cpp, using zlib for ZIP deflate streams.
*/
#ifndef GCV_FREECAD_FCSTD_ZIP_H
#define GCV_FREECAD_FCSTD_ZIP_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef int64_t zip_int64_t;
typedef uint64_t zip_uint64_t;
typedef struct zip zip_t;
typedef struct zip_file zip_file_t;
typedef struct zip_source zip_source_t;
typedef struct zip_stat {
zip_uint64_t valid;
zip_uint64_t size;
} zip_stat_t;
#define ZIP_RDONLY 0
#define ZIP_CREATE 1
#define ZIP_TRUNCATE 8
#define ZIP_FL_OVERWRITE 8192
#define ZIP_STAT_SIZE 0x0004u
zip_t *zip_open(const char *path, int flags, int *errorp);
int zip_close(zip_t *archive);
void zip_discard(zip_t *archive);
zip_file_t *zip_fopen(zip_t *archive, const char *fname, int flags);
zip_int64_t zip_fread(zip_file_t *file, void *buf, zip_uint64_t nbytes);
int zip_fclose(zip_file_t *file);
void zip_stat_init(zip_stat_t *st);
int zip_stat(zip_t *archive, const char *fname, int flags, zip_stat_t *st);
zip_source_t *zip_source_buffer(zip_t *archive, const void *data, zip_uint64_t len, int freep);
zip_int64_t zip_file_add(zip_t *archive, const char *name, zip_source_t *source, int flags);
void zip_source_free(zip_source_t *source);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,104 @@
FreeCAD GCV plugin improvement targets
======================================
Current measured baseline
-------------------------
Sample: (https://github.com/starseeker/open2open)
open2open/phasma-mech-assembly/FreeCAD native models/Simple structure w. panels.FCStd
Recent full import result:
.build/bin/gcv -i <sample.FCStd> -o /tmp/brlcad_freecad_simple_optimized.g
real 70.64s
output size 276M
With GCV_FREECAD_TIMING=1:
importer total_ms=47456
wall real=69.89s
converted=196
hidden=107
cached=64
conversion_failed=2
This means roughly 22s of the wall time is outside the measured FreeCAD
reader/converter path, most likely in downstream .g serialization.
Completed performance fix
-------------------------
OCCTToON_Brep loop-gap repair previously rebuilt a pcurve reference-count map
inside every loop by scanning all trims created so far. On large BReps this was
effectively O(loop count * trim count). It has been replaced with an incremental
trim_curve_refcount map.
Measured example:
b_ClydeSpace_30Wh_battery_pack__simplified__001_
before: convert_ms=14486, pass3_loop_gap_us=13812995
after: convert_ms=689, pass3_loop_gap_us=18837
Next targets
------------
1. Attribute the post-import .g writing cost.
The timed importer reports about 47.5s, while the complete gcv run takes
about 69.9s. Add or reuse timing around the GCV/database output path to
identify whether the remaining ~22s is object insertion, directory updates,
final db close/write, or BRep serialization.
2. Investigate large FeaturePython BRep entries.
The largest remaining successful object cost in the sample is:
b_CubeADCS_Gen_2_001_
FCStd entry size: 62631280 bytes
faces: 28576
read_ms=1122
brep_read_ms=2500
convert_ms=7659
add_ms=289
Similar large objects dominate the importer time. The immediate work is to
profile these individually and decide whether the cost is unavoidable geometry
size or whether more caching/instancing is possible.
3. Test file-backed OCCT BRep reads for large BRP payloads.
Current path reads BRP content from the FCStd zip into memory and then uses an
in-memory stream for BRepTools::Read. For tens-of-megabytes BRP payloads,
test extracting to a temporary file and using the filename-based OCCT read
API. Compare read_ms and brep_read_ms on CubeADCS-scale entries.
4. Avoid converting repeated assembly instances as full standalone BReps.
Exact BRP text caching helps when the payloads are byte-identical, but many
assembly repetitions differ by placement or generated payload. If FreeCAD
object placement/link metadata can be mapped to BRL-CAD comb instances with
transforms, this would reduce both conversion time and .g size. This is
likely the largest architectural improvement.
5. Handle or cheaply reject expensive conversion failures.
In the sample, two large antenna mechanism objects failed after expensive
parse/conversion work:
b_Phasma_Primus_Antenna_Deployment_Mechanism__001_001
read_ms=708, brep_read_ms=1456, convert_ms=3192
b_Phasma_Primus_Antenna_Deployment_Mechanism__001_002
read_ms=721, brep_read_ms=1488, convert_ms=3214
Determine why OCCTToON_Brep returns invalid for these and either fix the
conversion or add a cheaper preflight/skip path if they are out of scope.
6. Further split remaining Pass 3 face conversion time if needed.
After the loop-gap refcount fix, pass3_loop_gap_us is no longer dominant.
Remaining Pass 3 time is mostly surface conversion, pcurve conversion, and
miscellaneous face/loop/trim construction. If #1-#5 are exhausted, add finer
counters around NewFace/NewLoop/NewTrim, wire traversal, pcurve domain
remapping, and surface cache hits/misses.
7. Keep diagnostics gated.
GCV_FREECAD_TIMING is the current opt-in switch for detailed importer and
OCCTToON_Brep timings. Any additional instrumentation should stay behind this
environment variable, especially if placed inside face/edge/trim loops.
+241
View File
@@ -0,0 +1,241 @@
/* F R E E C A D _ R E A D . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file freecad_read.cpp
*
* Import FreeCAD .FCStd files into BRL-CAD using open2open's FCStd reader and
* OCCT -> openNURBS BRep conversion core.
*/
#include "common.h"
#include "bu/path.h"
#include "gcv/api.h"
#include "wdb.h"
#include "opennurbs.h"
#include "open2open/fcstd_convert.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <vector>
namespace freecad_read
{
static std::string
clean_name(const std::string &raw, const char *fallback, int index)
{
std::string name = raw.empty() ? std::string(fallback) + "_" + std::to_string(index) : raw;
for (std::string::iterator it = name.begin(); it != name.end(); ++it) {
unsigned char c = static_cast<unsigned char>(*it);
if (!std::isalnum(c) && *it != '_' && *it != '-' && *it != '.')
*it = '_';
}
if (name.empty())
name = std::string(fallback) + "_" + std::to_string(index);
return name;
}
static std::string
unique_name(const std::string &base, std::map<std::string, int> &used)
{
std::map<std::string, int>::iterator found = used.find(base);
if (found == used.end()) {
used[base] = 1;
return base;
}
int next = ++found->second;
return base + "_" + std::to_string(next);
}
static std::string
root_name_from_path(const char *source_path, const gcv_opts *gcv_options)
{
if (gcv_options && gcv_options->default_name)
return clean_name(gcv_options->default_name, "freecad_model", 0);
bu_vls basename = BU_VLS_INIT_ZERO;
if (source_path && bu_path_component(&basename, source_path, BU_PATH_BASENAME_EXTLESS)) {
std::string result = clean_name(bu_vls_cstr(&basename), "freecad_model", 0);
bu_vls_free(&basename);
return result;
}
bu_vls_free(&basename);
return "freecad_model";
}
static void
add_color_attribute(struct rt_wdb *wdbp, const std::string &name, const ON_3dmObjectAttributes *attrs)
{
if (!wdbp || !attrs)
return;
ON_Color c = attrs->m_color;
if (c == ON_Color::UnsetColor)
return;
bu_vls rgb = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&rgb, "%d/%d/%d", c.Red(), c.Green(), c.Blue());
(void)db5_update_attribute(name.c_str(), "color", bu_vls_cstr(&rgb), wdbp->dbip);
bu_vls_free(&rgb);
}
static int
freecad_read(gcv_context *context, const gcv_opts *gcv_options, const void *UNUSED(options_data), const char *source_path)
{
if (!context || !context->dbip || !source_path)
return 0;
ONX_Model model;
int converted = open2open::FCStdFileToONX_Model(source_path, model);
if (converted <= 0) {
bu_log("freecad_read: no FreeCAD shapes were converted from %s\n", source_path);
return 0;
}
struct rt_wdb *wdbp = wdb_dbopen(context->dbip, RT_WDB_TYPE_DB_INMEM);
if (!wdbp)
return 0;
std::map<std::string, int> used;
std::vector<std::string> solids;
ONX_ModelComponentIterator it(model, ON_ModelComponent::Type::ModelGeometry);
int index = 0;
for (const ON_ModelComponent *component = it.FirstComponent(); component; component = it.NextComponent(), ++index) {
const ON_ModelGeometryComponent *geometry_component = ON_ModelGeometryComponent::Cast(component);
if (!geometry_component)
continue;
const ON_Geometry *geometry = geometry_component->Geometry(NULL);
if (!geometry)
continue;
std::unique_ptr<ON_Brep> brep_holder;
const ON_Brep *brep = ON_Brep::Cast(geometry);
if (!brep && geometry->HasBrepForm()) {
brep_holder.reset(geometry->BrepForm());
brep = brep_holder.get();
}
if (!brep)
continue;
if (brep->m_F.Count() <= 0)
continue;
std::string raw_name;
const ON_3dmObjectAttributes *attrs = geometry_component->Attributes(NULL);
if (attrs && attrs->m_name.IsNotEmpty()) {
ON_String name_utf8(attrs->m_name);
raw_name = name_utf8.Array();
}
std::string solid_name = unique_name(clean_name(raw_name, "freecad_shape", index), used);
ON_Brep *writable_brep = const_cast<ON_Brep *>(brep);
if (mk_brep(wdbp, solid_name.c_str(), (void *)writable_brep)) {
bu_log("freecad_read: failed to write BRep %s\n", solid_name.c_str());
continue;
}
(void)db5_update_attribute(solid_name.c_str(), "importer", "gcv-freecad", wdbp->dbip);
add_color_attribute(wdbp, solid_name, attrs);
solids.push_back(solid_name);
}
if (solids.empty()) {
bu_log("freecad_read: no BRep solids were written for %s\n", source_path);
return 0;
}
struct wmember root;
BU_LIST_INIT(&root.l);
for (std::vector<std::string>::const_iterator s = solids.begin(); s != solids.end(); ++s)
mk_addmember(s->c_str(), &root.l, NULL, WMOP_UNION);
std::string root_name = unique_name(root_name_from_path(source_path, gcv_options), used);
if (mk_comb(wdbp, root_name.c_str(), &root.l, 0, NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0))
bu_log("freecad_read: failed to write top-level combination %s\n", root_name.c_str());
return 1;
}
static int
freecad_can_read(const char *source_path)
{
if (!source_path)
return 0;
const char *ext = strrchr(source_path, '.');
if (ext && BU_STR_EQUIV(ext + 1, "fcstd"))
return 1;
open2open::FcstdDoc doc;
return open2open::ReadFcstdDoc(source_path, doc) ? 1 : 0;
}
static const struct gcv_filter gcv_conv_freecad_read = {
"FreeCAD Reader", GCV_FILTER_READ, BU_MIME_MODEL_VND_FREECAD, freecad_can_read,
NULL, NULL, freecad_read
};
extern "C" const struct gcv_filter gcv_conv_freecad_write;
static const struct gcv_filter * const filters[] = {&gcv_conv_freecad_read, &gcv_conv_freecad_write, NULL};
}
extern "C"
{
extern const gcv_plugin gcv_plugin_info_s = {freecad_read::filters};
COMPILER_DLLEXPORT const struct gcv_plugin *
gcv_plugin_info()
{
return &gcv_plugin_info_s;
}
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
@@ -0,0 +1,237 @@
/* F R E E C A D _ W R I T E . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file freecad_write.cpp
*
* Export BRL-CAD BRep primitives to FreeCAD .FCStd using open2open's
* openNURBS -> OCCT writer.
*/
#include "common.h"
#include "bu/path.h"
#include "gcv/api.h"
#include "raytrace.h"
#include "rt/comb.h"
#include "rt/geom.h"
#include "opennurbs.h"
#include "open2open/fcstd_convert.h"
#include <cctype>
#include <cstring>
#include <set>
#include <string>
#include <vector>
namespace freecad_write
{
static std::string
clean_name(const char *raw, const char *fallback, int index)
{
std::string name = (raw && raw[0]) ? raw : std::string(fallback) + "_" + std::to_string(index);
for (std::string::iterator it = name.begin(); it != name.end(); ++it) {
unsigned char c = static_cast<unsigned char>(*it);
if (!std::isalnum(c) && *it != '_' && *it != '-' && *it != '.')
*it = '_';
}
return name;
}
static bool
append_brep(struct db_i *dbip, const struct directory *dp, ONX_Model &model, int index)
{
if (!dbip || !dp)
return false;
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
if (rt_db_get_internal(&intern, dp, dbip, NULL) < 0)
return false;
if (intern.idb_type != ID_BREP) {
rt_db_free_internal(&intern);
return false;
}
struct rt_brep_internal *bi = (struct rt_brep_internal *)intern.idb_ptr;
if (!bi || !bi->brep) {
rt_db_free_internal(&intern);
return false;
}
ON_3dmObjectAttributes attrs;
attrs.Default();
attrs.m_name = clean_name(dp->d_namep, "brlcad_brep", index).c_str();
model.AddModelGeometryComponent(ON_Brep::New(*bi->brep), &attrs, true);
rt_db_free_internal(&intern);
return true;
}
static void
free_child_matrices(matp_t *mats)
{
if (!mats)
return;
for (int i = 0; mats[i]; ++i)
bu_free(mats[i], "free matrix");
bu_free(mats, "free mats array");
}
static int
append_breps_from_dp(struct db_i *dbip, const struct directory *dp, ONX_Model &model, int &index, std::set<const struct directory *> &seen_breps, std::set<const struct directory *> &seen_combs)
{
if (!dbip || !dp)
return 0;
if (dp->d_flags & RT_DIR_SOLID) {
if (seen_breps.count(dp))
return 0;
seen_breps.insert(dp);
if (append_brep(dbip, dp, model, index)) {
++index;
return 1;
}
return 0;
}
if (!(dp->d_flags & RT_DIR_COMB) || seen_combs.count(dp))
return 0;
seen_combs.insert(dp);
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
if (rt_db_get_internal(&intern, dp, dbip, NULL) < 0)
return 0;
if (intern.idb_type != ID_COMBINATION) {
rt_db_free_internal(&intern);
return 0;
}
struct rt_comb_internal *comb = (struct rt_comb_internal *)intern.idb_ptr;
struct directory **children = NULL;
matp_t *mats = NULL;
int child_count = db_comb_children(dbip, comb, &children, NULL, &mats);
int brep_count = 0;
for (int i = 0; i < child_count && children && children[i] != RT_DIR_NULL; ++i)
brep_count += append_breps_from_dp(dbip, children[i], model, index, seen_breps, seen_combs);
if (children)
bu_free(children, "free children struct directory ptr array");
free_child_matrices(mats);
rt_db_free_internal(&intern);
return brep_count;
}
static int
append_selected_breps(struct db_i *dbip, const gcv_opts *gcv_options, ONX_Model &model)
{
int count = 0;
int index = 0;
std::set<const struct directory *> seen_breps;
std::set<const struct directory *> seen_combs;
if (gcv_options && gcv_options->num_objects) {
for (size_t i = 0; i < gcv_options->num_objects; ++i) {
const char *name = gcv_options->object_names[i];
const struct directory *dp = db_lookup(dbip, name, LOOKUP_QUIET);
if (!dp)
continue;
count += append_breps_from_dp(dbip, dp, model, index, seen_breps, seen_combs);
}
return count;
}
struct directory *dp;
FOR_ALL_DIRECTORY_START(dp, dbip) {
if (dp->d_flags & RT_DIR_SOLID) {
if (!seen_breps.count(dp) && append_brep(dbip, dp, model, index)) {
seen_breps.insert(dp);
++count;
++index;
}
}
} FOR_ALL_DIRECTORY_END;
return count;
}
static int
freecad_write(gcv_context *context, const gcv_opts *gcv_options, const void *UNUSED(options_data), const char *dest_path)
{
if (!context || !context->dbip || !dest_path)
return 0;
ONX_Model model;
model.AddDefaultLayer(L"Default", ON_Color::UnsetColor);
model.m_properties.m_RevisionHistory.NewRevision();
model.m_properties.m_Application.m_application_name = "BRL-CAD gcv FreeCAD writer";
int brep_count = append_selected_breps(context->dbip, gcv_options, model);
if (brep_count <= 0) {
bu_log("freecad_write: no BRep primitives available to export\n");
return 0;
}
int written = open2open::ONX_ModelToFCStdFile(dest_path, model);
if (written <= 0) {
bu_log("freecad_write: failed to write %s\n", dest_path);
return 0;
}
if (written < brep_count)
bu_log("freecad_write: wrote %d of %d BRep primitives to %s\n", written, brep_count, dest_path);
return 1;
}
extern "C" const struct gcv_filter gcv_conv_freecad_write = {
"FreeCAD Writer", GCV_FILTER_WRITE, BU_MIME_MODEL_VND_FREECAD, NULL,
NULL, NULL, freecad_write
};
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
@@ -0,0 +1,49 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// brep_convert.h — B-Rep topology conversion helpers.
//
// These functions translate complete B-Rep solid models between openNURBS
// (ON_Brep) and OpenCASCADE (TopoDS_Shape).
#ifndef OPEN2OPEN_BREP_CONVERT_H
#define OPEN2OPEN_BREP_CONVERT_H
#include <TopoDS_Shape.hxx>
// Forward declarations from openNURBS
class ON_Brep;
namespace open2open {
/// Convert an openNURBS B-Rep to an OCCT TopoDS_Shape (Shell or Solid).
///
/// All NURBS curves and surfaces in the brep are translated using the
/// geometry helpers from geom_convert.h. Non-NURBS primitives are
/// first converted to their NURBS representations via
/// ON_Curve::NurbsCurve() / ON_Surface::NurbsSurface().
///
/// @param brep Source B-Rep solid.
/// @param linear_tolerance Positional tolerance propagated to OCCT topology.
/// @return A TopoDS_Solid (or TopoDS_Shell for open models).
/// A null shape is returned on failure.
TopoDS_Shape ON_BrepToOCCT(const ON_Brep& brep,
double linear_tolerance = 1e-6);
/// Convert an OCCT TopoDS_Shape to an openNURBS ON_Brep.
///
/// The function traverses the shape with TopExp_Explorer, translates each
/// geometric entity to NURBS using the geometry helpers, and populates the
/// provided ON_Brep with the resulting topology.
///
/// @param shape Source OCCT shape (must contain Face topology).
/// @param brep Destination B-Rep (will be cleared first).
/// @param linear_tolerance Tolerance used when building the ON_Brep.
/// @return true on success; false if the conversion failed.
bool OCCTToON_Brep(const TopoDS_Shape& shape,
ON_Brep& brep,
double linear_tolerance = 1e-6);
} // namespace open2open
#endif // OPEN2OPEN_BREP_CONVERT_H
@@ -0,0 +1,185 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// fcstd_convert.h — FreeCAD .FCStd archive reader.
//
// Provides read-only access to the metadata and display colours stored in a
// FreeCAD `.FCStd` archive, complementing the B-Rep conversion functions in
// `brep_convert.h`.
//
// A `.FCStd` file is a standard ZIP archive containing:
//
// Document.xml — object tree (type, name, label, shape file mapping)
// GuiDocument.xml — per-object display settings (colour, visibility …)
// PartShape.brp, — OpenCASCADE B-Rep shapes in OCCT BRep text format
// PartShape1.brp …
// DiffuseColor, — per-face colour lists (binary, one per shape)
// DiffuseColor1 …
//
// DiffuseColor binary format
// --------------------------
// Each `DiffuseColor` file contains:
// uint32 count (little-endian: number of faces)
// uint32 color[count] (per face, packed as 0xRRGGBBAA little-endian)
//
// Note: FreeCAD stores alpha = 0 for fully opaque faces (inverted convention).
//
// Usage
// -----
// open2open::FcstdDoc doc;
// if (open2open::ReadFcstdDoc("/path/to/Model.FCStd", doc)) {
// for (auto& obj : doc.objects) {
// std::printf("name=%s brp=%s faces=%zu\n",
// obj.label.c_str(), obj.brp_file.c_str(),
// obj.face_colors.size());
// }
// }
//
// Dependencies
// ------------
// In this BRL-CAD-local copy, FCStd archive I/O is backed by zlib through the
// plugin's narrow archive helper. OPEN2OPEN_HAVE_LIBZIP is kept as the
// existing open2open compile guard for the FCStd read/write entry points.
#ifndef OPEN2OPEN_FCSTD_CONVERT_H
#define OPEN2OPEN_FCSTD_CONVERT_H
#include <cstdint>
#include <string>
#include <vector>
namespace open2open {
// ---------------------------------------------------------------------------
// Per-object display colour (packed 0xRRGGBBAA, alpha=0 → fully opaque)
// ---------------------------------------------------------------------------
using FcstdColor = std::uint32_t;
// Convenience helpers for unpacking a FcstdColor.
inline std::uint8_t FcstdColorR(FcstdColor c) { return (c >> 24) & 0xFFu; }
inline std::uint8_t FcstdColorG(FcstdColor c) { return (c >> 16) & 0xFFu; }
inline std::uint8_t FcstdColorB(FcstdColor c) { return (c >> 8) & 0xFFu; }
// alpha=0 means fully opaque in FreeCAD's convention.
inline std::uint8_t FcstdColorA(FcstdColor c) { return (c ) & 0xFFu; }
// Convert FreeCAD packed color to a normalised [0,1] RGBA tuple.
// FreeCAD alpha=0 → standard alpha=1 (opaque).
inline void FcstdColorToRGBAf(FcstdColor c,
float& r, float& g, float& b, float& a)
{
r = FcstdColorR(c) / 255.0f;
g = FcstdColorG(c) / 255.0f;
b = FcstdColorB(c) / 255.0f;
std::uint8_t fc_alpha = FcstdColorA(c);
a = (fc_alpha == 0) ? 1.0f : (1.0f - fc_alpha / 255.0f);
}
// ---------------------------------------------------------------------------
// Parse a DiffuseColor binary blob (in memory) into per-face colors.
//
// @param data Pointer to the raw bytes (e.g. content of DiffuseColor).
// @param size Number of bytes.
// @param colors_out Receives count packed FcstdColor values; cleared first.
// @return true on success; false if the data is truncated/invalid.
// ---------------------------------------------------------------------------
bool ParseDiffuseColors(const void* data,
std::size_t size,
std::vector<FcstdColor>& colors_out);
// ---------------------------------------------------------------------------
// Information about one geometric object inside an FCStd archive.
// ---------------------------------------------------------------------------
struct FcstdObject {
std::string name; ///< Internal name used as key (e.g. "Part__Feature")
std::string type; ///< FreeCAD object type (e.g. "Part::Feature")
std::string label; ///< User-facing label (e.g. "ESQ-126-38-G-D_socket")
std::string part_label; ///< Label of the containing App::Part (empty if none)
std::string brp_file; ///< Path inside ZIP to the BRep file (e.g. "PartShape.brp")
FcstdColor shape_color = 0xC8C8C800u; ///< Overall shape colour (packed RGBA, FreeCAD alpha=0 → opaque)
bool visible = true;
std::vector<FcstdColor> face_colors; ///< Per-face colours (may be empty)
};
// ---------------------------------------------------------------------------
// Document-level metadata extracted from Document.xml.
// ---------------------------------------------------------------------------
struct FcstdDocMeta {
std::string label; ///< Document label (e.g. "ESQ-126-38-G-D")
std::string created_by;
std::string creation_date;
std::string last_modified_date;
std::string comment;
std::string company;
};
// ---------------------------------------------------------------------------
// Everything read from one FCStd archive.
// ---------------------------------------------------------------------------
struct FcstdDoc {
FcstdDocMeta meta;
std::vector<FcstdObject> objects; ///< Only objects with a BRep shape file
};
// ---------------------------------------------------------------------------
// Read an FCStd archive and populate a FcstdDoc.
//
// Reads Document.xml, GuiDocument.xml, and all DiffuseColor binary blobs.
// Objects that have no associated BRep file (e.g. App::Origin, App::Line) are
// silently skipped.
//
// @param path Filesystem path to the .FCStd file.
// @param doc Output: populated on success; unchanged on failure.
// @return true if the archive was opened and at least Document.xml was
// parsed; false if the file could not be opened as a ZIP archive.
//
// This function requires OPEN2OPEN_HAVE_LIBZIP, which enables the local
// zlib-backed FCStd archive implementation in the BRL-CAD plugin build.
// ---------------------------------------------------------------------------
bool ReadFcstdDoc(const std::string& path, FcstdDoc& doc);
#ifdef OPEN2OPEN_HAVE_LIBZIP
// Forward-declare the openNURBS model type in its actual namespace.
// Callers must include "opennurbs.h" themselves.
} // namespace open2open
class ONX_Model;
namespace open2open {
// ---------------------------------------------------------------------------
// Convert an FCStd file to an ONX_Model.
//
// Opens the archive at @p path, reads all B-Rep shapes (*.brp entries),
// converts each shape via OCCTToON_Brep(), and appends to @p model.
// Object name, display colour and layer name are propagated as model
// attributes. Per-face colours from DiffuseColor are stored on the ON_Brep
// using the first face colour as the object colour.
//
// @param path Filesystem path to the .FCStd file.
// @param model Destination model. Existing content is NOT cleared.
// @param tol Linear tolerance passed to OCCTToON_Brep().
// @return Number of shapes successfully converted (0 on total failure).
// ---------------------------------------------------------------------------
int FCStdFileToONX_Model(const std::string& path,
::ONX_Model& model,
double tol = 1e-6);
// ---------------------------------------------------------------------------
// Convert an ONX_Model to a FreeCAD .FCStd archive.
//
// Writes each B-Rep geometry object in @p model as an OCCT BRep text file
// inside the ZIP archive, along with a minimal Document.xml that links
// object names to their shape files, and a GuiDocument.xml with display
// colours.
//
// @param path Output filesystem path for the .FCStd file.
// @param model Source openNURBS model.
// @param tol Linear tolerance passed to ON_BrepToOCCT().
// @return Number of shapes successfully written (0 on total failure).
// ---------------------------------------------------------------------------
int ONX_ModelToFCStdFile(const std::string& path,
const ::ONX_Model& model,
double tol = 1e-6);
#endif // OPEN2OPEN_HAVE_LIBZIP
} // namespace open2open
#endif // OPEN2OPEN_FCSTD_CONVERT_H
@@ -0,0 +1,62 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// geom_convert.h — Low-level NURBS geometry conversion helpers.
//
// These functions translate individual curve and surface objects between
// openNURBS and OpenCASCADE (OCCT) representations. They are the building
// blocks for the higher-level B-Rep topology conversion in brep_convert.h.
#ifndef OPEN2OPEN_GEOM_CONVERT_H
#define OPEN2OPEN_GEOM_CONVERT_H
#include <Standard_Handle.hxx>
#include <Geom_BSplineCurve.hxx>
#include <Geom_BSplineSurface.hxx>
#include <Geom2d_BSplineCurve.hxx>
// Forward declarations from openNURBS
class ON_NurbsCurve;
class ON_NurbsSurface;
namespace open2open {
//--------------------------------------------------------------------
// openNURBS → OCCT
//--------------------------------------------------------------------
/// Convert a 3-D openNURBS NURBS curve to an OCCT Geom_BSplineCurve.
/// @param c Source curve (m_dim must be 3).
/// @return Null handle if the conversion fails (e.g. wrong dimension).
Handle(Geom_BSplineCurve) ON_NurbsCurveToOCCT(const ON_NurbsCurve& c);
/// Convert a 2-D openNURBS NURBS curve (parameter-space trim) to a
/// Geom2d_BSplineCurve.
/// @param c Source curve (m_dim must be 2).
/// @return Null handle on failure.
Handle(Geom2d_BSplineCurve) ON_NurbsCurve2dToOCCT(const ON_NurbsCurve& c);
/// Convert an openNURBS NURBS surface to an OCCT Geom_BSplineSurface.
/// @param s Source surface.
/// @return Null handle on failure.
Handle(Geom_BSplineSurface) ON_NurbsSurfaceToOCCT(const ON_NurbsSurface& s);
//--------------------------------------------------------------------
// OCCT → openNURBS
//--------------------------------------------------------------------
/// Convert an OCCT Geom_BSplineCurve to an openNURBS ON_NurbsCurve.
/// @param c Source curve handle.
/// @param out Destination object (will be overwritten).
/// @return true on success.
bool OCCTCurveToON(const Handle(Geom_BSplineCurve)& c, ON_NurbsCurve& out);
/// Convert an OCCT Geom_BSplineSurface to an openNURBS ON_NurbsSurface.
/// @param s Source surface handle.
/// @param out Destination object (will be overwritten).
/// @return true on success.
bool OCCTSurfaceToON(const Handle(Geom_BSplineSurface)& s, ON_NurbsSurface& out);
} // namespace open2open
#endif // OPEN2OPEN_GEOM_CONVERT_H
@@ -0,0 +1,49 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// mesh_convert.h — Polygonal mesh conversion helpers.
//
// These functions translate polygonal mesh geometry between openNURBS
// (ON_Mesh) and OpenCASCADE (Poly_Triangulation).
#ifndef OPEN2OPEN_MESH_CONVERT_H
#define OPEN2OPEN_MESH_CONVERT_H
#include <Standard_Handle.hxx>
#include <Poly_Triangulation.hxx>
// Forward declaration from openNURBS
class ON_Mesh;
namespace open2open {
/// Convert an openNURBS polygon mesh to an OCCT Poly_Triangulation.
///
/// Quad faces in the openNURBS mesh are triangulated by splitting along
/// the diagonal between vertex 0 and vertex 2 of each quad. Vertex
/// positions, per-vertex normals, and per-vertex texture coordinates
/// are all preserved when present. Vertex colors are not carried over
/// because Poly_Triangulation does not support per-vertex colour data.
///
/// Vertex positions are taken from the double-precision array (m_dV) when
/// it is populated; otherwise from the single-precision array (m_V).
///
/// @param mesh Source openNURBS mesh.
/// @return A new Poly_Triangulation handle, or a null handle if the
/// mesh is empty or the conversion fails.
Handle(Poly_Triangulation) ON_MeshToOCCT(const ON_Mesh& mesh);
/// Convert an OCCT Poly_Triangulation to an openNURBS ON_Mesh.
///
/// Each triangle becomes an ON_MeshFace using the degenerate-quad encoding
/// that openNURBS uses for triangles: vi[2] == vi[3]. Per-vertex normals
/// and UV texture coordinates are copied when the triangulation provides them.
///
/// @param tri Source OCCT triangulation (must not be null).
/// @param mesh Destination mesh (cleared before writing).
/// @return true on success; false if @p tri is null or empty.
bool OCCTToON_Mesh(const Handle(Poly_Triangulation)& tri, ON_Mesh& mesh);
} // namespace open2open
#endif // OPEN2OPEN_MESH_CONVERT_H
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,413 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// geom_convert.cpp — NURBS curve and surface translation between
// openNURBS and OpenCASCADE (OCCT).
#include "open2open/geom_convert.h"
// ---- openNURBS ----
#include "opennurbs.h"
// ---- OCCT ----
#include <TColgp_Array1OfPnt.hxx>
#include <TColgp_Array1OfPnt2d.hxx>
#include <TColgp_Array2OfPnt.hxx>
#include <TColStd_Array1OfReal.hxx>
#include <TColStd_Array1OfInteger.hxx>
#include <TColStd_Array2OfReal.hxx>
#include <Geom_BSplineCurve.hxx>
#include <Geom_BSplineSurface.hxx>
#include <Geom2d_BSplineCurve.hxx>
#include <vector>
namespace open2open {
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/// Convert an openNURBS flat knot array to OCCT (unique knots + multiplicities).
///
/// openNURBS stores `order + cv_count - 2` knot values – omitting one
/// "phantom" knot at each end that OCCT needs. This function builds a
/// temporary extended array of length `num_knots + 2`, prepending
/// @p phantom_lo and appending @p phantom_hi, then deduplicates into
/// the OCCT (knot, multiplicity) format.
///
/// For clamped (non-periodic) curves pass `phantom_lo = flat_knots[0]`
/// and `phantom_hi = flat_knots[num_knots-1]`.
///
/// For uniform (non-clamped, non-periodic) curves pass extrapolated values:
/// `phantom_lo = 2*k[0] - k[1]` and `phantom_hi = 2*k[last] - k[last-1]`.
///
/// For periodic curves pass `periodic = true` and the phantoms are ignored.
static void ExtractKnotsAndMults(const double* flat_knots,
int num_knots,
TColStd_Array1OfReal& knots,
TColStd_Array1OfInteger& mults,
double phantom_lo,
double phantom_hi,
bool periodic = false)
{
if (periodic) {
// Periodic: pass through without adding phantom knots
int num_distinct = 0;
for (int i = 0; i < num_knots; ) {
++num_distinct;
int j = i + 1;
while (j < num_knots && flat_knots[j] == flat_knots[i]) ++j;
i = j;
}
knots.Resize(1, num_distinct, Standard_False);
mults.Resize(1, num_distinct, Standard_False);
int idx = 1, i = 0;
while (i < num_knots) {
int j = i + 1;
while (j < num_knots && flat_knots[j] == flat_knots[i]) ++j;
knots.SetValue(idx, flat_knots[i]);
mults.SetValue(idx, j - i);
++idx; i = j;
}
return;
}
// Build extended array: [phantom_lo] + flat_knots[] + [phantom_hi]
const int ext_len = num_knots + 2;
std::vector<double> ext(ext_len);
ext[0] = phantom_lo;
for (int k = 0; k < num_knots; ++k)
ext[k + 1] = flat_knots[k];
ext[ext_len - 1] = phantom_hi;
// Count distinct values in the extended array
int num_distinct = 0;
for (int i = 0; i < ext_len; ) {
++num_distinct;
int j = i + 1;
while (j < ext_len && ext[j] == ext[i]) ++j;
i = j;
}
knots.Resize(1, num_distinct, Standard_False);
mults.Resize(1, num_distinct, Standard_False);
int idx = 1, i = 0;
while (i < ext_len) {
int j = i + 1;
while (j < ext_len && ext[j] == ext[i]) ++j;
knots.SetValue(idx, ext[i]);
mults.SetValue(idx, j - i);
++idx; i = j;
}
}
// ---------------------------------------------------------------------------
// ON_NurbsCurve → Geom_BSplineCurve (3-D)
// ---------------------------------------------------------------------------
Handle(Geom_BSplineCurve) ON_NurbsCurveToOCCT(const ON_NurbsCurve& c)
{
if (c.m_dim != 3 || c.m_order < 2 || c.m_cv_count < c.m_order ||
!c.m_cv || !c.m_knot)
return Handle(Geom_BSplineCurve)();
const int degree = c.m_order - 1;
const int num_poles = c.m_cv_count;
const bool rational = (c.m_is_rat != 0);
// ON_IsKnotVectorPeriodic detects uniform knot spacing at both ends.
// We always build a non-periodic OCCT B-spline (the evaluation is
// identical) and add the two phantom knots that openNURBS omits.
// True closed curves are handled as non-periodic with a domain
// restricted to the interior knot span.
// --- control points (OCCT is 1-indexed, Euclidean) ---
TColgp_Array1OfPnt poles(1, num_poles);
TColStd_Array1OfReal weights(1, num_poles);
for (int i = 0; i < num_poles; ++i) {
const double* cv = c.m_cv + static_cast<std::ptrdiff_t>(i) * c.m_cv_stride;
if (rational) {
double w = cv[c.m_dim]; // homogeneous weight
if (w == 0.0) return Handle(Geom_BSplineCurve)();
poles.SetValue(i + 1, gp_Pnt(cv[0] / w, cv[1] / w, cv[2] / w));
weights.SetValue(i + 1, w);
} else {
poles.SetValue(i + 1, gp_Pnt(cv[0], cv[1], cv[2]));
weights.SetValue(i + 1, 1.0);
}
}
// --- knot vector ---
// openNURBS omits one "phantom" knot at each end; OCCT requires them.
// For clamped ends the phantom = boundary knot value.
// For non-clamped ends extrapolate by one knot-step.
const int num_flat_knots = c.m_order + c.m_cv_count - 2;
const double k0 = c.m_knot[0];
const double k1 = c.m_knot[1];
const double kn1 = c.m_knot[num_flat_knots - 2];
const double kn = c.m_knot[num_flat_knots - 1];
const double phantom_lo = (k0 != k1) ? 2.0 * k0 - k1 : k0;
const double phantom_hi = (kn != kn1) ? 2.0 * kn - kn1 : kn;
TColStd_Array1OfReal knots(1, 1);
TColStd_Array1OfInteger mults(1, 1);
ExtractKnotsAndMults(c.m_knot, num_flat_knots, knots, mults,
phantom_lo, phantom_hi, false);
try {
if (rational) {
return new Geom_BSplineCurve(poles, weights, knots, mults,
degree, false);
} else {
return new Geom_BSplineCurve(poles, knots, mults,
degree, false);
}
} catch (...) {
return Handle(Geom_BSplineCurve)();
}
}
// ---------------------------------------------------------------------------
// ON_NurbsCurve → Geom2d_BSplineCurve (2-D parameter-space)
// ---------------------------------------------------------------------------
Handle(Geom2d_BSplineCurve) ON_NurbsCurve2dToOCCT(const ON_NurbsCurve& c)
{
if (c.m_dim != 2 || c.m_order < 2 || c.m_cv_count < c.m_order ||
!c.m_cv || !c.m_knot)
return Handle(Geom2d_BSplineCurve)();
const int degree = c.m_order - 1;
const int num_poles = c.m_cv_count;
const bool rational = (c.m_is_rat != 0);
TColgp_Array1OfPnt2d poles(1, num_poles);
TColStd_Array1OfReal weights(1, num_poles);
for (int i = 0; i < num_poles; ++i) {
const double* cv = c.m_cv + static_cast<std::ptrdiff_t>(i) * c.m_cv_stride;
if (rational) {
double w = cv[c.m_dim];
if (w == 0.0) return Handle(Geom2d_BSplineCurve)();
poles.SetValue(i + 1, gp_Pnt2d(cv[0] / w, cv[1] / w));
weights.SetValue(i + 1, w);
} else {
poles.SetValue(i + 1, gp_Pnt2d(cv[0], cv[1]));
weights.SetValue(i + 1, 1.0);
}
}
const int num_flat_knots = c.m_order + c.m_cv_count - 2;
const double k2_0 = c.m_knot[0];
const double k2_1 = c.m_knot[1];
const double k2_n1 = c.m_knot[num_flat_knots - 2];
const double k2_n = c.m_knot[num_flat_knots - 1];
const double plo2 = (k2_0 != k2_1) ? 2.0 * k2_0 - k2_1 : k2_0;
const double phi2 = (k2_n != k2_n1) ? 2.0 * k2_n - k2_n1 : k2_n;
TColStd_Array1OfReal knots(1, 1);
TColStd_Array1OfInteger mults(1, 1);
ExtractKnotsAndMults(c.m_knot, num_flat_knots, knots, mults,
plo2, phi2, false);
try {
if (rational) {
return new Geom2d_BSplineCurve(poles, weights, knots, mults,
degree, false);
} else {
return new Geom2d_BSplineCurve(poles, knots, mults,
degree, false);
}
} catch (...) {
return Handle(Geom2d_BSplineCurve)();
}
}
// ---------------------------------------------------------------------------
// ON_NurbsSurface → Geom_BSplineSurface
// ---------------------------------------------------------------------------
Handle(Geom_BSplineSurface) ON_NurbsSurfaceToOCCT(const ON_NurbsSurface& s)
{
if (s.m_dim != 3 || s.m_order[0] < 2 || s.m_order[1] < 2 ||
s.m_cv_count[0] < s.m_order[0] || s.m_cv_count[1] < s.m_order[1] ||
!s.m_cv || !s.m_knot[0] || !s.m_knot[1])
return Handle(Geom_BSplineSurface)();
const int udeg = s.m_order[0] - 1;
const int vdeg = s.m_order[1] - 1;
const int nu = s.m_cv_count[0];
const int nv = s.m_cv_count[1];
const bool rational = (s.m_is_rat != 0);
// OCCT Array2: rows = U index (1..nu), cols = V index (1..nv)
TColgp_Array2OfPnt poles(1, nu, 1, nv);
TColStd_Array2OfReal weights(1, nu, 1, nv);
for (int i = 0; i < nu; ++i) {
for (int j = 0; j < nv; ++j) {
const double* cv = s.m_cv
+ static_cast<std::ptrdiff_t>(i) * s.m_cv_stride[0]
+ static_cast<std::ptrdiff_t>(j) * s.m_cv_stride[1];
if (rational) {
double w = cv[s.m_dim];
if (w == 0.0) return Handle(Geom_BSplineSurface)();
poles.SetValue(i + 1, j + 1,
gp_Pnt(cv[0] / w, cv[1] / w, cv[2] / w));
weights.SetValue(i + 1, j + 1, w);
} else {
poles.SetValue(i + 1, j + 1,
gp_Pnt(cv[0], cv[1], cv[2]));
weights.SetValue(i + 1, j + 1, 1.0);
}
}
}
const int nu_knots = s.m_order[0] + s.m_cv_count[0] - 2;
const int nv_knots = s.m_order[1] + s.m_cv_count[1] - 2;
// Compute phantom knot values for each parametric direction
auto PhantomLo = [](const double* k, int n) -> double {
return (k[0] != k[1]) ? 2.0 * k[0] - k[1] : k[0];
};
auto PhantomHi = [](const double* k, int n) -> double {
return (k[n-1] != k[n-2]) ? 2.0 * k[n-1] - k[n-2] : k[n-1];
};
TColStd_Array1OfReal uknots(1, 1), vknots(1, 1);
TColStd_Array1OfInteger umults(1, 1), vmults(1, 1);
ExtractKnotsAndMults(s.m_knot[0], nu_knots, uknots, umults,
PhantomLo(s.m_knot[0], nu_knots),
PhantomHi(s.m_knot[0], nu_knots), false);
ExtractKnotsAndMults(s.m_knot[1], nv_knots, vknots, vmults,
PhantomLo(s.m_knot[1], nv_knots),
PhantomHi(s.m_knot[1], nv_knots), false);
try {
if (rational) {
return new Geom_BSplineSurface(poles, weights,
uknots, vknots,
umults, vmults,
udeg, vdeg,
false, false);
} else {
return new Geom_BSplineSurface(poles,
uknots, vknots,
umults, vmults,
udeg, vdeg,
false, false);
}
} catch (...) {
return Handle(Geom_BSplineSurface)();
}
}
// ---------------------------------------------------------------------------
// Geom_BSplineCurve → ON_NurbsCurve (3-D)
// ---------------------------------------------------------------------------
bool OCCTCurveToON(const Handle(Geom_BSplineCurve)& c, ON_NurbsCurve& out)
{
if (c.IsNull()) return false;
const int degree = c->Degree();
const int num_poles = c->NbPoles();
const bool rational = c->IsRational() ? true : false;
if (!out.Create(3, rational, degree + 1, num_poles))
return false;
// --- control points ---
for (int i = 1; i <= num_poles; ++i) {
gp_Pnt p = c->Pole(i);
double w = rational ? c->Weight(i) : 1.0;
// openNURBS rational curves store homogeneous coordinates
out.SetCV(i - 1,
ON_4dPoint(p.X() * w, p.Y() * w, p.Z() * w, w));
}
// --- knot vector ---
// OCCT stores sum(mults) = num_poles + degree + 1 (non-periodic).
// openNURBS stores sum(flat) = num_poles + degree - 1 (2 fewer).
// The conversion always added one phantom knot at each end so we
// always strip exactly one from the first and last multiplicity groups.
const int num_knot_groups = c->NbKnots();
int ki = 0;
for (int j = 1; j <= num_knot_groups; ++j) {
double val = c->Knot(j);
int mult = c->Multiplicity(j);
if (j == 1 || j == num_knot_groups)
mult -= 1;
for (int m = 0; m < mult; ++m) {
out.m_knot[ki++] = val;
}
}
return out.IsValid() ? true : false;
}
// ---------------------------------------------------------------------------
// Geom_BSplineSurface → ON_NurbsSurface
// ---------------------------------------------------------------------------
bool OCCTSurfaceToON(const Handle(Geom_BSplineSurface)& s, ON_NurbsSurface& out)
{
if (s.IsNull()) return false;
const int udeg = s->UDegree();
const int vdeg = s->VDegree();
const int nu = s->NbUPoles();
const int nv = s->NbVPoles();
const bool rational = s->IsURational() || s->IsVRational();
if (!out.Create(3, rational, udeg + 1, vdeg + 1, nu, nv))
return false;
// --- control points ---
for (int i = 1; i <= nu; ++i) {
for (int j = 1; j <= nv; ++j) {
gp_Pnt p = s->Pole(i, j);
double w = rational ? s->Weight(i, j) : 1.0;
out.SetCV(i - 1, j - 1,
ON_4dPoint(p.X() * w, p.Y() * w, p.Z() * w, w));
}
}
// --- U knots ---
{
int ki = 0;
const int nku = s->NbUKnots();
// OCCT stores sum(mults) = nu + udeg + 1 (non-periodic).
// openNURBS stores sum(flat) = nu + udeg - 1 (2 fewer).
// Whether clamped or non-clamped, the conversion always added one
// phantom knot at each end, so we always strip one from each end.
for (int j = 1; j <= nku; ++j) {
double val = s->UKnot(j);
int mult = s->UMultiplicity(j);
if (j == 1 || j == nku)
mult -= 1;
for (int m = 0; m < mult; ++m)
out.m_knot[0][ki++] = val;
}
}
// --- V knots ---
{
int ki = 0;
const int nkv = s->NbVKnots();
for (int j = 1; j <= nkv; ++j) {
double val = s->VKnot(j);
int mult = s->VMultiplicity(j);
if (j == 1 || j == nkv)
mult -= 1;
for (int m = 0; m < mult; ++m)
out.m_knot[1][ki++] = val;
}
}
return out.IsValid() ? true : false;
}
} // namespace open2open
@@ -0,0 +1,171 @@
// Copyright (c) 2026, open2open contributors
// SPDX-License-Identifier: MIT
//
// mesh_convert.cpp — Polygonal mesh translation between openNURBS and OCCT.
#include "open2open/mesh_convert.h"
// ---- openNURBS ----
#include "opennurbs.h"
// ---- OCCT ----
#include <Poly_Triangulation.hxx>
#include <Poly_Triangle.hxx>
#include <gp_Pnt.hxx>
#include <gp_Pnt2d.hxx>
#include <gp_Vec3f.hxx>
namespace open2open {
// ---------------------------------------------------------------------------
// ON_Mesh → Poly_Triangulation
// ---------------------------------------------------------------------------
Handle(Poly_Triangulation) ON_MeshToOCCT(const ON_Mesh& mesh)
{
const int nv = mesh.m_V.Count();
const int nf = mesh.m_F.Count();
if (nv <= 0 || nf <= 0)
return Handle(Poly_Triangulation)();
// Count output triangles: quads become 2 triangles, tris stay as 1.
int ntri = 0;
for (int fi = 0; fi < nf; ++fi) {
const ON_MeshFace& f = mesh.m_F[fi];
ntri += f.IsTriangle() ? 1 : 2;
}
if (ntri == 0)
return Handle(Poly_Triangulation)();
const bool has_uv = mesh.HasTextureCoordinates() &&
mesh.m_T.Count() == nv;
const bool has_normals = mesh.HasVertexNormals() &&
mesh.m_N.Count() == nv;
Handle(Poly_Triangulation) tri =
new Poly_Triangulation(nv, ntri, has_uv, has_normals);
// Copy vertices — prefer double-precision array when available.
const bool use_double = (mesh.m_dV.Count() == nv);
for (int vi = 0; vi < nv; ++vi) {
gp_Pnt pt;
if (use_double) {
const ON_3dPoint& p = mesh.m_dV[vi];
pt = gp_Pnt(p.x, p.y, p.z);
} else {
const ON_3fPoint& p = mesh.m_V[vi];
pt = gp_Pnt(static_cast<double>(p.x),
static_cast<double>(p.y),
static_cast<double>(p.z));
}
tri->SetNode(vi + 1, pt); // Poly_Triangulation is 1-indexed
}
// Copy per-vertex normals.
if (has_normals) {
for (int ni = 0; ni < nv; ++ni) {
const ON_3fVector& n = mesh.m_N[ni];
tri->SetNormal(ni + 1,
gp_Vec3f(static_cast<float>(n.x),
static_cast<float>(n.y),
static_cast<float>(n.z)));
}
}
// Copy per-vertex texture coordinates.
if (has_uv) {
for (int ui = 0; ui < nv; ++ui) {
const ON_2fPoint& uv = mesh.m_T[ui];
tri->SetUVNode(ui + 1,
gp_Pnt2d(static_cast<double>(uv.x),
static_cast<double>(uv.y)));
}
}
// Copy faces — split quads into two triangles.
// ON_MeshFace vi[] is 0-indexed; Poly_Triangle is 1-indexed.
int tidx = 1;
for (int fi = 0; fi < nf; ++fi) {
const ON_MeshFace& f = mesh.m_F[fi];
tri->SetTriangle(tidx++,
Poly_Triangle(f.vi[0] + 1,
f.vi[1] + 1,
f.vi[2] + 1));
if (!f.IsTriangle()) {
// Second triangle covers the quad's second half:
// vertices 0, 2, 3 of the original quad.
tri->SetTriangle(tidx++,
Poly_Triangle(f.vi[0] + 1,
f.vi[2] + 1,
f.vi[3] + 1));
}
}
return tri;
}
// ---------------------------------------------------------------------------
// Poly_Triangulation → ON_Mesh
// ---------------------------------------------------------------------------
bool OCCTToON_Mesh(const Handle(Poly_Triangulation)& tri, ON_Mesh& mesh)
{
if (tri.IsNull())
return false;
const int nv = tri->NbNodes();
const int nt = tri->NbTriangles();
if (nv <= 0 || nt <= 0)
return false;
mesh.Destroy();
// Allocate and fill vertex array.
mesh.m_V.SetCapacity(nv);
mesh.m_V.SetCount(nv);
for (int vi = 1; vi <= nv; ++vi) {
const gp_Pnt& p = tri->Node(vi);
mesh.m_V[vi - 1] = ON_3fPoint(static_cast<float>(p.X()),
static_cast<float>(p.Y()),
static_cast<float>(p.Z()));
}
// Allocate and fill per-vertex normal array.
if (tri->HasNormals()) {
mesh.m_N.SetCapacity(nv);
mesh.m_N.SetCount(nv);
for (int ni = 1; ni <= nv; ++ni) {
gp_Vec3f n;
tri->Normal(ni, n);
mesh.m_N[ni - 1] = ON_3fVector(n.x(), n.y(), n.z());
}
}
// Allocate and fill per-vertex UV array.
if (tri->HasUVNodes()) {
mesh.m_T.SetCapacity(nv);
mesh.m_T.SetCount(nv);
for (int ui = 1; ui <= nv; ++ui) {
const gp_Pnt2d& uv = tri->UVNode(ui);
mesh.m_T[ui - 1] = ON_2fPoint(static_cast<float>(uv.X()),
static_cast<float>(uv.Y()));
}
}
// Allocate and fill face array.
// openNURBS encodes triangles as degenerate quads: vi[2] == vi[3].
mesh.m_F.SetCapacity(nt);
mesh.m_F.SetCount(nt);
for (int ti = 1; ti <= nt; ++ti) {
const Poly_Triangle& t = tri->Triangle(ti);
ON_MeshFace& f = mesh.m_F[ti - 1];
f.vi[0] = t.Value(1) - 1; // convert 1-indexed → 0-indexed
f.vi[1] = t.Value(2) - 1;
f.vi[2] = t.Value(3) - 1;
f.vi[3] = f.vi[2]; // degenerate-quad triangle encoding
}
return true;
}
} // namespace open2open