2018-12-07 14:10:16 +01:00
#include "igl/random_points_on_mesh.h"
#include "igl/AABB.h"
2019-02-06 10:57:45 +01:00
#include <tbb/parallel_for.h>
2018-12-07 14:10:16 +01:00
#include "SLAAutoSupports.hpp"
#include "Model.hpp"
2018-12-14 16:15:59 +01:00
#include "ExPolygon.hpp"
#include "SVG.hpp"
#include "Point.hpp"
2018-12-21 12:57:14 +01:00
#include "ClipperUtils.hpp"
2019-02-17 13:05:22 +01:00
#include "Tesselate.hpp"
#include "libslic3r.h"
2018-12-07 14:10:16 +01:00
#include <iostream>
2018-12-21 12:35:20 +01:00
#include <random>
2018-12-07 14:10:16 +01:00
namespace Slic3r {
2018-12-21 12:35:20 +01:00
2019-02-04 08:40:20 +01:00
/*float SLAAutoSupports::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2)
2018-12-07 14:10:16 +01:00
{
n1.normalize();
n2.normalize();
2018-12-21 12:35:20 +01:00
Vec3d v = (p2-p1);
2018-12-07 14:10:16 +01:00
v.normalize();
float c1 = n1.dot(v);
float c2 = n2.dot(v);
float result = pow(p1(0)-p2(0), 2) + pow(p1(1)-p2(1), 2) + pow(p1(2)-p2(2), 2);
// Check for division by zero:
if(fabs(c1 - c2) > 0.0001)
result *= (asin(c1) - asin(c2)) / (c1 - c2);
return result;
}
float SLAAutoSupports::get_required_density(float angle) const
{
// calculation would be density_0 * cos(angle). To provide one more degree of freedom, we will scale the angle
// to get the user-set density for 45 deg. So it ends up as density_0 * cos(K * angle).
2018-12-07 14:53:24 +01:00
float K = 4.f * float(acos(m_config.density_at_45/m_config.density_at_horizontal) / M_PI);
return std::max(0.f, float(m_config.density_at_horizontal * cos(K*angle)));
2018-12-07 14:10:16 +01:00
}
2018-12-21 12:35:20 +01:00
float SLAAutoSupports::distance_limit(float angle) const
{
return 1./(2.4*get_required_density(angle));
2019-01-30 08:26:23 +01:00
}*/
SLAAutoSupports :: SLAAutoSupports ( const TriangleMesh & mesh , const sla :: EigenMesh3D & emesh , const std :: vector < ExPolygons >& slices , const std :: vector < float >& heights ,
const Config & config , std :: function < void ( void ) > throw_on_cancel )
2019-02-04 09:50:25 +01:00
: m_config ( config ), m_emesh ( emesh ), m_throw_on_cancel ( throw_on_cancel )
2019-01-30 08:26:23 +01:00
{
process ( slices , heights );
project_onto_mesh ( m_output );
}
void SLAAutoSupports :: project_onto_mesh ( std :: vector < sla :: SupportPoint >& points ) const
{
// The function makes sure that all the points are really exactly placed on the mesh.
igl :: Hit hit_up { 0 , 0 , 0.f , 0.f , 0.f };
igl :: Hit hit_down { 0 , 0 , 0.f , 0.f , 0.f };
2019-02-04 09:50:25 +01:00
2019-02-06 10:57:45 +01:00
// Use a reasonable granularity to account for the worker thread synchronization cost.
tbb :: parallel_for ( tbb :: blocked_range < size_t > ( 0 , points . size (), 64 ),
[ this , & points ]( const tbb :: blocked_range < size_t >& range ) {
for ( size_t point_id = range . begin (); point_id < range . end (); ++ point_id ) {
if (( point_id % 16 ) == 0 )
// Don't call the following function too often as it flushes CPU write caches due to synchronization primitves.
m_throw_on_cancel ();
Vec3f & p = points [ point_id ]. pos ;
// Project the point upward and downward and choose the closer intersection with the mesh.
//bool up = igl::ray_mesh_intersect(p.cast<float>(), Vec3f(0., 0., 1.), m_V, m_F, hit_up);
//bool down = igl::ray_mesh_intersect(p.cast<float>(), Vec3f(0., 0., -1.), m_V, m_F, hit_down);
2019-02-04 09:50:25 +01:00
2019-02-06 10:57:45 +01:00
sla :: EigenMesh3D :: hit_result hit_up = m_emesh . query_ray_hit ( p . cast < double > (), Vec3d ( 0. , 0. , 1. ));
sla :: EigenMesh3D :: hit_result hit_down = m_emesh . query_ray_hit ( p . cast < double > (), Vec3d ( 0. , 0. , - 1. ));
2019-02-04 09:50:25 +01:00
2019-02-06 10:57:45 +01:00
bool up = hit_up . face () != - 1 ;
bool down = hit_down . face () != - 1 ;
2019-01-30 08:26:23 +01:00
2019-02-06 10:57:45 +01:00
if ( ! up && ! down )
continue ;
2019-01-30 08:26:23 +01:00
2019-02-06 10:57:45 +01:00
sla :: EigenMesh3D :: hit_result & hit = ( ! down || ( hit_up . distance () < hit_down . distance ())) ? hit_up : hit_down ;
//int fid = hit.face();
//Vec3f bc(1-hit.u-hit.v, hit.u, hit.v);
//p = (bc(0) * m_V.row(m_F(fid, 0)) + bc(1) * m_V.row(m_F(fid, 1)) + bc(2)*m_V.row(m_F(fid, 2))).cast<float>();
2019-02-04 09:50:25 +01:00
2019-02-06 10:57:45 +01:00
p = p + ( hit . distance () * hit . direction ()). cast < float > ();
}
});
2019-01-30 08:26:23 +01:00
}
void SLAAutoSupports :: process ( const std :: vector < ExPolygons >& slices , const std :: vector < float >& heights )
{
std :: vector < std :: pair < ExPolygon , coord_t >> islands ;
2019-02-06 10:57:45 +01:00
std :: vector < Structure > structures_old ;
std :: vector < Structure > structures_new ;
2019-01-30 08:26:23 +01:00
for ( unsigned int i = 0 ; i < slices . size (); ++ i ) {
const ExPolygons & expolys_top = slices [ i ];
2019-02-06 10:57:45 +01:00
//FIXME WTF?
2019-01-30 08:26:23 +01:00
const float height = ( i > 2 ? heights [ i - 3 ] : heights [ 0 ] - ( heights [ 1 ] - heights [ 0 ]));
2019-02-01 16:12:00 +01:00
const float layer_height = ( i != 0 ? heights [ i ] - heights [ i - 1 ] : heights [ 0 ]);
2019-01-30 08:26:23 +01:00
2019-02-06 10:57:45 +01:00
const float safe_angle = 5.f * ( float ( M_PI ) / 180.f ); // smaller number - less supports
const float between_layers_offset = float ( scale_ ( layer_height / std :: tan ( safe_angle )));
2019-02-01 16:12:00 +01:00
// FIXME: calculate actual pixel area from printer config:
//const float pixel_area = pow(wxGetApp().preset_bundle->project_config.option<ConfigOptionFloat>("display_width") / wxGetApp().preset_bundle->project_config.option<ConfigOptionInt>("display_pixels_x"), 2.f); //
const float pixel_area = pow ( 0.047f , 2.f );
2019-01-30 08:26:23 +01:00
// Check all ExPolygons on this slice and check whether they are new or belonging to something below.
for ( const ExPolygon & polygon : expolys_top ) {
2019-02-06 10:57:45 +01:00
float area = float ( polygon . area () * SCALING_FACTOR * SCALING_FACTOR );
if ( area < pixel_area )
2019-01-30 08:26:23 +01:00
continue ;
2019-02-06 10:57:45 +01:00
//FIXME this is not a correct centroid of a polygon with holes.
structures_new . emplace_back ( polygon , get_extents ( polygon . contour ), Slic3r :: unscale ( polygon . contour . centroid ()). cast < float > (), area , height );
Structure & top = structures_new . back ();
//FIXME This has a quadratic time complexity, it will be excessively slow for many tiny islands.
// At least it is now using a bounding box check for pre-filtering.
2019-02-17 13:05:22 +01:00
for ( Structure & bottom : structures_old )
if ( top . overlaps ( bottom )) {
top . structures_below . push_back ( & bottom );
2019-02-06 10:57:45 +01:00
float centroids_dist = ( bottom . centroid - top . centroid ). norm ();
2019-02-04 10:56:49 +01:00
// Penalization resulting from centroid offset:
2019-02-17 13:05:22 +01:00
// bottom.supports_force *= std::min(1.f, 1.f - std::min(1.f, (1600.f * layer_height) * centroids_dist * centroids_dist / bottom.area));
2019-02-06 10:57:45 +01:00
bottom . supports_force *= std :: min ( 1.f , 1.f - std :: min ( 1.f , 80.f * centroids_dist * centroids_dist / bottom . area ));
2019-02-04 10:56:49 +01:00
// Penalization resulting from increasing polygon area:
2019-02-17 13:05:22 +01:00
bottom . supports_force *= std :: min ( 1.f , 20.f * bottom . area / top . area );
2019-01-30 08:26:23 +01:00
}
}
// Let's assign proper support force to each of them:
2019-02-06 10:57:45 +01:00
for ( const Structure & below : structures_old ) {
std :: vector < Structure *> above_list ;
float above_area = 0.f ;
for ( Structure & new_str : structures_new )
for ( const Structure * below1 : new_str . structures_below )
if ( & below == below1 ) {
above_list . push_back ( & new_str );
above_area += above_list . back () -> area ;
2019-01-30 08:26:23 +01:00
}
2019-02-06 10:57:45 +01:00
for ( Structure * above : above_list )
above -> supports_force += below . supports_force * above -> area / above_area ;
2019-01-30 08:26:23 +01:00
}
// Now iterate over all polygons and append new points if needed.
2019-02-06 10:57:45 +01:00
for ( Structure & s : structures_new ) {
if ( s . structures_below . empty ()) // completely new island - needs support no doubt
2019-01-30 08:26:23 +01:00
uniformly_cover ( * s . polygon , s , true );
2019-02-06 10:57:45 +01:00
else
2019-01-30 08:26:23 +01:00
// Let's see if there's anything that overlaps enough to need supports:
// What we now have in polygons needs support, regardless of what the forces are, so we can add them.
2019-02-17 13:05:22 +01:00
for ( const ExPolygon & p : diff_ex ( to_polygons ( * s . polygon ), offset ( s . expolygons_below (), between_layers_offset )))
2019-02-06 10:57:45 +01:00
//FIXME is it an island point or not? Vojtech thinks it is.
2019-01-30 08:26:23 +01:00
uniformly_cover ( p , s );
}
// We should also check if current support is enough given the polygon area.
2019-02-06 10:57:45 +01:00
for ( Structure & s : structures_new ) {
// Areas not supported by the areas below.
ExPolygons e = diff_ex ( to_polygons ( * s . polygon ), s . polygons_below ());
2019-02-17 13:05:22 +01:00
float e_area = 0.f ;
for ( const ExPolygon & ex : e )
e_area += float ( ex . area ());
2019-02-04 10:56:49 +01:00
// Penalization resulting from large diff from the last layer:
2019-02-06 10:57:45 +01:00
// s.supports_force /= std::max(1.f, (layer_height / 0.3f) * e_area / s.area);
2019-02-17 13:05:22 +01:00
s . supports_force /= std :: max ( 1.f , 0.17f * ( e_area * float ( SCALING_FACTOR * SCALING_FACTOR )) / s . area );
2019-02-04 10:56:49 +01:00
2019-02-06 10:57:45 +01:00
if ( s . area * m_config . tear_pressure > s . supports_force ) {
//FIXME Don't calculate area inside the compare function!
//FIXME Cover until the force deficit is covered. Cover multiple areas, sort by decreasing area.
2019-01-30 08:26:23 +01:00
ExPolygons :: iterator largest_it = std :: max_element ( e . begin (), e . end (), []( const ExPolygon & a , const ExPolygon & b ) { return a . area () < b . area (); });
if ( ! e . empty ())
2019-02-06 10:57:45 +01:00
//FIXME add the support force deficit as a parameter, only cover until the defficiency is covered.
2019-01-30 08:26:23 +01:00
uniformly_cover ( * largest_it , s );
}
}
// All is done. Prepare to advance to the next layer.
2019-02-06 10:57:45 +01:00
structures_old = std :: move ( structures_new );
structures_new . clear ();
2019-01-30 08:26:23 +01:00
m_throw_on_cancel ();
#ifdef SLA_AUTOSUPPORTS_DEBUG
/*std::string layer_num_str = std::string((i<10 ? "0" : "")) + std::string((i<100 ? "0" : "")) + std::to_string(i);
output_expolygons(expolys_top, "top" + layer_num_str + ".svg");
output_expolygons(diff, "diff" + layer_num_str + ".svg");
if (!islands.empty())
output_expolygons(islands, "islands" + layer_num_str + ".svg");*/
#endif /* SLA_AUTOSUPPORTS_DEBUG */
}
}
2019-02-17 13:05:22 +01:00
std :: vector < Vec2f > sample_expolygon ( const ExPolygon & expoly , float samples_per_mm2 , std :: mt19937 & rng )
{
// Triangulate the polygon with holes into triplets of 3D points.
std :: vector < Vec2f > triangles = Slic3r :: triangulate_expolygon_2f ( expoly );
std :: vector < Vec2f > out ;
if ( ! triangles . empty ())
{
// Calculate area of each triangle.
std :: vector < float > areas ;
areas . reserve ( triangles . size () / 3 );
for ( size_t i = 0 ; i < triangles . size (); ) {
const Vec2f & a = triangles [ i ++ ];
const Vec2f v1 = triangles [ i ++ ] - a ;
const Vec2f v2 = triangles [ i ++ ] - a ;
areas . emplace_back ( 0.5f * std :: abs ( cross2 ( v1 , v2 )));
if ( i != 3 )
// Prefix sum of the areas.
areas . back () += areas [ areas . size () - 2 ];
}
size_t num_samples = size_t ( ceil ( areas . back () * samples_per_mm2 ));
std :: uniform_real_distribution <> random_triangle ( 0. , double ( areas . back ()));
std :: uniform_real_distribution <> random_float ( 0. , 1. );
for ( size_t i = 0 ; i < num_samples ; ++ i ) {
double r = random_triangle ( rng );
size_t idx_triangle = std :: min < size_t > ( std :: upper_bound ( areas . begin (), areas . end (), ( float ) r ) - areas . begin (), areas . size () - 1 ) * 3 ;
// Select a random point on the triangle.
double u = float ( sqrt ( random_float ( rng )));
double v = float ( random_float ( rng ));
const Vec2f & a = triangles [ idx_triangle ++ ];
const Vec2f & b = triangles [ idx_triangle ++ ];
const Vec2f & c = triangles [ idx_triangle ];
const Vec2f x = a * ( 1.f - u ) + b * ( u * ( 1.f - v )) + c * ( v * u );
out . emplace_back ( x );
}
}
return out ;
}
std :: vector < Vec2f > sample_expolygon_with_boundary ( const ExPolygon & expoly , float samples_per_mm2 , float samples_per_mm_boundary , std :: mt19937 & rng )
{
std :: vector < Vec2f > out = sample_expolygon ( expoly , samples_per_mm2 , rng );
double point_stepping_scaled = scale_ ( 1.f ) / samples_per_mm_boundary ;
for ( size_t i_contour = 0 ; i_contour <= expoly . holes . size (); ++ i_contour ) {
const Polygon & contour = ( i_contour == 0 ) ? expoly . contour : expoly . holes [ i_contour - 1 ];
const Points pts = contour . equally_spaced_points ( point_stepping_scaled );
for ( size_t i = 0 ; i < pts . size (); ++ i )
out . emplace_back ( unscale < float > ( pts [ i ]. x ()), unscale < float > ( pts [ i ]. y ()));
}
return out ;
}
std :: vector < Vec2f > poisson_disk_from_samples ( const std :: vector < Vec2f > & raw_samples , float radius )
{
Vec2f corner_min ( FLT_MAX , FLT_MAX );
for ( const Vec2f & pt : raw_samples ) {
corner_min . x () = std :: min ( corner_min . x (), pt . x ());
corner_min . y () = std :: min ( corner_min . y (), pt . y ());
}
// Assign the raw samples to grid cells, sort the grid cells lexicographically.
struct RawSample {
Vec2f coord ;
Vec2i cell_id ;
};
std :: vector < RawSample > raw_samples_sorted ;
RawSample sample ;
for ( const Vec2f & pt : raw_samples ) {
sample . coord = pt ;
sample . cell_id = (( pt - corner_min ) / radius ). cast < int > ();
raw_samples_sorted . emplace_back ( sample );
}
std :: sort ( raw_samples_sorted . begin (), raw_samples_sorted . end (), []( const RawSample & lhs , const RawSample & rhs )
{ return lhs . cell_id . x () < rhs . cell_id . x () || ( lhs . cell_id . x () == rhs . cell_id . x () && lhs . cell_id . y () < rhs . cell_id . y ()); });
struct PoissonDiskGridEntry {
// Resulting output sample points for this cell:
enum {
max_positions = 4
};
Vec2f poisson_samples [ max_positions ];
int num_poisson_samples = 0 ;
// Index into raw_samples:
int first_sample_idx ;
int sample_cnt ;
};
struct CellIDHash {
std :: size_t operator ()( const Vec2i & cell_id ) {
return std :: hash < int > ()( cell_id . x ()) ^ std :: hash < int > ()( cell_id . y () * 593 );
}
};
// Map from cell IDs to hash_data. Each hash_data points to the range in raw_samples corresponding to that cell.
// (We could just store the samples in hash_data. This implementation is an artifact of the reference paper, which
// is optimizing for GPU acceleration that we haven't implemented currently.)
typedef std :: unordered_map < Vec2i , PoissonDiskGridEntry , CellIDHash > Cells ;
std :: unordered_map < Vec2i , PoissonDiskGridEntry , CellIDHash > cells ;
{
Cells :: iterator last_cell_id_it ;
Vec2i last_cell_id ( - 1 , - 1 );
for ( int i = 0 ; i < raw_samples_sorted . size (); ++ i ) {
const RawSample & sample = raw_samples_sorted [ i ];
if ( sample . cell_id == last_cell_id ) {
// This sample is in the same cell as the previous, so just increase the count. Cells are
// always contiguous, since we've sorted raw_samples_sorted by cell ID.
++ last_cell_id_it -> second . sample_cnt ;
} else {
// This is a new cell.
PoissonDiskGridEntry data ;
data . first_sample_idx = i ;
data . sample_cnt = 1 ;
auto result = cells . insert ({ sample . cell_id , data });
last_cell_id = sample . cell_id ;
last_cell_id_it = result . first ;
}
}
}
const int max_trials = 5 ;
const float radius_squared = radius * radius ;
for ( int trial = 0 ; trial < max_trials ; ++ trial ) {
// Create sample points for each entry in cells.
for ( auto & it : cells ) {
const Vec2i & cell_id = it . first ;
PoissonDiskGridEntry & cell_data = it . second ;
// This cell's raw sample points start at first_sample_idx. On trial 0, try the first one. On trial 1, try first_sample_idx + 1.
int next_sample_idx = cell_data . first_sample_idx + trial ;
if ( trial >= cell_data . sample_cnt )
// There are no more points to try for this cell.
continue ;
const RawSample & candidate = raw_samples_sorted [ next_sample_idx ];
// See if this point conflicts with any other points in this cell, or with any points in
// neighboring cells. Note that it's possible to have more than one point in the same cell.
bool conflict = false ;
for ( int i = - 1 ; i < 2 && ! conflict ; ++ i ) {
for ( int j = - 1 ; j < 2 ; ++ j ) {
const auto & it_neighbor = cells . find ( cell_id + Vec2i ( i , j ));
if ( it_neighbor != cells . end ()) {
const PoissonDiskGridEntry & neighbor = it_neighbor -> second ;
for ( int i_sample = 0 ; i_sample < neighbor . num_poisson_samples ; ++ i_sample )
if (( neighbor . poisson_samples [ i_sample ] - candidate . coord ). squaredNorm () < radius_squared ) {
conflict = true ;
break ;
}
}
}
}
if ( ! conflict ) {
// Store the new sample.
assert ( cell_data . num_poisson_samples < cell_data . max_positions );
if ( cell_data . num_poisson_samples < cell_data . max_positions )
cell_data . poisson_samples [ cell_data . num_poisson_samples ++ ] = candidate . coord ;
}
}
}
// Copy the results to the output.
std :: vector < Vec2f > out ;
for ( const auto it : cells )
for ( int i = 0 ; i < it . second . num_poisson_samples ; ++ i )
out . emplace_back ( it . second . poisson_samples [ i ]);
return out ;
}
2019-01-30 08:26:23 +01:00
void SLAAutoSupports :: uniformly_cover ( const ExPolygon & island , Structure & structure , bool is_new_island , bool just_one )
{
//int num_of_points = std::max(1, (int)((island.area()*pow(SCALING_FACTOR, 2) * m_config.tear_pressure)/m_config.support_force));
2019-02-17 13:05:22 +01:00
2019-01-30 08:26:23 +01:00
const float density_horizontal = m_config . tear_pressure / m_config . support_force ;
2019-02-17 13:05:22 +01:00
//FIXME why?
const float poisson_radius = 1.f / ( 5.f * density_horizontal );
// const float poisson_radius = 1.f / (15.f * density_horizontal);
const float samples_per_mm2 = 30.f / ( float ( M_PI ) * poisson_radius * poisson_radius );
2019-01-30 08:26:23 +01:00
2019-02-06 10:57:45 +01:00
//FIXME share the random generator. The random generator may be not so cheap to initialize, also we don't want the random generator to be restarted for each polygon.
2019-02-17 13:05:22 +01:00
std :: random_device rd ;
std :: mt19937 rng ( rd ());
std :: vector < Vec2f > raw_samples = sample_expolygon_with_boundary ( island , samples_per_mm2 , 5.f / poisson_radius , rng );
std :: vector < Vec2f > poisson_samples = poisson_disk_from_samples ( raw_samples , poisson_radius );
2019-01-30 08:26:23 +01:00
2019-02-17 13:05:22 +01:00
#ifdef SLA_AUTOSUPPORTS_DEBUG
{
static int irun = 0 ;
Slic3r :: SVG svg ( debug_out_path ( "SLA_supports-uniformly_cover-%d.svg" , irun ++ ), get_extents ( island ));
svg . draw ( island );
for ( const Vec2f & pt : raw_samples )
svg . draw ( Point ( scale_ ( pt . x ()), scale_ ( pt . y ())), "red" );
for ( const Vec2f & pt : poisson_samples )
svg . draw ( Point ( scale_ ( pt . x ()), scale_ ( pt . y ())), "blue" );
}
#endif /* NDEBUG */
2019-01-30 08:26:23 +01:00
2019-02-17 13:05:22 +01:00
assert ( ! poisson_samples . empty ());
for ( const Vec2f & pt : poisson_samples ) {
m_output . emplace_back ( float ( pt ( 0 )), float ( pt ( 1 )), structure . height , 0.4f , is_new_island );
2019-02-06 10:57:45 +01:00
structure . supports_force += m_config . support_force ;
}
2018-12-21 12:35:20 +01:00
}
2018-12-07 14:10:16 +01:00
2018-12-22 10:55:15 +01:00
#ifdef SLA_AUTOSUPPORTS_DEBUG
2019-02-06 10:57:45 +01:00
void SLAAutoSupports :: output_structures ( const std :: vector < Structure >& structures )
2019-01-30 08:26:23 +01:00
{
for ( unsigned int i = 0 ; i < structures . size (); ++ i ) {
std :: stringstream ss ;
ss << structures [ i ]. unique_id . count () << "_" << std :: setw ( 10 ) << std :: setfill ( '0' ) << 1000 + ( int ) structures [ i ]. height / 1000 << ".png" ;
output_expolygons ( std :: vector < ExPolygon > { * structures [ i ]. polygon }, ss . str ());
}
}
2019-02-06 10:57:45 +01:00
void SLAAutoSupports :: output_expolygons ( const ExPolygons & expolys , const std :: string & filename )
2018-12-14 16:15:59 +01:00
{
BoundingBox bb ( Point ( - 30000000 , - 30000000 ), Point ( 30000000 , 30000000 ));
Slic3r :: SVG svg_cummulative ( filename , bb );
for ( size_t i = 0 ; i < expolys . size (); ++ i ) {
/*Slic3r::SVG svg("single"+std::to_string(i)+".svg", bb);
svg.draw(expolys[i]);
svg.draw_outline(expolys[i].contour, "black", scale_(0.05));
svg.draw_outline(expolys[i].holes, "blue", scale_(0.05));
svg.Close();*/
svg_cummulative . draw ( expolys [ i ]);
svg_cummulative . draw_outline ( expolys [ i ]. contour , "black" , scale_ ( 0.05 ));
svg_cummulative . draw_outline ( expolys [ i ]. holes , "blue" , scale_ ( 0.05 ));
}
}
2019-01-30 08:26:23 +01:00
#endif
2018-12-21 12:35:20 +01:00
2019-01-14 17:28:02 +01:00
} // namespace Slic3r