2015-07-01 18:14:05 +00:00
|
|
|
|
#include "GCode.hpp"
|
2015-07-01 21:14:40 +00:00
|
|
|
|
#include "ExtrusionEntity.hpp"
|
2015-07-02 16:57:40 +00:00
|
|
|
|
#include <algorithm>
|
2015-07-02 18:24:16 +00:00
|
|
|
|
#include <cstdlib>
|
2015-07-01 18:14:05 +00:00
|
|
|
|
|
|
|
|
|
namespace Slic3r {
|
|
|
|
|
|
|
|
|
|
AvoidCrossingPerimeters::AvoidCrossingPerimeters()
|
|
|
|
|
: use_external_mp(false), use_external_mp_once(false), disable_once(true),
|
|
|
|
|
_external_mp(NULL), _layer_mp(NULL)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AvoidCrossingPerimeters::~AvoidCrossingPerimeters()
|
|
|
|
|
{
|
|
|
|
|
if (this->_external_mp != NULL)
|
|
|
|
|
delete this->_external_mp;
|
|
|
|
|
|
|
|
|
|
if (this->_layer_mp != NULL)
|
|
|
|
|
delete this->_layer_mp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
AvoidCrossingPerimeters::init_external_mp(const ExPolygons &islands)
|
|
|
|
|
{
|
|
|
|
|
if (this->_external_mp != NULL)
|
|
|
|
|
delete this->_external_mp;
|
|
|
|
|
|
|
|
|
|
this->_external_mp = new MotionPlanner(islands);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
AvoidCrossingPerimeters::init_layer_mp(const ExPolygons &islands)
|
|
|
|
|
{
|
|
|
|
|
if (this->_layer_mp != NULL)
|
|
|
|
|
delete this->_layer_mp;
|
|
|
|
|
|
|
|
|
|
this->_layer_mp = new MotionPlanner(islands);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Polyline
|
2015-07-01 21:00:52 +00:00
|
|
|
|
AvoidCrossingPerimeters::travel_to(GCode &gcodegen, Point point)
|
2015-07-01 18:14:05 +00:00
|
|
|
|
{
|
|
|
|
|
if (this->use_external_mp || this->use_external_mp_once) {
|
|
|
|
|
// get current origin set in gcodegen
|
|
|
|
|
// (the one that will be used to translate the G-code coordinates by)
|
2015-07-01 21:00:52 +00:00
|
|
|
|
Point scaled_origin = Point::new_scale(gcodegen.origin.x, gcodegen.origin.y);
|
2015-07-01 18:14:05 +00:00
|
|
|
|
|
|
|
|
|
// represent last_pos in absolute G-code coordinates
|
2015-07-01 21:00:52 +00:00
|
|
|
|
Point last_pos = gcodegen.last_pos();
|
2015-07-01 18:14:05 +00:00
|
|
|
|
last_pos.translate(scaled_origin);
|
|
|
|
|
|
|
|
|
|
// represent point in absolute G-code coordinates
|
|
|
|
|
point.translate(scaled_origin);
|
|
|
|
|
|
|
|
|
|
// calculate path
|
|
|
|
|
Polyline travel = this->_external_mp->shortest_path(last_pos, point);
|
|
|
|
|
|
|
|
|
|
// translate the path back into the shifted coordinate system that gcodegen
|
|
|
|
|
// is currently using for writing coordinates
|
|
|
|
|
travel.translate(scaled_origin.negative());
|
|
|
|
|
return travel;
|
|
|
|
|
} else {
|
2015-07-01 21:00:52 +00:00
|
|
|
|
return this->_layer_mp->shortest_path(gcodegen.last_pos(), point);
|
2015-07-01 18:14:05 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 19:01:42 +00:00
|
|
|
|
OozePrevention::OozePrevention()
|
|
|
|
|
: enable(false)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 13:02:20 +00:00
|
|
|
|
std::string
|
|
|
|
|
OozePrevention::pre_toolchange(GCode &gcodegen)
|
|
|
|
|
{
|
|
|
|
|
std::string gcode;
|
|
|
|
|
|
|
|
|
|
// move to the nearest standby point
|
|
|
|
|
if (!this->standby_points.empty()) {
|
|
|
|
|
// get current position in print coordinates
|
|
|
|
|
Pointf3 writer_pos = gcodegen.writer.get_position();
|
|
|
|
|
Point pos = Point::new_scale(writer_pos.x, writer_pos.y);
|
|
|
|
|
|
|
|
|
|
// find standby point
|
|
|
|
|
Point standby_point;
|
|
|
|
|
pos.nearest_point(this->standby_points, &standby_point);
|
|
|
|
|
|
|
|
|
|
/* We don't call gcodegen.travel_to() because we don't need retraction (it was already
|
|
|
|
|
triggered by the caller) nor avoid_crossing_perimeters and also because the coordinates
|
|
|
|
|
of the destination point must not be transformed by origin nor current extruder offset. */
|
|
|
|
|
gcode += gcodegen.writer.travel_to_xy(Pointf::new_unscale(standby_point),
|
|
|
|
|
"move to standby position");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (gcodegen.config.standby_temperature_delta.value != 0) {
|
|
|
|
|
// we assume that heating is always slower than cooling, so no need to block
|
|
|
|
|
gcode += gcodegen.writer.set_temperature
|
|
|
|
|
(this->_get_temp(gcodegen) + gcodegen.config.standby_temperature_delta.value, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
|
|
|
|
OozePrevention::post_toolchange(GCode &gcodegen)
|
|
|
|
|
{
|
|
|
|
|
std::string gcode;
|
|
|
|
|
|
|
|
|
|
if (gcodegen.config.standby_temperature_delta.value != 0) {
|
|
|
|
|
gcode += gcodegen.writer.set_temperature(this->_get_temp(gcodegen), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int
|
|
|
|
|
OozePrevention::_get_temp(GCode &gcodegen)
|
|
|
|
|
{
|
|
|
|
|
return (gcodegen.layer != NULL && gcodegen.layer->id() == 0)
|
|
|
|
|
? gcodegen.config.first_layer_temperature.get_at(gcodegen.writer.extruder()->id)
|
|
|
|
|
: gcodegen.config.temperature.get_at(gcodegen.writer.extruder()->id);
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 18:57:16 +00:00
|
|
|
|
Wipe::Wipe()
|
|
|
|
|
: enable(false)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
Wipe::has_path()
|
|
|
|
|
{
|
|
|
|
|
return !this->path.points.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
Wipe::reset_path()
|
|
|
|
|
{
|
|
|
|
|
this->path = Polyline();
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 21:00:52 +00:00
|
|
|
|
std::string
|
|
|
|
|
Wipe::wipe(GCode &gcodegen, bool toolchange)
|
|
|
|
|
{
|
|
|
|
|
std::string gcode;
|
|
|
|
|
|
|
|
|
|
/* Reduce feedrate a bit; travel speed is often too high to move on existing material.
|
|
|
|
|
Too fast = ripping of existing material; too slow = short wipe path, thus more blob. */
|
|
|
|
|
double wipe_speed = gcodegen.writer.config.travel_speed.value * 0.8;
|
|
|
|
|
|
|
|
|
|
// get the retraction length
|
|
|
|
|
double length = toolchange
|
|
|
|
|
? gcodegen.writer.extruder()->retract_length_toolchange()
|
|
|
|
|
: gcodegen.writer.extruder()->retract_length();
|
|
|
|
|
|
|
|
|
|
if (length > 0) {
|
|
|
|
|
/* Calculate how long we need to travel in order to consume the required
|
|
|
|
|
amount of retraction. In other words, how far do we move in XY at wipe_speed
|
|
|
|
|
for the time needed to consume retract_length at retract_speed? */
|
|
|
|
|
double wipe_dist = scale_(length / gcodegen.writer.extruder()->retract_speed() * wipe_speed);
|
|
|
|
|
|
|
|
|
|
/* Take the stored wipe path and replace first point with the current actual position
|
|
|
|
|
(they might be different, for example, in case of loop clipping). */
|
|
|
|
|
Polyline wipe_path;
|
|
|
|
|
wipe_path.append(gcodegen.last_pos());
|
|
|
|
|
wipe_path.append(
|
|
|
|
|
this->path.points.begin() + 1,
|
|
|
|
|
this->path.points.end()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
wipe_path.clip_end(wipe_path.length() - wipe_dist);
|
|
|
|
|
|
|
|
|
|
// subdivide the retraction in segments
|
|
|
|
|
double retracted = 0;
|
|
|
|
|
Lines lines = wipe_path.lines();
|
|
|
|
|
for (Lines::const_iterator line = lines.begin(); line != lines.end(); ++line) {
|
|
|
|
|
double segment_length = line->length();
|
|
|
|
|
/* Reduce retraction length a bit to avoid effective retraction speed to be greater than the configured one
|
|
|
|
|
due to rounding (TODO: test and/or better math for this) */
|
|
|
|
|
double dE = length * (segment_length / wipe_dist) * 0.95;
|
|
|
|
|
gcode += gcodegen.writer.set_speed(wipe_speed*60);
|
|
|
|
|
gcode += gcodegen.writer.extrude_to_xy(
|
|
|
|
|
gcodegen.point_to_gcode(line->b),
|
|
|
|
|
-dE,
|
|
|
|
|
(std::string)"wipe and retract" + (gcodegen.enable_cooling_markers ? ";_WIPE" : "")
|
|
|
|
|
);
|
|
|
|
|
retracted += dE;
|
|
|
|
|
}
|
|
|
|
|
gcodegen.writer.extruder()->retracted += retracted;
|
|
|
|
|
|
|
|
|
|
// prevent wiping again on same path
|
|
|
|
|
this->reset_path();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 12:31:21 +00:00
|
|
|
|
#define EXTRUDER_CONFIG(OPT) this->config.OPT.get_at(this->writer.extruder()->id)
|
|
|
|
|
|
2015-07-01 19:47:17 +00:00
|
|
|
|
GCode::GCode()
|
2015-07-28 21:29:25 +00:00
|
|
|
|
: placeholder_parser(NULL), enable_loop_clipping(true), enable_cooling_markers(false), layer_count(0),
|
|
|
|
|
layer_index(-1), layer(NULL), first_layer(false), elapsed_time(0), volumetric_speed(0),
|
|
|
|
|
_last_pos_defined(false)
|
2015-07-01 21:00:52 +00:00
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-19 13:49:29 +00:00
|
|
|
|
const Point&
|
|
|
|
|
GCode::last_pos() const
|
2015-07-01 21:00:52 +00:00
|
|
|
|
{
|
|
|
|
|
return this->_last_pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
GCode::set_last_pos(const Point &pos)
|
|
|
|
|
{
|
|
|
|
|
this->_last_pos = pos;
|
|
|
|
|
this->_last_pos_defined = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
GCode::last_pos_defined() const
|
|
|
|
|
{
|
|
|
|
|
return this->_last_pos_defined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
GCode::apply_print_config(const PrintConfig &print_config)
|
|
|
|
|
{
|
|
|
|
|
this->writer.apply_print_config(print_config);
|
|
|
|
|
this->config.apply(print_config);
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 17:33:08 +00:00
|
|
|
|
void
|
|
|
|
|
GCode::set_extruders(const std::vector<unsigned int> &extruder_ids)
|
|
|
|
|
{
|
|
|
|
|
this->writer.set_extruders(extruder_ids);
|
|
|
|
|
|
|
|
|
|
// enable wipe path generation if any extruder has wipe enabled
|
|
|
|
|
this->wipe.enable = false;
|
|
|
|
|
for (std::vector<unsigned int>::const_iterator it = extruder_ids.begin();
|
|
|
|
|
it != extruder_ids.end(); ++it) {
|
|
|
|
|
if (this->config.wipe.get_at(*it)) {
|
|
|
|
|
this->wipe.enable = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 21:00:52 +00:00
|
|
|
|
void
|
|
|
|
|
GCode::set_origin(const Pointf &pointf)
|
|
|
|
|
{
|
|
|
|
|
// if origin increases (goes towards right), last_pos decreases because it goes towards left
|
2015-12-19 13:49:29 +00:00
|
|
|
|
const Point translate(
|
2015-07-01 21:00:52 +00:00
|
|
|
|
scale_(this->origin.x - pointf.x),
|
|
|
|
|
scale_(this->origin.y - pointf.y)
|
|
|
|
|
);
|
|
|
|
|
this->_last_pos.translate(translate);
|
|
|
|
|
this->wipe.path.translate(translate);
|
|
|
|
|
this->origin = pointf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
|
|
|
|
GCode::preamble()
|
|
|
|
|
{
|
|
|
|
|
std::string gcode = this->writer.preamble();
|
|
|
|
|
|
|
|
|
|
/* Perform a *silent* move to z_offset: we need this to initialize the Z
|
|
|
|
|
position of our writer object so that any initial lift taking place
|
|
|
|
|
before the first layer change will raise the extruder from the correct
|
|
|
|
|
initial Z instead of 0. */
|
|
|
|
|
this->writer.travel_to_z(this->config.z_offset.value);
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 17:33:08 +00:00
|
|
|
|
std::string
|
|
|
|
|
GCode::change_layer(const Layer &layer)
|
|
|
|
|
{
|
|
|
|
|
this->layer = &layer;
|
|
|
|
|
this->layer_index++;
|
|
|
|
|
this->first_layer = (layer.id() == 0);
|
|
|
|
|
|
|
|
|
|
// avoid computing islands and overhangs if they're not needed
|
|
|
|
|
if (this->config.avoid_crossing_perimeters) {
|
|
|
|
|
ExPolygons islands;
|
|
|
|
|
union_(layer.slices, &islands, true);
|
|
|
|
|
this->avoid_crossing_perimeters.init_layer_mp(islands);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string gcode;
|
|
|
|
|
if (this->layer_count > 0) {
|
|
|
|
|
gcode += this->writer.update_progress(this->layer_index, this->layer_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
coordf_t z = layer.print_z + this->config.z_offset.value; // in unscaled coordinates
|
|
|
|
|
if (EXTRUDER_CONFIG(retract_layer_change) && this->writer.will_move_z(z)) {
|
|
|
|
|
gcode += this->retract();
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
std::ostringstream comment;
|
|
|
|
|
comment << "move to next layer (" << this->layer_index << ")";
|
|
|
|
|
gcode += this->writer.travel_to_z(z, comment.str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// forget last wiping path as wiping after raising Z is pointless
|
|
|
|
|
this->wipe.reset_path();
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 16:57:40 +00:00
|
|
|
|
std::string
|
2015-07-02 18:24:16 +00:00
|
|
|
|
GCode::extrude(ExtrusionLoop loop, std::string description, double speed)
|
2015-07-02 16:57:40 +00:00
|
|
|
|
{
|
2015-07-02 18:24:16 +00:00
|
|
|
|
// get a copy; don't modify the orientation of the original loop object otherwise
|
|
|
|
|
// next copies (if any) would not detect the correct orientation
|
|
|
|
|
|
|
|
|
|
// extrude all loops ccw
|
|
|
|
|
bool was_clockwise = loop.make_counter_clockwise();
|
|
|
|
|
|
|
|
|
|
// find the point of the loop that is closest to the current extruder position
|
|
|
|
|
// or randomize if requested
|
|
|
|
|
Point last_pos = this->last_pos();
|
|
|
|
|
if (this->config.spiral_vase) {
|
|
|
|
|
loop.split_at(last_pos);
|
|
|
|
|
} else if (this->config.seam_position == spNearest || this->config.seam_position == spAligned) {
|
2015-12-19 15:36:07 +00:00
|
|
|
|
const Polygon polygon = loop.polygon();
|
2015-07-02 18:24:16 +00:00
|
|
|
|
|
|
|
|
|
// simplify polygon in order to skip false positives in concave/convex detection
|
|
|
|
|
// (loop is always ccw as polygon.simplify() only works on ccw polygons)
|
|
|
|
|
Polygons simplified = polygon.simplify(scale_(EXTRUDER_CONFIG(nozzle_diameter))/2);
|
|
|
|
|
|
|
|
|
|
// restore original winding order so that concave and convex detection always happens
|
|
|
|
|
// on the right/outer side of the polygon
|
|
|
|
|
if (was_clockwise) {
|
|
|
|
|
for (Polygons::iterator p = simplified.begin(); p != simplified.end(); ++p)
|
|
|
|
|
p->reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// concave vertices have priority
|
|
|
|
|
Points candidates;
|
|
|
|
|
for (Polygons::const_iterator p = simplified.begin(); p != simplified.end(); ++p) {
|
|
|
|
|
Points concave = p->concave_points(PI*4/3);
|
|
|
|
|
candidates.insert(candidates.end(), concave.begin(), concave.end());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if no concave points were found, look for convex vertices
|
|
|
|
|
if (candidates.empty()) {
|
|
|
|
|
for (Polygons::const_iterator p = simplified.begin(); p != simplified.end(); ++p) {
|
|
|
|
|
Points convex = p->convex_points(PI*2/3);
|
|
|
|
|
candidates.insert(candidates.end(), convex.begin(), convex.end());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retrieve the last start position for this object
|
|
|
|
|
if (this->layer != NULL && this->_seam_position.count(this->layer->object()) > 0) {
|
|
|
|
|
last_pos = this->_seam_position[this->layer->object()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Point point;
|
|
|
|
|
if (this->config.seam_position == spNearest) {
|
|
|
|
|
if (candidates.empty()) candidates = polygon.points;
|
|
|
|
|
last_pos.nearest_point(candidates, &point);
|
|
|
|
|
|
|
|
|
|
// On 32-bit Linux, Clipper will change some point coordinates by 1 unit
|
|
|
|
|
// while performing simplify_polygons(), thus split_at_vertex() won't
|
|
|
|
|
// find them anymore.
|
|
|
|
|
if (!loop.split_at_vertex(point)) loop.split_at(point);
|
|
|
|
|
} else if (!candidates.empty()) {
|
|
|
|
|
Points non_overhang;
|
|
|
|
|
for (Points::const_iterator p = candidates.begin(); p != candidates.end(); ++p) {
|
|
|
|
|
if (!loop.has_overhang_point(*p))
|
|
|
|
|
non_overhang.push_back(*p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!non_overhang.empty())
|
|
|
|
|
candidates = non_overhang;
|
|
|
|
|
|
|
|
|
|
last_pos.nearest_point(candidates, &point);
|
|
|
|
|
if (!loop.split_at_vertex(point)) loop.split_at(point); // see note above
|
|
|
|
|
} else {
|
|
|
|
|
point = last_pos.projection_onto(polygon);
|
|
|
|
|
loop.split_at(point);
|
|
|
|
|
}
|
|
|
|
|
if (this->layer != NULL)
|
|
|
|
|
this->_seam_position[this->layer->object()] = point;
|
|
|
|
|
} else if (this->config.seam_position == spRandom) {
|
|
|
|
|
if (loop.role == elrContourInternalPerimeter) {
|
|
|
|
|
Polygon polygon = loop.polygon();
|
|
|
|
|
Point centroid = polygon.centroid();
|
|
|
|
|
last_pos = Point(polygon.bounding_box().max.x, centroid.y);
|
|
|
|
|
last_pos.rotate(rand() % 2*PI, centroid);
|
|
|
|
|
}
|
|
|
|
|
loop.split_at(last_pos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clip the path to avoid the extruder to get exactly on the first point of the loop;
|
|
|
|
|
// if polyline was shorter than the clipping distance we'd get a null polyline, so
|
|
|
|
|
// we discard it in that case
|
|
|
|
|
double clip_length = this->enable_loop_clipping
|
|
|
|
|
? scale_(EXTRUDER_CONFIG(nozzle_diameter)) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
// get paths
|
|
|
|
|
ExtrusionPaths paths;
|
|
|
|
|
loop.clip_end(clip_length, &paths);
|
|
|
|
|
if (paths.empty()) return "";
|
|
|
|
|
|
|
|
|
|
// apply the small perimeter speed
|
|
|
|
|
if (paths.front().is_perimeter() && loop.length() <= SMALL_PERIMETER_LENGTH) {
|
|
|
|
|
if (speed == -1) speed = this->config.get_abs_value("small_perimeter_speed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extrude along the path
|
|
|
|
|
std::string gcode;
|
|
|
|
|
for (ExtrusionPaths::const_iterator path = paths.begin(); path != paths.end(); ++path)
|
|
|
|
|
gcode += this->_extrude(*path, description, speed);
|
|
|
|
|
|
|
|
|
|
// reset acceleration
|
|
|
|
|
gcode += this->writer.set_acceleration(this->config.default_acceleration.value);
|
|
|
|
|
|
|
|
|
|
if (this->wipe.enable)
|
|
|
|
|
this->wipe.path = paths.front().polyline; // TODO: don't limit wipe to last path
|
|
|
|
|
|
|
|
|
|
// make a little move inwards before leaving loop
|
|
|
|
|
if (paths.back().role == erExternalPerimeter && this->layer != NULL && this->config.perimeters > 1) {
|
|
|
|
|
// detect angle between last and first segment
|
|
|
|
|
// the side depends on the original winding order of the polygon (left for contours, right for holes)
|
|
|
|
|
Point a = paths.front().polyline.points[1]; // second point
|
|
|
|
|
Point b = *(paths.back().polyline.points.end()-3); // second to last point
|
|
|
|
|
if (was_clockwise) {
|
|
|
|
|
// swap points
|
|
|
|
|
Point c = a; a = b; b = c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double angle = paths.front().first_point().ccw_angle(a, b) / 3;
|
|
|
|
|
|
|
|
|
|
// turn left if contour, turn right if hole
|
|
|
|
|
if (was_clockwise) angle *= -1;
|
|
|
|
|
|
|
|
|
|
// create the destination point along the first segment and rotate it
|
|
|
|
|
// we make sure we don't exceed the segment length because we don't know
|
|
|
|
|
// the rotation of the second segment so we might cross the object boundary
|
|
|
|
|
Line first_segment(
|
|
|
|
|
paths.front().polyline.points[0],
|
|
|
|
|
paths.front().polyline.points[1]
|
|
|
|
|
);
|
|
|
|
|
double distance = std::min(
|
|
|
|
|
scale_(EXTRUDER_CONFIG(nozzle_diameter)),
|
|
|
|
|
first_segment.length()
|
|
|
|
|
);
|
|
|
|
|
Point point = first_segment.point_at(distance);
|
|
|
|
|
point.rotate(angle, first_segment.a);
|
|
|
|
|
|
|
|
|
|
// generate the travel move
|
|
|
|
|
gcode += this->writer.travel_to_xy(this->point_to_gcode(point), "move inwards before travel");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
|
|
|
|
GCode::extrude(const ExtrusionEntity &entity, std::string description, double speed)
|
|
|
|
|
{
|
|
|
|
|
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity)) {
|
|
|
|
|
return this->extrude(*path, description, speed);
|
|
|
|
|
} else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity)) {
|
|
|
|
|
return this->extrude(*loop, description, speed);
|
|
|
|
|
} else {
|
|
|
|
|
CONFESS("Invalid argument supplied to extrude()");
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
|
|
|
|
GCode::extrude(const ExtrusionPath &path, std::string description, double speed)
|
|
|
|
|
{
|
|
|
|
|
std::string gcode = this->_extrude(path, description, speed);
|
2015-07-02 16:57:40 +00:00
|
|
|
|
|
|
|
|
|
// reset acceleration
|
|
|
|
|
gcode += this->writer.set_acceleration(this->config.default_acceleration.value);
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
2015-07-02 18:24:16 +00:00
|
|
|
|
GCode::_extrude(ExtrusionPath path, std::string description, double speed)
|
2015-07-02 16:57:40 +00:00
|
|
|
|
{
|
|
|
|
|
path.simplify(SCALED_RESOLUTION);
|
|
|
|
|
|
|
|
|
|
std::string gcode;
|
|
|
|
|
|
|
|
|
|
// go to first point of extrusion path
|
|
|
|
|
if (!this->_last_pos_defined || !this->_last_pos.coincides_with(path.first_point())) {
|
|
|
|
|
gcode += this->travel_to(
|
|
|
|
|
path.first_point(),
|
|
|
|
|
path.role,
|
|
|
|
|
"move to first " + description + " point"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// compensate retraction
|
|
|
|
|
gcode += this->unretract();
|
|
|
|
|
|
|
|
|
|
// adjust acceleration
|
|
|
|
|
{
|
|
|
|
|
double acceleration;
|
|
|
|
|
if (this->config.first_layer_acceleration.value > 0 && this->first_layer) {
|
|
|
|
|
acceleration = this->config.first_layer_acceleration.value;
|
|
|
|
|
} else if (this->config.perimeter_acceleration.value > 0 && path.is_perimeter()) {
|
|
|
|
|
acceleration = this->config.perimeter_acceleration.value;
|
|
|
|
|
} else if (this->config.bridge_acceleration.value > 0 && path.is_bridge()) {
|
|
|
|
|
acceleration = this->config.bridge_acceleration.value;
|
|
|
|
|
} else if (this->config.infill_acceleration.value > 0 && path.is_infill()) {
|
|
|
|
|
acceleration = this->config.infill_acceleration.value;
|
|
|
|
|
} else {
|
|
|
|
|
acceleration = this->config.default_acceleration.value;
|
|
|
|
|
}
|
|
|
|
|
gcode += this->writer.set_acceleration(acceleration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculate extrusion length per distance unit
|
|
|
|
|
double e_per_mm = this->writer.extruder()->e_per_mm3 * path.mm3_per_mm;
|
|
|
|
|
if (this->writer.extrusion_axis().empty()) e_per_mm = 0;
|
|
|
|
|
|
|
|
|
|
// set speed
|
|
|
|
|
if (speed == -1) {
|
|
|
|
|
if (path.role == erPerimeter) {
|
|
|
|
|
speed = this->config.get_abs_value("perimeter_speed");
|
|
|
|
|
} else if (path.role == erExternalPerimeter) {
|
|
|
|
|
speed = this->config.get_abs_value("external_perimeter_speed");
|
|
|
|
|
} else if (path.role == erOverhangPerimeter || path.role == erBridgeInfill) {
|
|
|
|
|
speed = this->config.get_abs_value("bridge_speed");
|
|
|
|
|
} else if (path.role == erInternalInfill) {
|
|
|
|
|
speed = this->config.get_abs_value("infill_speed");
|
|
|
|
|
} else if (path.role == erSolidInfill) {
|
|
|
|
|
speed = this->config.get_abs_value("solid_infill_speed");
|
|
|
|
|
} else if (path.role == erTopSolidInfill) {
|
|
|
|
|
speed = this->config.get_abs_value("top_solid_infill_speed");
|
|
|
|
|
} else if (path.role == erGapFill) {
|
|
|
|
|
speed = this->config.get_abs_value("gap_fill_speed");
|
|
|
|
|
} else {
|
|
|
|
|
CONFESS("Invalid speed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this->first_layer) {
|
|
|
|
|
speed = this->config.get_abs_value("first_layer_speed", speed);
|
|
|
|
|
}
|
|
|
|
|
if (this->volumetric_speed != 0 && speed == 0) {
|
|
|
|
|
speed = this->volumetric_speed / path.mm3_per_mm;
|
|
|
|
|
}
|
|
|
|
|
if (this->config.max_volumetric_speed.value > 0) {
|
|
|
|
|
// cap speed with max_volumetric_speed anyway (even if user is not using autospeed)
|
|
|
|
|
speed = std::min(
|
|
|
|
|
speed,
|
|
|
|
|
this->config.max_volumetric_speed.value / path.mm3_per_mm
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
double F = speed * 60; // convert mm/sec to mm/min
|
|
|
|
|
|
|
|
|
|
// extrude arc or line
|
|
|
|
|
if (path.is_bridge() && this->enable_cooling_markers)
|
|
|
|
|
gcode += ";_BRIDGE_FAN_START\n";
|
2015-07-02 17:14:55 +00:00
|
|
|
|
gcode += this->writer.set_speed(F);
|
|
|
|
|
double path_length = 0;
|
2015-07-02 16:57:40 +00:00
|
|
|
|
{
|
2015-08-10 22:03:45 +00:00
|
|
|
|
std::string comment = this->config.gcode_comments ? description : "";
|
2015-07-02 17:14:55 +00:00
|
|
|
|
Lines lines = path.polyline.lines();
|
|
|
|
|
for (Lines::const_iterator line = lines.begin(); line != lines.end(); ++line) {
|
|
|
|
|
const double line_length = line->length() * SCALING_FACTOR;
|
|
|
|
|
path_length += line_length;
|
|
|
|
|
|
|
|
|
|
gcode += this->writer.extrude_to_xy(
|
|
|
|
|
this->point_to_gcode(line->b),
|
|
|
|
|
e_per_mm * line_length,
|
|
|
|
|
comment
|
|
|
|
|
);
|
2015-07-02 16:57:40 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2015-07-02 17:14:55 +00:00
|
|
|
|
if (this->wipe.enable) {
|
|
|
|
|
this->wipe.path = path.polyline;
|
|
|
|
|
this->wipe.path.reverse();
|
|
|
|
|
}
|
2015-07-02 16:57:40 +00:00
|
|
|
|
if (path.is_bridge() && this->enable_cooling_markers)
|
|
|
|
|
gcode += ";_BRIDGE_FAN_END\n";
|
2015-07-02 17:14:55 +00:00
|
|
|
|
|
2015-07-02 16:57:40 +00:00
|
|
|
|
this->set_last_pos(path.last_point());
|
|
|
|
|
|
|
|
|
|
if (this->config.cooling)
|
|
|
|
|
this->elapsed_time += path_length / F * 60;
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 13:12:04 +00:00
|
|
|
|
// This method accepts &point in print coordinates.
|
|
|
|
|
std::string
|
|
|
|
|
GCode::travel_to(const Point &point, ExtrusionRole role, std::string comment)
|
|
|
|
|
{
|
|
|
|
|
/* Define the travel move as a line between current position and the taget point.
|
|
|
|
|
This is expressed in print coordinates, so it will need to be translated by
|
2015-07-02 16:57:40 +00:00
|
|
|
|
this->origin in order to get G-code coordinates. */
|
2015-07-02 13:12:04 +00:00
|
|
|
|
Polyline travel;
|
|
|
|
|
travel.append(this->last_pos());
|
|
|
|
|
travel.append(point);
|
|
|
|
|
|
|
|
|
|
// check whether a straight travel move would need retraction
|
|
|
|
|
bool needs_retraction = this->needs_retraction(travel, role);
|
|
|
|
|
|
|
|
|
|
// if a retraction would be needed, try to use avoid_crossing_perimeters to plan a
|
|
|
|
|
// multi-hop travel path inside the configuration space
|
|
|
|
|
if (needs_retraction
|
|
|
|
|
&& this->config.avoid_crossing_perimeters
|
|
|
|
|
&& !this->avoid_crossing_perimeters.disable_once) {
|
|
|
|
|
travel = this->avoid_crossing_perimeters.travel_to(*this, point);
|
|
|
|
|
|
|
|
|
|
// check again whether the new travel path still needs a retraction
|
|
|
|
|
needs_retraction = this->needs_retraction(travel, role);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-allow avoid_crossing_perimeters for the next travel moves
|
|
|
|
|
this->avoid_crossing_perimeters.disable_once = false;
|
|
|
|
|
this->avoid_crossing_perimeters.use_external_mp_once = false;
|
|
|
|
|
|
|
|
|
|
// generate G-code for the travel move
|
|
|
|
|
std::string gcode;
|
|
|
|
|
if (needs_retraction) gcode += this->retract();
|
|
|
|
|
|
|
|
|
|
// use G1 because we rely on paths being straight (G0 may make round paths)
|
|
|
|
|
Lines lines = travel.lines();
|
|
|
|
|
for (Lines::const_iterator line = lines.begin(); line != lines.end(); ++line)
|
|
|
|
|
gcode += this->writer.travel_to_xy(this->point_to_gcode(line->b), comment);
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 21:14:40 +00:00
|
|
|
|
bool
|
|
|
|
|
GCode::needs_retraction(const Polyline &travel, ExtrusionRole role)
|
|
|
|
|
{
|
2015-07-02 12:31:21 +00:00
|
|
|
|
if (travel.length() < scale_(EXTRUDER_CONFIG(retract_before_travel))) {
|
2015-07-01 21:14:40 +00:00
|
|
|
|
// skip retraction if the move is shorter than the configured threshold
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (role == erSupportMaterial) {
|
2015-07-02 17:33:08 +00:00
|
|
|
|
const SupportLayer* support_layer = dynamic_cast<const SupportLayer*>(this->layer);
|
2015-07-02 12:29:20 +00:00
|
|
|
|
if (support_layer != NULL && support_layer->support_islands.contains(travel)) {
|
2015-07-01 21:14:40 +00:00
|
|
|
|
// skip retraction if this is a travel move inside a support material island
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this->config.only_retract_when_crossing_perimeters && this->layer != NULL) {
|
|
|
|
|
if (this->config.fill_density.value > 0
|
|
|
|
|
&& this->layer->any_internal_region_slice_contains(travel)) {
|
|
|
|
|
/* skip retraction if travel is contained in an internal slice *and*
|
|
|
|
|
internal infill is enabled (so that stringing is entirely not visible) */
|
|
|
|
|
return false;
|
|
|
|
|
} else if (this->layer->any_bottom_region_slice_contains(travel)
|
|
|
|
|
&& this->layer->upper_layer != NULL
|
|
|
|
|
&& this->layer->upper_layer->slices.contains(travel)
|
|
|
|
|
&& (this->config.bottom_solid_layers.value >= 2 || this->config.fill_density.value > 0)) {
|
|
|
|
|
/* skip retraction if travel is contained in an *infilled* bottom slice
|
|
|
|
|
but only if it's also covered by an *infilled* upper layer's slice
|
|
|
|
|
so that it's not visible from above (a bottom surface might not have an
|
|
|
|
|
upper slice in case of a thin membrane) */
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retract if only_retract_when_crossing_perimeters is disabled or doesn't apply
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 21:00:52 +00:00
|
|
|
|
std::string
|
|
|
|
|
GCode::retract(bool toolchange)
|
|
|
|
|
{
|
|
|
|
|
std::string gcode;
|
|
|
|
|
|
|
|
|
|
if (this->writer.extruder() == NULL)
|
|
|
|
|
return gcode;
|
|
|
|
|
|
|
|
|
|
// wipe (if it's enabled for this extruder and we have a stored wipe path)
|
2015-07-02 12:31:21 +00:00
|
|
|
|
if (EXTRUDER_CONFIG(wipe) && this->wipe.has_path()) {
|
2015-07-01 21:00:52 +00:00
|
|
|
|
gcode += this->wipe.wipe(*this, toolchange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The parent class will decide whether we need to perform an actual retraction
|
|
|
|
|
(the extruder might be already retracted fully or partially). We call these
|
|
|
|
|
methods even if we performed wipe, since this will ensure the entire retraction
|
|
|
|
|
length is honored in case wipe path was too short. */
|
|
|
|
|
gcode += toolchange ? this->writer.retract_for_toolchange() : this->writer.retract();
|
|
|
|
|
|
|
|
|
|
gcode += this->writer.reset_e();
|
|
|
|
|
if (this->writer.extruder()->retract_length() > 0 || this->config.use_firmware_retraction)
|
|
|
|
|
gcode += this->writer.lift();
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string
|
|
|
|
|
GCode::unretract()
|
|
|
|
|
{
|
|
|
|
|
std::string gcode;
|
|
|
|
|
gcode += this->writer.unlift();
|
|
|
|
|
gcode += this->writer.unretract();
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-02 13:02:20 +00:00
|
|
|
|
std::string
|
|
|
|
|
GCode::set_extruder(unsigned int extruder_id)
|
|
|
|
|
{
|
|
|
|
|
this->placeholder_parser->set("current_extruder", extruder_id);
|
|
|
|
|
if (!this->writer.need_toolchange(extruder_id))
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
// if we are running a single-extruder setup, just set the extruder and return nothing
|
|
|
|
|
if (!this->writer.multiple_extruders) {
|
|
|
|
|
return this->writer.toolchange(extruder_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// prepend retraction on the current extruder
|
|
|
|
|
std::string gcode = this->retract(true);
|
|
|
|
|
|
|
|
|
|
// append custom toolchange G-code
|
|
|
|
|
if (this->writer.extruder() != NULL && !this->config.toolchange_gcode.value.empty()) {
|
|
|
|
|
PlaceholderParser pp = *this->placeholder_parser;
|
|
|
|
|
pp.set("previous_extruder", this->writer.extruder()->id);
|
|
|
|
|
pp.set("next_extruder", extruder_id);
|
|
|
|
|
gcode += pp.process(this->config.toolchange_gcode.value) + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if ooze prevention is enabled, park current extruder in the nearest
|
|
|
|
|
// standby point and set it to the standby temperature
|
|
|
|
|
if (this->ooze_prevention.enable && this->writer.extruder() != NULL)
|
|
|
|
|
gcode += this->ooze_prevention.pre_toolchange(*this);
|
|
|
|
|
|
|
|
|
|
// append the toolchange command
|
|
|
|
|
gcode += this->writer.toolchange(extruder_id);
|
|
|
|
|
|
|
|
|
|
// set the new extruder to the operating temperature
|
|
|
|
|
if (this->ooze_prevention.enable)
|
|
|
|
|
gcode += this->ooze_prevention.post_toolchange(*this);
|
|
|
|
|
|
|
|
|
|
return gcode;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 21:00:52 +00:00
|
|
|
|
// convert a model-space scaled point into G-code coordinates
|
|
|
|
|
Pointf
|
|
|
|
|
GCode::point_to_gcode(const Point &point)
|
2015-07-01 19:47:17 +00:00
|
|
|
|
{
|
2015-07-02 12:31:21 +00:00
|
|
|
|
Pointf extruder_offset = EXTRUDER_CONFIG(extruder_offset);
|
2015-07-01 21:00:52 +00:00
|
|
|
|
return Pointf(
|
|
|
|
|
unscale(point.x) + this->origin.x - extruder_offset.x,
|
|
|
|
|
unscale(point.y) + this->origin.y - extruder_offset.y
|
|
|
|
|
);
|
2015-07-01 19:47:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-07-01 18:14:05 +00:00
|
|
|
|
}
|