Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export to Shapeways SVX voxel format. #143

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ lib/fab/v2syntax.yy.hpp

.project


tags
5 changes: 4 additions & 1 deletion app/app.pro
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ TEMPLATE = app

INCLUDEPATH += src

LIBS += -L../lib/fab -lSbFab -L../lib/graph -lSbGraph
LIBS += -L../lib/fab -lSbFab -L../lib/graph -lSbGraph -L../lib/quazip -lquazip -lz
INCLUDEPATH += ../lib/fab/inc
INCLUDEPATH += ../lib/graph/inc

PRE_TARGETDEPS += ../lib/graph/libSbGraph.a
PRE_TARGETDEPS += ../lib/fab/libSbFab.a
PRE_TARGETDEPS += ../lib/quazip/libquazip.a

include(../qt/common.pri)
include(../qt/python.pri)
Expand Down Expand Up @@ -78,6 +79,7 @@ SOURCES += \
src/render/render_image.cpp \
src/export/export_mesh.cpp \
src/export/export_heightmap.cpp \
src/export/export_voxels.cpp \
src/control/control.cpp \
src/control/control_root.cpp \
src/control/proxy.cpp \
Expand Down Expand Up @@ -126,6 +128,7 @@ HEADERS += \
src/export/export_mesh.h \
src/export/export_worker.h \
src/export/export_heightmap.h \
src/export/export_voxels.h \
src/control/control.h \
src/control/control_root.h \
src/control/proxy.h \
Expand Down
170 changes: 170 additions & 0 deletions app/src/export/export_voxels.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#include <Python.h>

#include <QCoreApplication>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QThread>
#include <QTime>
#include <QDebug>
#include <QDir>
#include <QTemporaryDir>

#include <stdlib.h>
#include <string.h>
#include <iomanip>
#include <fstream>

#include "export/export_voxels.h"

#include "ui/dialogs/resolution_dialog.h"
#include "ui/dialogs/exporting_dialog.h"

#include "fab/util/region.h"
#include "fab/tree/eval.h"
#include "fab/formats/lodepng.h"

#include "quazip/JlCompress.h"

void ExportVoxelsWorker::run()
{
float _resolution = resolution;
QString _filename = filename;

if (resolution == -1)
{
auto resolution_dialog = new ResolutionDialog(
bounds, RESOLUTION_DIALOG_3D, UNITLESS, NO_DETECT_FEATURES);
if (!resolution_dialog->exec())
return;
_resolution = resolution_dialog->getResolution();
delete resolution_dialog;
}

if (filename.isEmpty())
_filename = QFileDialog::getSaveFileName(
NULL, "Export Voxels", "", "*.svx");

if (_filename.isEmpty())
return;

if (!QFileInfo(QFileInfo(_filename).path()).isWritable())
{
QMessageBox::critical(NULL, "Export error",
"<b>Export error:</b><br>"
"Target file is not writable.");
return;
}

auto exporting_dialog = new ExportingDialog();

int halt = 0;
auto thread = new QThread();
auto task = new ExportVoxelsTask(
shape, bounds, _resolution,
_filename, &halt);
task->moveToThread(thread);

connect(thread, &QThread::started,
task, &ExportVoxelsTask::render);
connect(task, &ExportVoxelsTask::finished,
thread, &QThread::quit);
connect(thread, &QThread::finished,
thread, &QThread::deleteLater);
connect(thread, &QThread::finished,
task, &ExportVoxelsTask::deleteLater);
connect(thread, &QThread::destroyed,
exporting_dialog, &ExportingDialog::accept);

thread->start();

// If the dialog was cancelled, set the halt flag and wait for the
// thread to finish (processing events all the while).
if (exporting_dialog->exec() == QDialog::Rejected)
{
halt = 1;
while (thread->isRunning())
QCoreApplication::processEvents();
}
delete exporting_dialog;
}

////////////////////////////////////////////////////////////////////////////////

void ExportVoxelsTask::render()
{
const float THRESH = -0.1;

auto ni=uint32_t((bounds.xmax - bounds.xmin) * resolution);
auto nj=uint32_t((bounds.ymax - bounds.ymin) * resolution);
auto nk=uint32_t((bounds.zmax - bounds.zmin) * resolution);

QTemporaryDir outputDir("svx-output");

auto densityPath = outputDir.path() + "/density";
QDir().mkdir(densityPath);

std::ostringstream max_number_width_oss;
max_number_width_oss << (nj - 1);
size_t max_number_width = max_number_width_oss.str().length() + 1;

std::vector<unsigned char> slice_data;
slice_data.resize(ni * nk);

for (unsigned j = 0; j < nj; ++j) {
auto Y = bounds.ymin*(nj - j)/(float)nj + bounds.ymax*j/(float)nj;

for (unsigned i = 0; i < ni; ++i) {
auto X = bounds.xmin*(ni - i)/(float)ni + bounds.xmax*i/(float)ni;

for(unsigned k = 0; k < nk; ++k) {
auto Z = bounds.zmin*(nk - k)/(float)nk + bounds.zmax*k/(float)nk;

float result = eval_f(shape.tree.get(), X, Y, Z);

unsigned char voxel_value = 0;
if(result > 0) {
voxel_value = 0;
}
else if(result < THRESH) {
voxel_value = 255;
}
else {
voxel_value = static_cast<unsigned char>( (result / THRESH) * 255);
}

slice_data[ni*k+i] = voxel_value;
}
}

std::stringstream ss;
ss << densityPath.toStdString();
ss << "/slice";
ss << std::setfill(' ') << std::setw(max_number_width);
ss << j;
ss << ".png";

lodepng::State state;
state.info_raw.colortype = LCT_GREY;
state.info_raw.bitdepth = 8;
state.info_png.color.colortype = LCT_GREY;
state.info_png.color.bitdepth = 8;
state.encoder.auto_convert = 0;
std::vector<unsigned char> buffer;
unsigned error = lodepng::encode(buffer, slice_data, ni, nk, state);
lodepng::save_file(buffer, ss.str().c_str());
}

std::ofstream manifest_file(outputDir.path().toStdString() + "/manifest.xml");
manifest_file << "<?xml version=\"1.0\"?>" << std::endl;
manifest_file << "<grid gridSizeX = \"" << ni << "\" gridSizeY = \"" << nj << "\" gridSizeZ = \"" << nk << "\" voxelSize = \"1.0E-4\" subvoxelBits = \"8\">" << std::endl;
manifest_file << " <channels>" << std::endl;
manifest_file << " <channel type = \"DENSITY\" slices = \"density/slice%" << max_number_width << "d.png\"/>" << std::endl;
manifest_file << " </channels>" << std::endl;
manifest_file << "</grid>" << std::endl;
manifest_file.close();

JlCompress::compressDir(filename, outputDir.path());

emit(finished());
}
47 changes: 47 additions & 0 deletions app/src/export/export_voxels.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#ifndef EXPORT_VOXELS_H
#define EXPORT_VOXELS_H

#include <Python.h>

#include "export/export_worker.h"
#include "fab/types/shape.h"

////////////////////////////////////////////////////////////////////////////////

class ExportVoxelsWorker : public ExportWorker
{
public:
explicit ExportVoxelsWorker(Shape s, Bounds b, QString f, float r)
: ExportWorker(s, b, f, r) {}

void run() override;

protected:
bool detect_features;
};

////////////////////////////////////////////////////////////////////////////////

class ExportVoxelsTask : public QObject
{
Q_OBJECT
public:
explicit ExportVoxelsTask(Shape s, Bounds b, float r,
QString f, volatile int* halt)
: shape(s), bounds(b), resolution(r),
filename(f), halt(halt)
{}
public slots:
void render();
signals:
void finished();
protected:
Shape shape;
Bounds bounds;
float resolution;
QString filename;

volatile int* halt;
};

#endif
42 changes: 42 additions & 0 deletions app/src/graph/hooks/export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "ui/canvas/graph_scene.h"
#include "export/export_mesh.h"
#include "export/export_heightmap.h"
#include "export/export_voxels.h"

#include <QString>

Expand Down Expand Up @@ -123,6 +124,47 @@ object ScriptExportHooks::stl(tuple args, dict kwargs)
return object();
}

object ScriptExportHooks::voxels(tuple args, dict kwargs)
{
ScriptExportHooks* self = extract<ScriptExportHooks*>(args[0])();

if (self->called)
throw AppHooks::Exception(
"Cannot define multiple export tasks in a single script.");
self->called = true;

if (len(args) != 2)
throw AppHooks::Exception(
"export.voxels must be called with shape as first argument.");

Shape shape = get_shape(args);

Bounds bounds = shape.bounds;
if (kwargs.has_key("bounds"))
bounds = get_bounds(kwargs);

// Sanity-check bounds
if (isinf(bounds.xmin) || isinf(bounds.xmax) ||
isinf(bounds.ymin) || isinf(bounds.ymax) ||
isinf(bounds.zmin) || isinf(bounds.zmax))
{
throw AppHooks::Exception(
"Exporting voxels with invalid (infinite) bounds");
}

bool pad = get_pad(kwargs);

if (pad)
bounds = pad_bounds(bounds);

QString filename = get_filename(kwargs);
float resolution = get_resolution(kwargs);;

self->scene->setExportWorker(self->node, new ExportVoxelsWorker(
shape, bounds, filename, resolution));

return object();
}
object ScriptExportHooks::heightmap(tuple args, dict kwargs)
{
ScriptExportHooks* self = extract<ScriptExportHooks*>(args[0])();
Expand Down
4 changes: 4 additions & 0 deletions app/src/graph/hooks/export.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ struct ScriptExportHooks
static boost::python::object heightmap(
boost::python::tuple args,
boost::python::dict kwargs);

static boost::python::object voxels(
boost::python::tuple args,
boost::python::dict kwargs);

static Shape get_shape(boost::python::tuple args);
static Bounds get_bounds(boost::python::dict kwargs);
Expand Down
4 changes: 3 additions & 1 deletion app/src/graph/hooks/hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ BOOST_PYTHON_MODULE(_AppHooks)
);

class_<ScriptExportHooks>("ScriptExportHooks", init<>())
.def("voxels", raw_function(&ScriptExportHooks::voxels),
"FIXME"
)
.def("stl", raw_function(&ScriptExportHooks::stl),
"stl(shape, bounds=None, pad=True, filename=None,\n"
" resolution=None, detect_features=False)\n"
Expand Down Expand Up @@ -145,4 +148,3 @@ void AppHooks::loadDatumHooks(PyObject* g)
PyDict_SetItemString(g, "math", PyImport_ImportModule("math"));
Q_ASSERT(!PyErr_Occurred());
}

4 changes: 2 additions & 2 deletions app/src/ui/dialogs/resolution_dialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include "ui_resolution_dialog.h"
#include "ui/dialogs/resolution_dialog.h"

ResolutionDialog::ResolutionDialog(Bounds bounds, bool dimensions, bool has_units,
ResolutionDialog::ResolutionDialog(Bounds bounds, bool dimensions, bool has_units, bool has_detect_features,
long max_voxels, QWidget* parent)
: QDialog(parent), bounds(bounds), ui(new Ui::ResolutionDialog),
z_bounded(!isinf(bounds.zmax) && !isinf(bounds.zmin))
Expand All @@ -17,7 +17,7 @@ ResolutionDialog::ResolutionDialog(Bounds bounds, bool dimensions, bool has_unit
ui->unit_label->hide();
}

if (dimensions == RESOLUTION_DIALOG_2D)
if (dimensions == RESOLUTION_DIALOG_2D || !has_detect_features)
ui->detect_features->hide();

// Re-do the layout, since things may have just been hidden
Expand Down
5 changes: 4 additions & 1 deletion app/src/ui/dialogs/resolution_dialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ class ResolutionDialog;
#define HAS_UNITS 1
#define UNITLESS 0

#define HAS_DETECT_FEATURES 1
#define NO_DETECT_FEATURES 0

class ResolutionDialog : public QDialog
{
Q_OBJECT
public:
explicit ResolutionDialog(Bounds b, bool dimensions, bool has_units,
explicit ResolutionDialog(Bounds b, bool dimensions, bool has_units, bool has_detect_features=HAS_DETECT_FEATURES,
long max_voxels=(1<<22), QWidget* parent=0);
float getResolution() const;
float getMMperUnit() const;
Expand Down
1 change: 1 addition & 0 deletions lib/fab/fab.pri
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include(lemon.pri)
SOURCES += \
src/fab.cpp \
src/formats/png.c \
src/formats/lodepng.cpp \
src/formats/stl.c \
src/tree/eval.c \
src/tree/math/math_f.c \
Expand Down
Loading