mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-05-16 01:52:11 +00:00
config bundles, project files (3MFs, AMFs). When loading these files,
the caller may decide whether to substitute some of the configuration
values the current PrusaSlicer version does not understand with
some reasonable default value, and whether to report it. If substitution
is disabled, an exception is being thrown as before this commit.
If substitution is enabled, list of substitutions is returned by the
API to be presented to the user. This allows us to introduce for example
new firmware flavor key in PrusaSlicer 2.4 while letting PrusaSlicer
2.3.2 to fall back to some default and to report it to the user.
When slicing from command line, substutions are performed by default
and reported into the console, however substitutions may be either
disabled or made silent with the new "config-compatibility" command
line option.
Substitute enums and bools only. Allow booleans to be parsed as
true: "1", "enabled", "on" case insensitive
false: "0", "disabled", "off" case insensitive
This will allow us in the future for example to switch the draft_shield
boolean to an enum with the following values: "disabled" / "enabled" / "limited".
Added "enum_bitmask.hpp" - support for type safe sets of options.
See for example PresetBundle::load_configbundle(...
LoadConfigBundleAttributes flags) for an example of intended usage.
WIP: GUI for reporting the list of config substitutions needs to be
implemented by @YuSanka.
1366 lines
58 KiB
C++
1366 lines
58 KiB
C++
#include <limits>
|
|
#include <string.h>
|
|
#include <map>
|
|
#include <string>
|
|
#include <expat.h>
|
|
|
|
#include <boost/nowide/cstdio.hpp>
|
|
|
|
#include "../libslic3r.h"
|
|
#include "../Exception.hpp"
|
|
#include "../Model.hpp"
|
|
#include "../GCode.hpp"
|
|
#include "../PrintConfig.hpp"
|
|
#include "../Utils.hpp"
|
|
#include "../I18N.hpp"
|
|
#include "../Geometry.hpp"
|
|
#include "../CustomGCode.hpp"
|
|
#include "../LocalesUtils.hpp"
|
|
|
|
#include "AMF.hpp"
|
|
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/property_tree/xml_parser.hpp>
|
|
namespace pt = boost::property_tree;
|
|
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/nowide/fstream.hpp>
|
|
#include "miniz_extension.hpp"
|
|
|
|
#if 0
|
|
// Enable debugging and assert in this file.
|
|
#define DEBUG
|
|
#define _DEBUG
|
|
#undef NDEBUG
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
|
|
// VERSION NUMBERS
|
|
// 0 : .amf, .amf.xml and .zip.amf files saved by older slic3r. No version definition in them.
|
|
// 1 : Introduction of amf versioning. No other change in data saved into amf files.
|
|
// 2 : Added z component of offset
|
|
// Added x and y components of rotation
|
|
// Added x, y and z components of scale
|
|
// Added x, y and z components of mirror
|
|
// 3 : Added volumes' matrices and source data, meshes transformed back to their coordinate system on loading.
|
|
// WARNING !! -> the version number has been rolled back to 2
|
|
// the next change should use 4
|
|
const unsigned int VERSION_AMF = 2;
|
|
const unsigned int VERSION_AMF_COMPATIBLE = 3;
|
|
const char* SLIC3RPE_AMF_VERSION = "slic3rpe_amf_version";
|
|
|
|
const char* SLIC3R_CONFIG_TYPE = "slic3rpe_config";
|
|
|
|
namespace Slic3r
|
|
{
|
|
|
|
//! macro used to mark string used at localization,
|
|
//! return same string
|
|
#define L(s) (s)
|
|
#define _(s) Slic3r::I18N::translate(s)
|
|
|
|
struct AMFParserContext
|
|
{
|
|
AMFParserContext(XML_Parser parser, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model) :
|
|
m_parser(parser),
|
|
m_model(*model),
|
|
m_config(config),
|
|
m_config_substitutions(config_substitutions)
|
|
{
|
|
m_path.reserve(12);
|
|
}
|
|
|
|
void stop(const std::string &msg = std::string())
|
|
{
|
|
assert(! m_error);
|
|
assert(m_error_message.empty());
|
|
m_error = true;
|
|
m_error_message = msg;
|
|
XML_StopParser(m_parser, 0);
|
|
}
|
|
|
|
bool error() const { return m_error; }
|
|
const char* error_message() const {
|
|
return m_error ?
|
|
// The error was signalled by the user code, not the expat parser.
|
|
(m_error_message.empty() ? "Invalid AMF format" : m_error_message.c_str()) :
|
|
// The error was signalled by the expat parser.
|
|
XML_ErrorString(XML_GetErrorCode(m_parser));
|
|
}
|
|
|
|
void startElement(const char *name, const char **atts);
|
|
void endElement(const char *name);
|
|
void endDocument();
|
|
void characters(const XML_Char *s, int len);
|
|
|
|
static void XMLCALL startElement(void *userData, const char *name, const char **atts)
|
|
{
|
|
AMFParserContext *ctx = (AMFParserContext*)userData;
|
|
ctx->startElement(name, atts);
|
|
}
|
|
|
|
static void XMLCALL endElement(void *userData, const char *name)
|
|
{
|
|
AMFParserContext *ctx = (AMFParserContext*)userData;
|
|
ctx->endElement(name);
|
|
}
|
|
|
|
/* s is not 0 terminated. */
|
|
static void XMLCALL characters(void *userData, const XML_Char *s, int len)
|
|
{
|
|
AMFParserContext *ctx = (AMFParserContext*)userData;
|
|
ctx->characters(s, len);
|
|
}
|
|
|
|
static const char* get_attribute(const char **atts, const char *id) {
|
|
if (atts == nullptr)
|
|
return nullptr;
|
|
while (*atts != nullptr) {
|
|
if (strcmp(*(atts ++), id) == 0)
|
|
return *atts;
|
|
++ atts;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
enum AMFNodeType {
|
|
NODE_TYPE_INVALID = 0,
|
|
NODE_TYPE_UNKNOWN,
|
|
NODE_TYPE_AMF, // amf
|
|
// amf/metadata
|
|
NODE_TYPE_MATERIAL, // amf/material
|
|
// amf/material/metadata
|
|
NODE_TYPE_OBJECT, // amf/object
|
|
// amf/object/metadata
|
|
NODE_TYPE_LAYER_CONFIG, // amf/object/layer_config_ranges
|
|
NODE_TYPE_RANGE, // amf/object/layer_config_ranges/range
|
|
// amf/object/layer_config_ranges/range/metadata
|
|
NODE_TYPE_MESH, // amf/object/mesh
|
|
NODE_TYPE_VERTICES, // amf/object/mesh/vertices
|
|
NODE_TYPE_VERTEX, // amf/object/mesh/vertices/vertex
|
|
NODE_TYPE_COORDINATES, // amf/object/mesh/vertices/vertex/coordinates
|
|
NODE_TYPE_COORDINATE_X, // amf/object/mesh/vertices/vertex/coordinates/x
|
|
NODE_TYPE_COORDINATE_Y, // amf/object/mesh/vertices/vertex/coordinates/y
|
|
NODE_TYPE_COORDINATE_Z, // amf/object/mesh/vertices/vertex/coordinates/z
|
|
NODE_TYPE_VOLUME, // amf/object/mesh/volume
|
|
// amf/object/mesh/volume/metadata
|
|
NODE_TYPE_TRIANGLE, // amf/object/mesh/volume/triangle
|
|
NODE_TYPE_VERTEX1, // amf/object/mesh/volume/triangle/v1
|
|
NODE_TYPE_VERTEX2, // amf/object/mesh/volume/triangle/v2
|
|
NODE_TYPE_VERTEX3, // amf/object/mesh/volume/triangle/v3
|
|
NODE_TYPE_CONSTELLATION, // amf/constellation
|
|
NODE_TYPE_INSTANCE, // amf/constellation/instance
|
|
NODE_TYPE_DELTAX, // amf/constellation/instance/deltax
|
|
NODE_TYPE_DELTAY, // amf/constellation/instance/deltay
|
|
NODE_TYPE_DELTAZ, // amf/constellation/instance/deltaz
|
|
NODE_TYPE_RX, // amf/constellation/instance/rx
|
|
NODE_TYPE_RY, // amf/constellation/instance/ry
|
|
NODE_TYPE_RZ, // amf/constellation/instance/rz
|
|
NODE_TYPE_SCALE, // amf/constellation/instance/scale
|
|
NODE_TYPE_SCALEX, // amf/constellation/instance/scalex
|
|
NODE_TYPE_SCALEY, // amf/constellation/instance/scaley
|
|
NODE_TYPE_SCALEZ, // amf/constellation/instance/scalez
|
|
NODE_TYPE_MIRRORX, // amf/constellation/instance/mirrorx
|
|
NODE_TYPE_MIRRORY, // amf/constellation/instance/mirrory
|
|
NODE_TYPE_MIRRORZ, // amf/constellation/instance/mirrorz
|
|
NODE_TYPE_PRINTABLE, // amf/constellation/instance/mirrorz
|
|
NODE_TYPE_CUSTOM_GCODE, // amf/custom_code_per_height
|
|
NODE_TYPE_GCODE_PER_HEIGHT, // amf/custom_code_per_height/code
|
|
NODE_TYPE_CUSTOM_GCODE_MODE, // amf/custom_code_per_height/mode
|
|
NODE_TYPE_METADATA, // anywhere under amf/*/metadata
|
|
};
|
|
|
|
struct Instance {
|
|
Instance()
|
|
: deltax_set(false), deltay_set(false), deltaz_set(false)
|
|
, rx_set(false), ry_set(false), rz_set(false)
|
|
, scalex_set(false), scaley_set(false), scalez_set(false)
|
|
, mirrorx_set(false), mirrory_set(false), mirrorz_set(false)
|
|
, printable(true) {}
|
|
// Shift in the X axis.
|
|
float deltax;
|
|
bool deltax_set;
|
|
// Shift in the Y axis.
|
|
float deltay;
|
|
bool deltay_set;
|
|
// Shift in the Z axis.
|
|
float deltaz;
|
|
bool deltaz_set;
|
|
// Rotation around the X axis.
|
|
float rx;
|
|
bool rx_set;
|
|
// Rotation around the Y axis.
|
|
float ry;
|
|
bool ry_set;
|
|
// Rotation around the Z axis.
|
|
float rz;
|
|
bool rz_set;
|
|
// Scaling factors
|
|
float scalex;
|
|
bool scalex_set;
|
|
float scaley;
|
|
bool scaley_set;
|
|
float scalez;
|
|
bool scalez_set;
|
|
// Mirroring factors
|
|
float mirrorx;
|
|
bool mirrorx_set;
|
|
float mirrory;
|
|
bool mirrory_set;
|
|
float mirrorz;
|
|
bool mirrorz_set;
|
|
// printable property
|
|
bool printable;
|
|
|
|
bool anything_set() const { return deltax_set || deltay_set || deltaz_set ||
|
|
rx_set || ry_set || rz_set ||
|
|
scalex_set || scaley_set || scalez_set ||
|
|
mirrorx_set || mirrory_set || mirrorz_set; }
|
|
};
|
|
|
|
struct Object {
|
|
Object() : idx(-1) {}
|
|
int idx;
|
|
std::vector<Instance> instances;
|
|
};
|
|
|
|
// Version of the amf file
|
|
unsigned int m_version { 0 };
|
|
// Current Expat XML parser instance.
|
|
XML_Parser m_parser;
|
|
// Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state
|
|
// after returning from XML_Parse() function, thus we keep the error state here.
|
|
bool m_error { false };
|
|
std::string m_error_message;
|
|
// Model to receive objects extracted from an AMF file.
|
|
Model &m_model;
|
|
// Current parsing path in the XML file.
|
|
std::vector<AMFNodeType> m_path;
|
|
// Current object allocated for an amf/object XML subtree.
|
|
ModelObject *m_object { nullptr };
|
|
// Map from obect name to object idx & instances.
|
|
std::map<std::string, Object> m_object_instances_map;
|
|
// Vertices parsed for the current m_object.
|
|
std::vector<float> m_object_vertices;
|
|
// Current volume allocated for an amf/object/mesh/volume subtree.
|
|
ModelVolume *m_volume { nullptr };
|
|
// Faces collected for the current m_volume.
|
|
std::vector<int> m_volume_facets;
|
|
// Transformation matrix of a volume mesh from its coordinate system to Object's coordinate system.
|
|
Transform3d m_volume_transform;
|
|
// Current material allocated for an amf/metadata subtree.
|
|
ModelMaterial *m_material { nullptr };
|
|
// Current instance allocated for an amf/constellation/instance subtree.
|
|
Instance *m_instance { nullptr };
|
|
// Generic string buffer for vertices, face indices, metadata etc.
|
|
std::string m_value[5];
|
|
// Pointer to config to update if config data are stored inside the amf file
|
|
DynamicPrintConfig *m_config { nullptr };
|
|
// Config substitution rules and collected config substitution log.
|
|
ConfigSubstitutionContext *m_config_substitutions { nullptr };
|
|
|
|
private:
|
|
AMFParserContext& operator=(AMFParserContext&);
|
|
};
|
|
|
|
void AMFParserContext::startElement(const char *name, const char **atts)
|
|
{
|
|
AMFNodeType node_type_new = NODE_TYPE_UNKNOWN;
|
|
switch (m_path.size()) {
|
|
case 0:
|
|
// An AMF file must start with an <amf> tag.
|
|
node_type_new = NODE_TYPE_AMF;
|
|
if (strcmp(name, "amf") != 0)
|
|
this->stop();
|
|
break;
|
|
case 1:
|
|
if (strcmp(name, "metadata") == 0) {
|
|
const char *type = get_attribute(atts, "type");
|
|
if (type != nullptr) {
|
|
m_value[0] = type;
|
|
node_type_new = NODE_TYPE_METADATA;
|
|
}
|
|
} else if (strcmp(name, "material") == 0) {
|
|
const char *material_id = get_attribute(atts, "id");
|
|
m_material = m_model.add_material((material_id == nullptr) ? "_" : material_id);
|
|
node_type_new = NODE_TYPE_MATERIAL;
|
|
} else if (strcmp(name, "object") == 0) {
|
|
const char *object_id = get_attribute(atts, "id");
|
|
if (object_id == nullptr)
|
|
this->stop();
|
|
else {
|
|
assert(m_object_vertices.empty());
|
|
m_object = m_model.add_object();
|
|
m_object_instances_map[object_id].idx = int(m_model.objects.size())-1;
|
|
node_type_new = NODE_TYPE_OBJECT;
|
|
}
|
|
} else if (strcmp(name, "constellation") == 0) {
|
|
node_type_new = NODE_TYPE_CONSTELLATION;
|
|
} else if (strcmp(name, "custom_gcodes_per_height") == 0) {
|
|
node_type_new = NODE_TYPE_CUSTOM_GCODE;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (strcmp(name, "metadata") == 0) {
|
|
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
|
|
m_value[0] = get_attribute(atts, "type");
|
|
node_type_new = NODE_TYPE_METADATA;
|
|
}
|
|
} else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
|
|
node_type_new = NODE_TYPE_LAYER_CONFIG;
|
|
else if (strcmp(name, "mesh") == 0) {
|
|
if (m_path[1] == NODE_TYPE_OBJECT)
|
|
node_type_new = NODE_TYPE_MESH;
|
|
} else if (strcmp(name, "instance") == 0) {
|
|
if (m_path[1] == NODE_TYPE_CONSTELLATION) {
|
|
const char *object_id = get_attribute(atts, "objectid");
|
|
if (object_id == nullptr)
|
|
this->stop();
|
|
else {
|
|
m_object_instances_map[object_id].instances.push_back(AMFParserContext::Instance());
|
|
m_instance = &m_object_instances_map[object_id].instances.back();
|
|
node_type_new = NODE_TYPE_INSTANCE;
|
|
}
|
|
}
|
|
else
|
|
this->stop();
|
|
}
|
|
else if (m_path[1] == NODE_TYPE_CUSTOM_GCODE) {
|
|
if (strcmp(name, "code") == 0) {
|
|
node_type_new = NODE_TYPE_GCODE_PER_HEIGHT;
|
|
m_value[0] = get_attribute(atts, "print_z");
|
|
m_value[1] = get_attribute(atts, "extruder");
|
|
m_value[2] = get_attribute(atts, "color");
|
|
if (get_attribute(atts, "type"))
|
|
{
|
|
m_value[3] = get_attribute(atts, "type");
|
|
m_value[4] = get_attribute(atts, "extra");
|
|
}
|
|
else
|
|
{
|
|
// It means that data was saved in old version (2.2.0 and older) of PrusaSlicer
|
|
// read old data ...
|
|
std::string gcode = get_attribute(atts, "gcode");
|
|
// ... and interpret them to the new data
|
|
CustomGCode::Type type= gcode == "M600" ? CustomGCode::ColorChange :
|
|
gcode == "M601" ? CustomGCode::PausePrint :
|
|
gcode == "tool_change" ? CustomGCode::ToolChange : CustomGCode::Custom;
|
|
m_value[3] = std::to_string(static_cast<int>(type));
|
|
m_value[4] = type == CustomGCode::PausePrint ? m_value[2] :
|
|
type == CustomGCode::Custom ? gcode : "";
|
|
}
|
|
}
|
|
else if (strcmp(name, "mode") == 0) {
|
|
node_type_new = NODE_TYPE_CUSTOM_GCODE_MODE;
|
|
m_value[0] = get_attribute(atts, "value");
|
|
}
|
|
}
|
|
break;
|
|
case 3:
|
|
if (m_path[2] == NODE_TYPE_MESH) {
|
|
assert(m_object);
|
|
if (strcmp(name, "vertices") == 0)
|
|
node_type_new = NODE_TYPE_VERTICES;
|
|
else if (strcmp(name, "volume") == 0) {
|
|
assert(! m_volume);
|
|
m_volume = m_object->add_volume(TriangleMesh());
|
|
m_volume_transform = Transform3d::Identity();
|
|
node_type_new = NODE_TYPE_VOLUME;
|
|
}
|
|
} else if (m_path[2] == NODE_TYPE_INSTANCE) {
|
|
assert(m_instance);
|
|
if (strcmp(name, "deltax") == 0)
|
|
node_type_new = NODE_TYPE_DELTAX;
|
|
else if (strcmp(name, "deltay") == 0)
|
|
node_type_new = NODE_TYPE_DELTAY;
|
|
else if (strcmp(name, "deltaz") == 0)
|
|
node_type_new = NODE_TYPE_DELTAZ;
|
|
else if (strcmp(name, "rx") == 0)
|
|
node_type_new = NODE_TYPE_RX;
|
|
else if (strcmp(name, "ry") == 0)
|
|
node_type_new = NODE_TYPE_RY;
|
|
else if (strcmp(name, "rz") == 0)
|
|
node_type_new = NODE_TYPE_RZ;
|
|
else if (strcmp(name, "scalex") == 0)
|
|
node_type_new = NODE_TYPE_SCALEX;
|
|
else if (strcmp(name, "scaley") == 0)
|
|
node_type_new = NODE_TYPE_SCALEY;
|
|
else if (strcmp(name, "scalez") == 0)
|
|
node_type_new = NODE_TYPE_SCALEZ;
|
|
else if (strcmp(name, "scale") == 0)
|
|
node_type_new = NODE_TYPE_SCALE;
|
|
else if (strcmp(name, "mirrorx") == 0)
|
|
node_type_new = NODE_TYPE_MIRRORX;
|
|
else if (strcmp(name, "mirrory") == 0)
|
|
node_type_new = NODE_TYPE_MIRRORY;
|
|
else if (strcmp(name, "mirrorz") == 0)
|
|
node_type_new = NODE_TYPE_MIRRORZ;
|
|
else if (strcmp(name, "printable") == 0)
|
|
node_type_new = NODE_TYPE_PRINTABLE;
|
|
}
|
|
else if (m_path[2] == NODE_TYPE_LAYER_CONFIG && strcmp(name, "range") == 0) {
|
|
assert(m_object);
|
|
node_type_new = NODE_TYPE_RANGE;
|
|
}
|
|
break;
|
|
case 4:
|
|
if (m_path[3] == NODE_TYPE_VERTICES) {
|
|
if (strcmp(name, "vertex") == 0)
|
|
node_type_new = NODE_TYPE_VERTEX;
|
|
} else if (m_path[3] == NODE_TYPE_VOLUME) {
|
|
if (strcmp(name, "metadata") == 0) {
|
|
const char *type = get_attribute(atts, "type");
|
|
if (type == nullptr)
|
|
this->stop();
|
|
else {
|
|
m_value[0] = type;
|
|
node_type_new = NODE_TYPE_METADATA;
|
|
}
|
|
} else if (strcmp(name, "triangle") == 0)
|
|
node_type_new = NODE_TYPE_TRIANGLE;
|
|
}
|
|
else if (m_path[3] == NODE_TYPE_RANGE && strcmp(name, "metadata") == 0) {
|
|
m_value[0] = get_attribute(atts, "type");
|
|
node_type_new = NODE_TYPE_METADATA;
|
|
}
|
|
break;
|
|
case 5:
|
|
if (strcmp(name, "coordinates") == 0) {
|
|
if (m_path[4] == NODE_TYPE_VERTEX) {
|
|
node_type_new = NODE_TYPE_COORDINATES;
|
|
} else
|
|
this->stop();
|
|
} else if (name[0] == 'v' && name[1] >= '1' && name[1] <= '3' && name[2] == 0) {
|
|
if (m_path[4] == NODE_TYPE_TRIANGLE) {
|
|
node_type_new = AMFNodeType(NODE_TYPE_VERTEX1 + name[1] - '1');
|
|
} else
|
|
this->stop();
|
|
}
|
|
break;
|
|
case 6:
|
|
if ((name[0] == 'x' || name[0] == 'y' || name[0] == 'z') && name[1] == 0) {
|
|
if (m_path[5] == NODE_TYPE_COORDINATES)
|
|
node_type_new = AMFNodeType(NODE_TYPE_COORDINATE_X + name[0] - 'x');
|
|
else
|
|
this->stop();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
m_path.push_back(node_type_new);
|
|
}
|
|
|
|
void AMFParserContext::characters(const XML_Char *s, int len)
|
|
{
|
|
if (m_path.back() == NODE_TYPE_METADATA) {
|
|
m_value[1].append(s, len);
|
|
}
|
|
else
|
|
{
|
|
switch (m_path.size()) {
|
|
case 4:
|
|
if (m_path.back() == NODE_TYPE_DELTAX ||
|
|
m_path.back() == NODE_TYPE_DELTAY ||
|
|
m_path.back() == NODE_TYPE_DELTAZ ||
|
|
m_path.back() == NODE_TYPE_RX ||
|
|
m_path.back() == NODE_TYPE_RY ||
|
|
m_path.back() == NODE_TYPE_RZ ||
|
|
m_path.back() == NODE_TYPE_SCALEX ||
|
|
m_path.back() == NODE_TYPE_SCALEY ||
|
|
m_path.back() == NODE_TYPE_SCALEZ ||
|
|
m_path.back() == NODE_TYPE_SCALE ||
|
|
m_path.back() == NODE_TYPE_MIRRORX ||
|
|
m_path.back() == NODE_TYPE_MIRRORY ||
|
|
m_path.back() == NODE_TYPE_MIRRORZ ||
|
|
m_path.back() == NODE_TYPE_PRINTABLE)
|
|
m_value[0].append(s, len);
|
|
break;
|
|
case 6:
|
|
switch (m_path.back()) {
|
|
case NODE_TYPE_VERTEX1: m_value[0].append(s, len); break;
|
|
case NODE_TYPE_VERTEX2: m_value[1].append(s, len); break;
|
|
case NODE_TYPE_VERTEX3: m_value[2].append(s, len); break;
|
|
default: break;
|
|
}
|
|
case 7:
|
|
switch (m_path.back()) {
|
|
case NODE_TYPE_COORDINATE_X: m_value[0].append(s, len); break;
|
|
case NODE_TYPE_COORDINATE_Y: m_value[1].append(s, len); break;
|
|
case NODE_TYPE_COORDINATE_Z: m_value[2].append(s, len); break;
|
|
default: break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AMFParserContext::endElement(const char * /* name */)
|
|
{
|
|
assert(is_decimal_separator_point());
|
|
switch (m_path.back()) {
|
|
|
|
// Constellation transformation:
|
|
case NODE_TYPE_DELTAX:
|
|
assert(m_instance);
|
|
m_instance->deltax = float(atof(m_value[0].c_str()));
|
|
m_instance->deltax_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_DELTAY:
|
|
assert(m_instance);
|
|
m_instance->deltay = float(atof(m_value[0].c_str()));
|
|
m_instance->deltay_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_DELTAZ:
|
|
assert(m_instance);
|
|
m_instance->deltaz = float(atof(m_value[0].c_str()));
|
|
m_instance->deltaz_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_RX:
|
|
assert(m_instance);
|
|
m_instance->rx = float(atof(m_value[0].c_str()));
|
|
m_instance->rx_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_RY:
|
|
assert(m_instance);
|
|
m_instance->ry = float(atof(m_value[0].c_str()));
|
|
m_instance->ry_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_RZ:
|
|
assert(m_instance);
|
|
m_instance->rz = float(atof(m_value[0].c_str()));
|
|
m_instance->rz_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_SCALE:
|
|
assert(m_instance);
|
|
m_instance->scalex = float(atof(m_value[0].c_str()));
|
|
m_instance->scalex_set = true;
|
|
m_instance->scaley = float(atof(m_value[0].c_str()));
|
|
m_instance->scaley_set = true;
|
|
m_instance->scalez = float(atof(m_value[0].c_str()));
|
|
m_instance->scalez_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_SCALEX:
|
|
assert(m_instance);
|
|
m_instance->scalex = float(atof(m_value[0].c_str()));
|
|
m_instance->scalex_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_SCALEY:
|
|
assert(m_instance);
|
|
m_instance->scaley = float(atof(m_value[0].c_str()));
|
|
m_instance->scaley_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_SCALEZ:
|
|
assert(m_instance);
|
|
m_instance->scalez = float(atof(m_value[0].c_str()));
|
|
m_instance->scalez_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_MIRRORX:
|
|
assert(m_instance);
|
|
m_instance->mirrorx = float(atof(m_value[0].c_str()));
|
|
m_instance->mirrorx_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_MIRRORY:
|
|
assert(m_instance);
|
|
m_instance->mirrory = float(atof(m_value[0].c_str()));
|
|
m_instance->mirrory_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_MIRRORZ:
|
|
assert(m_instance);
|
|
m_instance->mirrorz = float(atof(m_value[0].c_str()));
|
|
m_instance->mirrorz_set = true;
|
|
m_value[0].clear();
|
|
break;
|
|
case NODE_TYPE_PRINTABLE:
|
|
assert(m_instance);
|
|
m_instance->printable = bool(atoi(m_value[0].c_str()));
|
|
m_value[0].clear();
|
|
break;
|
|
|
|
// Object vertices:
|
|
case NODE_TYPE_VERTEX:
|
|
assert(m_object);
|
|
// Parse the vertex data
|
|
m_object_vertices.emplace_back((float)atof(m_value[0].c_str()));
|
|
m_object_vertices.emplace_back((float)atof(m_value[1].c_str()));
|
|
m_object_vertices.emplace_back((float)atof(m_value[2].c_str()));
|
|
m_value[0].clear();
|
|
m_value[1].clear();
|
|
m_value[2].clear();
|
|
break;
|
|
|
|
// Faces of the current volume:
|
|
case NODE_TYPE_TRIANGLE:
|
|
assert(m_object && m_volume);
|
|
m_volume_facets.emplace_back(atoi(m_value[0].c_str()));
|
|
m_volume_facets.emplace_back(atoi(m_value[1].c_str()));
|
|
m_volume_facets.emplace_back(atoi(m_value[2].c_str()));
|
|
m_value[0].clear();
|
|
m_value[1].clear();
|
|
m_value[2].clear();
|
|
break;
|
|
|
|
// Closing the current volume. Create an STL from m_volume_facets pointing to m_object_vertices.
|
|
case NODE_TYPE_VOLUME:
|
|
{
|
|
assert(m_object && m_volume);
|
|
TriangleMesh mesh;
|
|
stl_file &stl = mesh.stl;
|
|
stl.stats.type = inmemory;
|
|
stl.stats.number_of_facets = int(m_volume_facets.size() / 3);
|
|
stl.stats.original_num_facets = stl.stats.number_of_facets;
|
|
stl_allocate(&stl);
|
|
|
|
bool has_transform = ! m_volume_transform.isApprox(Transform3d::Identity(), 1e-10);
|
|
for (size_t i = 0; i < m_volume_facets.size();) {
|
|
stl_facet &facet = stl.facet_start[i/3];
|
|
for (unsigned int v = 0; v < 3; ++v)
|
|
{
|
|
unsigned int tri_id = m_volume_facets[i++] * 3;
|
|
if (tri_id < 0 || tri_id + 2 >= m_object_vertices.size()) {
|
|
this->stop("Malformed triangle mesh");
|
|
return;
|
|
}
|
|
facet.vertex[v] = Vec3f(m_object_vertices[tri_id + 0], m_object_vertices[tri_id + 1], m_object_vertices[tri_id + 2]);
|
|
}
|
|
}
|
|
stl_get_size(&stl);
|
|
mesh.repair();
|
|
m_volume->set_mesh(std::move(mesh));
|
|
// stores the volume matrix taken from the metadata, if present
|
|
if (has_transform)
|
|
m_volume->source.transform = Slic3r::Geometry::Transformation(m_volume_transform);
|
|
if (m_volume->source.input_file.empty() && (m_volume->type() == ModelVolumeType::MODEL_PART))
|
|
{
|
|
m_volume->source.object_idx = (int)m_model.objects.size() - 1;
|
|
m_volume->source.volume_idx = (int)m_model.objects.back()->volumes.size() - 1;
|
|
m_volume->center_geometry_after_creation();
|
|
}
|
|
else
|
|
// pass false if the mesh offset has been already taken from the data
|
|
m_volume->center_geometry_after_creation(m_volume->source.input_file.empty());
|
|
|
|
m_volume->calculate_convex_hull();
|
|
m_volume_facets.clear();
|
|
m_volume = nullptr;
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE_OBJECT:
|
|
assert(m_object);
|
|
m_object_vertices.clear();
|
|
m_object = nullptr;
|
|
break;
|
|
|
|
case NODE_TYPE_MATERIAL:
|
|
assert(m_material);
|
|
m_material = nullptr;
|
|
break;
|
|
|
|
case NODE_TYPE_INSTANCE:
|
|
assert(m_instance);
|
|
m_instance = nullptr;
|
|
break;
|
|
|
|
case NODE_TYPE_GCODE_PER_HEIGHT: {
|
|
double print_z = double(atof(m_value[0].c_str()));
|
|
int extruder = atoi(m_value[1].c_str());
|
|
const std::string& color= m_value[2];
|
|
CustomGCode::Type type = static_cast<CustomGCode::Type>(atoi(m_value[3].c_str()));
|
|
const std::string& extra= m_value[4];
|
|
|
|
m_model.custom_gcode_per_print_z.gcodes.push_back(CustomGCode::Item{print_z, type, extruder, color, extra});
|
|
|
|
for (std::string& val: m_value)
|
|
val.clear();
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE_CUSTOM_GCODE_MODE: {
|
|
const std::string& mode = m_value[0];
|
|
|
|
m_model.custom_gcode_per_print_z.mode = mode == CustomGCode::SingleExtruderMode ? CustomGCode::Mode::SingleExtruder :
|
|
mode == CustomGCode::MultiAsSingleMode ? CustomGCode::Mode::MultiAsSingle :
|
|
CustomGCode::Mode::MultiExtruder;
|
|
for (std::string& val: m_value)
|
|
val.clear();
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE_METADATA:
|
|
if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) {
|
|
m_config->load_from_gcode_string(m_value[1].c_str(), *m_config_substitutions);
|
|
}
|
|
else if (strncmp(m_value[0].c_str(), "slic3r.", 7) == 0) {
|
|
const char *opt_key = m_value[0].c_str() + 7;
|
|
if (print_config_def.options.find(opt_key) != print_config_def.options.end()) {
|
|
ModelConfig *config = nullptr;
|
|
if (m_path.size() == 3) {
|
|
if (m_path[1] == NODE_TYPE_MATERIAL && m_material)
|
|
config = &m_material->config;
|
|
else if (m_path[1] == NODE_TYPE_OBJECT && m_object)
|
|
config = &m_object->config;
|
|
}
|
|
else if (m_path.size() == 5 && m_path[3] == NODE_TYPE_VOLUME && m_volume)
|
|
config = &m_volume->config;
|
|
else if (m_path.size() == 5 && m_path[3] == NODE_TYPE_RANGE && m_object && !m_object->layer_config_ranges.empty()) {
|
|
auto it = --m_object->layer_config_ranges.end();
|
|
config = &it->second;
|
|
}
|
|
if (config)
|
|
config->set_deserialize(opt_key, m_value[1], *m_config_substitutions);
|
|
} else if (m_path.size() == 3 && m_path[1] == NODE_TYPE_OBJECT && m_object && strcmp(opt_key, "layer_height_profile") == 0) {
|
|
// Parse object's layer height profile, a semicolon separated list of floats.
|
|
char *p = m_value[1].data();
|
|
std::vector<coordf_t> data;
|
|
for (;;) {
|
|
char *end = strchr(p, ';');
|
|
if (end != nullptr)
|
|
*end = 0;
|
|
data.emplace_back(float(atof(p)));
|
|
if (end == nullptr)
|
|
break;
|
|
p = end + 1;
|
|
}
|
|
m_object->layer_height_profile.set(std::move(data));
|
|
}
|
|
else if (m_path.size() == 3 && m_path[1] == NODE_TYPE_OBJECT && m_object && strcmp(opt_key, "sla_support_points") == 0) {
|
|
// Parse object's layer height profile, a semicolon separated list of floats.
|
|
unsigned char coord_idx = 0;
|
|
Eigen::Matrix<float, 5, 1, Eigen::DontAlign> point(Eigen::Matrix<float, 5, 1, Eigen::DontAlign>::Zero());
|
|
char *p = m_value[1].data();
|
|
for (;;) {
|
|
char *end = strchr(p, ';');
|
|
if (end != nullptr)
|
|
*end = 0;
|
|
|
|
point(coord_idx) = float(atof(p));
|
|
if (++coord_idx == 5) {
|
|
m_object->sla_support_points.push_back(sla::SupportPoint(point));
|
|
coord_idx = 0;
|
|
}
|
|
if (end == nullptr)
|
|
break;
|
|
p = end + 1;
|
|
}
|
|
m_object->sla_points_status = sla::PointsStatus::UserModified;
|
|
}
|
|
else if (m_path.size() == 5 && m_path[1] == NODE_TYPE_OBJECT && m_path[3] == NODE_TYPE_RANGE &&
|
|
m_object && strcmp(opt_key, "layer_height_range") == 0) {
|
|
// Parse object's layer_height_range, a semicolon separated doubles.
|
|
char* p = m_value[1].data();
|
|
char* end = strchr(p, ';');
|
|
*end = 0;
|
|
|
|
const t_layer_height_range range = {double(atof(p)), double(atof(end + 1))};
|
|
m_object->layer_config_ranges[range];
|
|
}
|
|
else if (m_path.size() == 5 && m_path[3] == NODE_TYPE_VOLUME && m_volume) {
|
|
if (strcmp(opt_key, "modifier") == 0) {
|
|
// Is this volume a modifier volume?
|
|
// "modifier" flag comes first in the XML file, so it may be later overwritten by the "type" flag.
|
|
m_volume->set_type((atoi(m_value[1].c_str()) == 1) ? ModelVolumeType::PARAMETER_MODIFIER : ModelVolumeType::MODEL_PART);
|
|
} else if (strcmp(opt_key, "volume_type") == 0) {
|
|
m_volume->set_type(ModelVolume::type_from_string(m_value[1]));
|
|
}
|
|
else if (strcmp(opt_key, "matrix") == 0) {
|
|
m_volume_transform = Slic3r::Geometry::transform3d_from_string(m_value[1]);
|
|
}
|
|
else if (strcmp(opt_key, "source_file") == 0) {
|
|
m_volume->source.input_file = m_value[1];
|
|
}
|
|
else if (strcmp(opt_key, "source_object_id") == 0) {
|
|
m_volume->source.object_idx = ::atoi(m_value[1].c_str());
|
|
}
|
|
else if (strcmp(opt_key, "source_volume_id") == 0) {
|
|
m_volume->source.volume_idx = ::atoi(m_value[1].c_str());
|
|
}
|
|
else if (strcmp(opt_key, "source_offset_x") == 0) {
|
|
m_volume->source.mesh_offset(0) = ::atof(m_value[1].c_str());
|
|
}
|
|
else if (strcmp(opt_key, "source_offset_y") == 0) {
|
|
m_volume->source.mesh_offset(1) = ::atof(m_value[1].c_str());
|
|
}
|
|
else if (strcmp(opt_key, "source_offset_z") == 0) {
|
|
m_volume->source.mesh_offset(2) = ::atof(m_value[1].c_str());
|
|
}
|
|
else if (strcmp(opt_key, "source_in_inches") == 0) {
|
|
m_volume->source.is_converted_from_inches = m_value[1] == "1";
|
|
}
|
|
else if (strcmp(opt_key, "source_in_meters") == 0) {
|
|
m_volume->source.is_converted_from_meters = m_value[1] == "1";
|
|
}
|
|
}
|
|
} else if (m_path.size() == 3) {
|
|
if (m_path[1] == NODE_TYPE_MATERIAL) {
|
|
if (m_material)
|
|
m_material->attributes[m_value[0]] = m_value[1];
|
|
} else if (m_path[1] == NODE_TYPE_OBJECT) {
|
|
if (m_object && m_value[0] == "name")
|
|
m_object->name = std::move(m_value[1]);
|
|
}
|
|
} else if (m_path.size() == 5 && m_path[3] == NODE_TYPE_VOLUME) {
|
|
if (m_volume && m_value[0] == "name")
|
|
m_volume->name = std::move(m_value[1]);
|
|
}
|
|
else if (strncmp(m_value[0].c_str(), SLIC3RPE_AMF_VERSION, strlen(SLIC3RPE_AMF_VERSION)) == 0) {
|
|
m_version = (unsigned int)atoi(m_value[1].c_str());
|
|
}
|
|
|
|
m_value[0].clear();
|
|
m_value[1].clear();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
m_path.pop_back();
|
|
}
|
|
|
|
void AMFParserContext::endDocument()
|
|
{
|
|
for (const auto &object : m_object_instances_map) {
|
|
if (object.second.idx == -1) {
|
|
BOOST_LOG_TRIVIAL(error) << "Undefined object " << object.first.c_str() << " referenced in constellation";
|
|
continue;
|
|
}
|
|
for (const Instance &instance : object.second.instances)
|
|
if (instance.anything_set()) {
|
|
ModelInstance *mi = m_model.objects[object.second.idx]->add_instance();
|
|
mi->set_offset(Vec3d(instance.deltax_set ? (double)instance.deltax : 0.0, instance.deltay_set ? (double)instance.deltay : 0.0, instance.deltaz_set ? (double)instance.deltaz : 0.0));
|
|
mi->set_rotation(Vec3d(instance.rx_set ? (double)instance.rx : 0.0, instance.ry_set ? (double)instance.ry : 0.0, instance.rz_set ? (double)instance.rz : 0.0));
|
|
mi->set_scaling_factor(Vec3d(instance.scalex_set ? (double)instance.scalex : 1.0, instance.scaley_set ? (double)instance.scaley : 1.0, instance.scalez_set ? (double)instance.scalez : 1.0));
|
|
mi->set_mirror(Vec3d(instance.mirrorx_set ? (double)instance.mirrorx : 1.0, instance.mirrory_set ? (double)instance.mirrory : 1.0, instance.mirrorz_set ? (double)instance.mirrorz : 1.0));
|
|
mi->printable = instance.printable;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load an AMF file into a provided model.
|
|
bool load_amf_file(const char *path, DynamicPrintConfig *config, ConfigSubstitutionContext *config_substitutions, Model *model)
|
|
{
|
|
if ((path == nullptr) || (model == nullptr))
|
|
return false;
|
|
|
|
XML_Parser parser = XML_ParserCreate(nullptr); // encoding
|
|
if (!parser) {
|
|
BOOST_LOG_TRIVIAL(error) << "Couldn't allocate memory for parser";
|
|
return false;
|
|
}
|
|
|
|
FILE *pFile = boost::nowide::fopen(path, "rt");
|
|
if (pFile == nullptr) {
|
|
BOOST_LOG_TRIVIAL(error) << "Cannot open file " << path;
|
|
return false;
|
|
}
|
|
|
|
AMFParserContext ctx(parser, config, config_substitutions, model);
|
|
XML_SetUserData(parser, (void*)&ctx);
|
|
XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement);
|
|
XML_SetCharacterDataHandler(parser, AMFParserContext::characters);
|
|
|
|
char buff[8192];
|
|
bool result = false;
|
|
for (;;) {
|
|
int len = (int)fread(buff, 1, 8192, pFile);
|
|
if (ferror(pFile)) {
|
|
BOOST_LOG_TRIVIAL(error) << "AMF parser: Read error";
|
|
break;
|
|
}
|
|
int done = feof(pFile);
|
|
if (XML_Parse(parser, buff, len, done) == XML_STATUS_ERROR || ctx.error()) {
|
|
BOOST_LOG_TRIVIAL(error) << "AMF parser: Parse error at line " << int(XML_GetCurrentLineNumber(parser)) << ": " << ctx.error_message();
|
|
break;
|
|
}
|
|
if (done) {
|
|
result = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
XML_ParserFree(parser);
|
|
::fclose(pFile);
|
|
|
|
if (result)
|
|
ctx.endDocument();
|
|
|
|
for (ModelObject* o : model->objects)
|
|
{
|
|
for (ModelVolume* v : o->volumes)
|
|
{
|
|
if (v->source.input_file.empty() && (v->type() == ModelVolumeType::MODEL_PART))
|
|
v->source.input_file = path;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version)
|
|
{
|
|
if (stat.m_uncomp_size == 0)
|
|
{
|
|
BOOST_LOG_TRIVIAL(error) << "Found invalid size";
|
|
close_zip_reader(&archive);
|
|
return false;
|
|
}
|
|
|
|
XML_Parser parser = XML_ParserCreate(nullptr); // encoding
|
|
if (!parser) {
|
|
BOOST_LOG_TRIVIAL(error) << "Couldn't allocate memory for parser";
|
|
close_zip_reader(&archive);
|
|
return false;
|
|
}
|
|
|
|
AMFParserContext ctx(parser, config, config_substitutions, model);
|
|
XML_SetUserData(parser, (void*)&ctx);
|
|
XML_SetElementHandler(parser, AMFParserContext::startElement, AMFParserContext::endElement);
|
|
XML_SetCharacterDataHandler(parser, AMFParserContext::characters);
|
|
|
|
struct CallbackData
|
|
{
|
|
XML_Parser& parser;
|
|
AMFParserContext& ctx;
|
|
const mz_zip_archive_file_stat& stat;
|
|
|
|
CallbackData(XML_Parser& parser, AMFParserContext& ctx, const mz_zip_archive_file_stat& stat) : parser(parser), ctx(ctx), stat(stat) {}
|
|
};
|
|
|
|
CallbackData data(parser, ctx, stat);
|
|
|
|
mz_bool res = 0;
|
|
|
|
try
|
|
{
|
|
res = mz_zip_reader_extract_file_to_callback(&archive, stat.m_filename, [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)->size_t {
|
|
CallbackData* data = (CallbackData*)pOpaque;
|
|
if (!XML_Parse(data->parser, (const char*)pBuf, (int)n, (file_ofs + n == data->stat.m_uncomp_size) ? 1 : 0) || data->ctx.error())
|
|
{
|
|
char error_buf[1024];
|
|
::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", data->ctx.error_message(), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser));
|
|
throw Slic3r::FileIOError(error_buf);
|
|
}
|
|
|
|
return n;
|
|
}, &data, 0);
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
BOOST_LOG_TRIVIAL(error) << "Error reading AMF file: " << e.what();
|
|
close_zip_reader(&archive);
|
|
return false;
|
|
}
|
|
|
|
if (res == 0)
|
|
{
|
|
BOOST_LOG_TRIVIAL(error) << "Error while extracting model data from zip archive";
|
|
close_zip_reader(&archive);
|
|
return false;
|
|
}
|
|
|
|
ctx.endDocument();
|
|
|
|
if (check_version && (ctx.m_version > VERSION_AMF_COMPATIBLE))
|
|
{
|
|
// std::string msg = _(L("The selected amf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible."));
|
|
// throw Slic3r::FileIOError(msg.c_str());
|
|
const std::string msg = (boost::format(_(L("The selected amf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str();
|
|
throw Slic3r::FileIOError(msg);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Load an AMF archive into a provided model.
|
|
bool load_amf_archive(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version)
|
|
{
|
|
if ((path == nullptr) || (model == nullptr))
|
|
return false;
|
|
|
|
mz_zip_archive archive;
|
|
mz_zip_zero_struct(&archive);
|
|
|
|
if (!open_zip_reader(&archive, path))
|
|
{
|
|
BOOST_LOG_TRIVIAL(error) << "Unable to init zip reader";
|
|
return false;
|
|
}
|
|
|
|
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
|
|
|
mz_zip_archive_file_stat stat;
|
|
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
|
|
for (mz_uint i = 0; i < num_entries; ++i)
|
|
{
|
|
if (mz_zip_reader_file_stat(&archive, i, &stat))
|
|
{
|
|
if (boost::iends_with(stat.m_filename, ".amf"))
|
|
{
|
|
try
|
|
{
|
|
if (!extract_model_from_archive(archive, stat, config, config_substitutions, model, check_version))
|
|
{
|
|
close_zip_reader(&archive);
|
|
BOOST_LOG_TRIVIAL(error) << "Archive does not contain a valid model";
|
|
return false;
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
// ensure the zip archive is closed and rethrow the exception
|
|
close_zip_reader(&archive);
|
|
throw Slic3r::FileIOError(e.what());
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0 // forward compatibility
|
|
// we then loop again the entries to read other files stored in the archive
|
|
for (mz_uint i = 0; i < num_entries; ++i)
|
|
{
|
|
if (mz_zip_reader_file_stat(&archive, i, &stat))
|
|
{
|
|
// add code to extract the file
|
|
}
|
|
}
|
|
#endif // forward compatibility
|
|
|
|
close_zip_reader(&archive);
|
|
|
|
for (ModelObject *o : model->objects)
|
|
for (ModelVolume *v : o->volumes)
|
|
if (v->source.input_file.empty() && (v->type() == ModelVolumeType::MODEL_PART))
|
|
v->source.input_file = path;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Load an AMF file into a provided model.
|
|
// If config is not a null pointer, updates it if the amf file/archive contains config data
|
|
bool load_amf(const char* path, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, Model* model, bool check_version)
|
|
{
|
|
CNumericLocalesSetter locales_setter; // use "C" locales and point as a decimal separator
|
|
|
|
if (boost::iends_with(path, ".amf.xml"))
|
|
// backward compatibility with older slic3r output
|
|
return load_amf_file(path, config, config_substitutions, model);
|
|
else if (boost::iends_with(path, ".amf"))
|
|
{
|
|
boost::nowide::ifstream file(path, boost::nowide::ifstream::binary);
|
|
if (!file.good())
|
|
return false;
|
|
|
|
std::string zip_mask(2, '\0');
|
|
file.read(zip_mask.data(), 2);
|
|
file.close();
|
|
|
|
return (zip_mask == "PK") ? load_amf_archive(path, config, config_substitutions, model, check_version) : load_amf_file(path, config, config_substitutions, model);
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool store_amf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources)
|
|
{
|
|
if ((path == nullptr) || (model == nullptr))
|
|
return false;
|
|
|
|
// forces ".zip.amf" extension
|
|
std::string export_path = path;
|
|
if (!boost::iends_with(export_path, ".zip.amf"))
|
|
export_path = boost::filesystem::path(export_path).replace_extension(".zip.amf").string();
|
|
|
|
mz_zip_archive archive;
|
|
mz_zip_zero_struct(&archive);
|
|
|
|
if (!open_zip_writer(&archive, export_path)) return false;
|
|
|
|
std::stringstream stream;
|
|
// https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10
|
|
// Conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double).
|
|
// It is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact.
|
|
// The default value of std::stream precision is 6 digits only!
|
|
stream << std::setprecision(std::numeric_limits<float>::max_digits10);
|
|
stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
stream << "<amf unit=\"millimeter\">\n";
|
|
stream << "<metadata type=\"cad\">Slic3r " << SLIC3R_VERSION << "</metadata>\n";
|
|
stream << "<metadata type=\"" << SLIC3RPE_AMF_VERSION << "\">" << VERSION_AMF << "</metadata>\n";
|
|
|
|
if (config != nullptr)
|
|
{
|
|
std::string str_config = "\n";
|
|
for (const std::string &key : config->keys())
|
|
if (key != "compatible_printers")
|
|
str_config += "; " + key + " = " + config->opt_serialize(key) + "\n";
|
|
stream << "<metadata type=\"" << SLIC3R_CONFIG_TYPE << "\">" << xml_escape(str_config) << "</metadata>\n";
|
|
}
|
|
|
|
for (const auto &material : model->materials) {
|
|
if (material.first.empty())
|
|
continue;
|
|
// note that material-id must never be 0 since it's reserved by the AMF spec
|
|
stream << " <material id=\"" << material.first << "\">\n";
|
|
for (const auto &attr : material.second->attributes)
|
|
stream << " <metadata type=\"" << attr.first << "\">" << attr.second << "</metadata>\n";
|
|
for (const std::string &key : material.second->config.keys())
|
|
stream << " <metadata type=\"slic3r." << key << "\">" << material.second->config.opt_serialize(key) << "</metadata>\n";
|
|
stream << " </material>\n";
|
|
}
|
|
std::string instances;
|
|
for (size_t object_id = 0; object_id < model->objects.size(); ++ object_id) {
|
|
ModelObject *object = model->objects[object_id];
|
|
stream << " <object id=\"" << object_id << "\">\n";
|
|
for (const std::string &key : object->config.keys())
|
|
stream << " <metadata type=\"slic3r." << key << "\">" << object->config.opt_serialize(key) << "</metadata>\n";
|
|
if (!object->name.empty())
|
|
stream << " <metadata type=\"name\">" << xml_escape(object->name) << "</metadata>\n";
|
|
const std::vector<double> &layer_height_profile = object->layer_height_profile.get();
|
|
if (layer_height_profile.size() >= 4 && (layer_height_profile.size() % 2) == 0) {
|
|
// Store the layer height profile as a single semicolon separated list.
|
|
stream << " <metadata type=\"slic3r.layer_height_profile\">";
|
|
stream << layer_height_profile.front();
|
|
for (size_t i = 1; i < layer_height_profile.size(); ++i)
|
|
stream << ";" << layer_height_profile[i];
|
|
stream << "\n </metadata>\n";
|
|
}
|
|
|
|
// Export layer height ranges including the layer range specific config overrides.
|
|
const t_layer_config_ranges& config_ranges = object->layer_config_ranges;
|
|
if (!config_ranges.empty())
|
|
{
|
|
// Store the layer config range as a single semicolon separated list.
|
|
stream << " <layer_config_ranges>\n";
|
|
size_t layer_counter = 0;
|
|
for (const auto &range : config_ranges) {
|
|
stream << " <range id=\"" << layer_counter << "\">\n";
|
|
|
|
stream << " <metadata type=\"slic3r.layer_height_range\">";
|
|
stream << range.first.first << ";" << range.first.second << "</metadata>\n";
|
|
|
|
for (const std::string& key : range.second.keys())
|
|
stream << " <metadata type=\"slic3r." << key << "\">" << range.second.opt_serialize(key) << "</metadata>\n";
|
|
|
|
stream << " </range>\n";
|
|
layer_counter++;
|
|
}
|
|
|
|
stream << " </layer_config_ranges>\n";
|
|
}
|
|
|
|
|
|
const std::vector<sla::SupportPoint>& sla_support_points = object->sla_support_points;
|
|
if (!sla_support_points.empty()) {
|
|
// Store the SLA supports as a single semicolon separated list.
|
|
stream << " <metadata type=\"slic3r.sla_support_points\">";
|
|
for (size_t i = 0; i < sla_support_points.size(); ++i) {
|
|
if (i != 0)
|
|
stream << ";";
|
|
stream << sla_support_points[i].pos(0) << ";" << sla_support_points[i].pos(1) << ";" << sla_support_points[i].pos(2) << ";" << sla_support_points[i].head_front_radius << ";" << sla_support_points[i].is_new_island;
|
|
}
|
|
stream << "\n </metadata>\n";
|
|
}
|
|
|
|
stream << " <mesh>\n";
|
|
stream << " <vertices>\n";
|
|
std::vector<int> vertices_offsets;
|
|
int num_vertices = 0;
|
|
for (ModelVolume *volume : object->volumes) {
|
|
vertices_offsets.push_back(num_vertices);
|
|
if (! volume->mesh().repaired)
|
|
throw Slic3r::FileIOError("store_amf() requires repair()");
|
|
if (! volume->mesh().has_shared_vertices())
|
|
throw Slic3r::FileIOError("store_amf() requires shared vertices");
|
|
const indexed_triangle_set &its = volume->mesh().its;
|
|
const Transform3d& matrix = volume->get_matrix();
|
|
for (size_t i = 0; i < its.vertices.size(); ++i) {
|
|
stream << " <vertex>\n";
|
|
stream << " <coordinates>\n";
|
|
Vec3f v = (matrix * its.vertices[i].cast<double>()).cast<float>();
|
|
stream << " <x>" << v(0) << "</x>\n";
|
|
stream << " <y>" << v(1) << "</y>\n";
|
|
stream << " <z>" << v(2) << "</z>\n";
|
|
stream << " </coordinates>\n";
|
|
stream << " </vertex>\n";
|
|
}
|
|
num_vertices += (int)its.vertices.size();
|
|
}
|
|
stream << " </vertices>\n";
|
|
for (size_t i_volume = 0; i_volume < object->volumes.size(); ++i_volume) {
|
|
ModelVolume *volume = object->volumes[i_volume];
|
|
int vertices_offset = vertices_offsets[i_volume];
|
|
if (volume->material_id().empty())
|
|
stream << " <volume>\n";
|
|
else
|
|
stream << " <volume materialid=\"" << volume->material_id() << "\">\n";
|
|
for (const std::string &key : volume->config.keys())
|
|
stream << " <metadata type=\"slic3r." << key << "\">" << volume->config.opt_serialize(key) << "</metadata>\n";
|
|
if (!volume->name.empty())
|
|
stream << " <metadata type=\"name\">" << xml_escape(volume->name) << "</metadata>\n";
|
|
if (volume->is_modifier())
|
|
stream << " <metadata type=\"slic3r.modifier\">1</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.volume_type\">" << ModelVolume::type_to_string(volume->type()) << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.matrix\">";
|
|
const Transform3d& matrix = volume->get_matrix() * volume->source.transform.get_matrix();
|
|
stream << std::setprecision(std::numeric_limits<double>::max_digits10);
|
|
for (int r = 0; r < 4; ++r)
|
|
{
|
|
for (int c = 0; c < 4; ++c)
|
|
{
|
|
stream << matrix(r, c);
|
|
if ((r != 3) || (c != 3))
|
|
stream << " ";
|
|
}
|
|
}
|
|
stream << "</metadata>\n";
|
|
if (!volume->source.input_file.empty())
|
|
{
|
|
std::string input_file = xml_escape(fullpath_sources ? volume->source.input_file : boost::filesystem::path(volume->source.input_file).filename().string());
|
|
stream << " <metadata type=\"slic3r.source_file\">" << input_file << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.source_object_id\">" << volume->source.object_idx << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.source_volume_id\">" << volume->source.volume_idx << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.source_offset_x\">" << volume->source.mesh_offset(0) << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.source_offset_y\">" << volume->source.mesh_offset(1) << "</metadata>\n";
|
|
stream << " <metadata type=\"slic3r.source_offset_z\">" << volume->source.mesh_offset(2) << "</metadata>\n";
|
|
}
|
|
if (volume->source.is_converted_from_inches)
|
|
stream << " <metadata type=\"slic3r.source_in_inches\">1</metadata>\n";
|
|
if (volume->source.is_converted_from_meters)
|
|
stream << " <metadata type=\"slic3r.source_in_meters\">1</metadata>\n";
|
|
stream << std::setprecision(std::numeric_limits<float>::max_digits10);
|
|
const indexed_triangle_set &its = volume->mesh().its;
|
|
for (size_t i = 0; i < its.indices.size(); ++i) {
|
|
stream << " <triangle>\n";
|
|
for (int j = 0; j < 3; ++j)
|
|
stream << " <v" << j + 1 << ">" << its.indices[i][j] + vertices_offset << "</v" << j + 1 << ">\n";
|
|
stream << " </triangle>\n";
|
|
}
|
|
stream << " </volume>\n";
|
|
}
|
|
stream << " </mesh>\n";
|
|
stream << " </object>\n";
|
|
if (!object->instances.empty()) {
|
|
for (ModelInstance *instance : object->instances) {
|
|
std::stringstream buf;
|
|
buf << " <instance objectid=\"" << object_id << "\">\n"
|
|
<< " <deltax>" << instance->get_offset(X) << "</deltax>\n"
|
|
<< " <deltay>" << instance->get_offset(Y) << "</deltay>\n"
|
|
<< " <deltaz>" << instance->get_offset(Z) << "</deltaz>\n"
|
|
<< " <rx>" << instance->get_rotation(X) << "</rx>\n"
|
|
<< " <ry>" << instance->get_rotation(Y) << "</ry>\n"
|
|
<< " <rz>" << instance->get_rotation(Z) << "</rz>\n"
|
|
<< " <scalex>" << instance->get_scaling_factor(X) << "</scalex>\n"
|
|
<< " <scaley>" << instance->get_scaling_factor(Y) << "</scaley>\n"
|
|
<< " <scalez>" << instance->get_scaling_factor(Z) << "</scalez>\n"
|
|
<< " <mirrorx>" << instance->get_mirror(X) << "</mirrorx>\n"
|
|
<< " <mirrory>" << instance->get_mirror(Y) << "</mirrory>\n"
|
|
<< " <mirrorz>" << instance->get_mirror(Z) << "</mirrorz>\n"
|
|
<< " <printable>" << instance->printable << "</printable>\n"
|
|
<< " </instance>\n";
|
|
|
|
//FIXME missing instance->scaling_factor
|
|
instances.append(buf.str());
|
|
}
|
|
}
|
|
}
|
|
if (! instances.empty()) {
|
|
stream << " <constellation id=\"1\">\n";
|
|
stream << instances;
|
|
stream << " </constellation>\n";
|
|
}
|
|
|
|
if (!model->custom_gcode_per_print_z.gcodes.empty())
|
|
{
|
|
std::string out = "";
|
|
pt::ptree tree;
|
|
|
|
pt::ptree& main_tree = tree.add("custom_gcodes_per_height", "");
|
|
|
|
for (const CustomGCode::Item& code : model->custom_gcode_per_print_z.gcodes)
|
|
{
|
|
pt::ptree& code_tree = main_tree.add("code", "");
|
|
// store custom_gcode_per_print_z gcodes information
|
|
code_tree.put("<xmlattr>.print_z" , code.print_z );
|
|
code_tree.put("<xmlattr>.type" , static_cast<int>(code.type));
|
|
code_tree.put("<xmlattr>.extruder" , code.extruder );
|
|
code_tree.put("<xmlattr>.color" , code.color );
|
|
code_tree.put("<xmlattr>.extra" , code.extra );
|
|
|
|
// add gcode field data for the old version of the PrusaSlicer
|
|
std::string gcode = code.type == CustomGCode::ColorChange ? config->opt_string("color_change_gcode") :
|
|
code.type == CustomGCode::PausePrint ? config->opt_string("pause_print_gcode") :
|
|
code.type == CustomGCode::Template ? config->opt_string("template_custom_gcode") :
|
|
code.type == CustomGCode::ToolChange ? "tool_change" : code.extra;
|
|
code_tree.put("<xmlattr>.gcode" , gcode );
|
|
}
|
|
|
|
pt::ptree& mode_tree = main_tree.add("mode", "");
|
|
// store mode of a custom_gcode_per_print_z
|
|
mode_tree.put("<xmlattr>.value",
|
|
model->custom_gcode_per_print_z.mode == CustomGCode::Mode::SingleExtruder ? CustomGCode::SingleExtruderMode :
|
|
model->custom_gcode_per_print_z.mode == CustomGCode::Mode::MultiAsSingle ?
|
|
CustomGCode::MultiAsSingleMode : CustomGCode::MultiExtruderMode);
|
|
|
|
if (!tree.empty())
|
|
{
|
|
std::ostringstream oss;
|
|
pt::write_xml(oss, tree);
|
|
out = oss.str();
|
|
|
|
size_t del_header_pos = out.find("<custom_gcodes_per_height");
|
|
if (del_header_pos != std::string::npos)
|
|
out.erase(out.begin(), out.begin() + del_header_pos);
|
|
|
|
// Post processing("beautification") of the output string
|
|
boost::replace_all(out, "><code", ">\n <code");
|
|
boost::replace_all(out, "><mode", ">\n <mode");
|
|
boost::replace_all(out, "><", ">\n<");
|
|
|
|
stream << out << "\n";
|
|
}
|
|
}
|
|
|
|
stream << "</amf>\n";
|
|
|
|
std::string internal_amf_filename = boost::ireplace_last_copy(boost::filesystem::path(export_path).filename().string(), ".zip.amf", ".amf");
|
|
std::string out = stream.str();
|
|
|
|
if (!mz_zip_writer_add_mem(&archive, internal_amf_filename.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION))
|
|
{
|
|
close_zip_writer(&archive);
|
|
boost::filesystem::remove(export_path);
|
|
return false;
|
|
}
|
|
|
|
if (!mz_zip_writer_finalize_archive(&archive))
|
|
{
|
|
close_zip_writer(&archive);
|
|
boost::filesystem::remove(export_path);
|
|
return false;
|
|
}
|
|
|
|
close_zip_writer(&archive);
|
|
|
|
return true;
|
|
}
|
|
|
|
}; // namespace Slic3r
|