remove colinear points

* this fix the issue that colinear points aren't anymore after the offset2 from slice_closing_radius.
 * this allow the XY hole compensation to apply correctly
 * up the max concave angle accepted to detect hole to grow by XY compensation from 0.06° to 6°.
This commit is contained in:
supermerill 2019-03-12 19:01:47 +01:00
parent b12f484b48
commit 3476579d44
12 changed files with 100 additions and 38 deletions

View File

@ -316,6 +316,35 @@ Point Polygon::point_projection(const Point &point) const
return proj; return proj;
} }
size_t Polygon::remove_colinear_points(coord_t max_offset){
size_t nb_del = 0;
if (points.size() < 3) return 0;
coord_t min_dist = max_offset * max_offset;
while (points.size() > 2 && Line::distance_to_squared(points[0], points.back(), points[1]) < min_dist){
//colinear! delete!
points.erase(points.begin());
nb_del++;
}
for (size_t idx = 1; idx < points.size()-1; ) {
//if (Line(previous, points[idx + 1]).distance_to(points[idx]) < SCALED_EPSILON){
if (Line::distance_to_squared(points[idx], points[idx-1], points[idx + 1]) < min_dist){
//colinear! delete!
points.erase(points.begin() + idx);
nb_del++;
} else {
idx++;
}
}
while (points.size() > 2 && Line::distance_to_squared(points.back(), points[points.size()-2], points.front()) < min_dist) {
//colinear! delete!
points.erase(points.end()-1);
nb_del++;
}
return nb_del;
}
BoundingBox get_extents(const Polygon &poly) BoundingBox get_extents(const Polygon &poly)
{ {
return poly.bounding_box(); return poly.bounding_box();

View File

@ -59,6 +59,9 @@ public:
Points convex_points(double angle = PI) const; Points convex_points(double angle = PI) const;
// Projection of a point onto the polygon. // Projection of a point onto the polygon.
Point point_projection(const Point &point) const; Point point_projection(const Point &point) const;
/// remove points that are (almost) on an existing line from previous & next point.
/// return number of point removed
size_t remove_colinear_points(coord_t max_offset);
}; };
extern BoundingBox get_extents(const Polygon &poly); extern BoundingBox get_extents(const Polygon &poly);

View File

@ -1870,10 +1870,10 @@ void PrintConfigDef::init_fff_params()
def = this->add("resolution", coFloat); def = this->add("resolution", coFloat);
def->label = L("Resolution"); def->label = L("Resolution");
def->tooltip = L("Minimum detail resolution, used to simplify the input file for speeding up " def->tooltip = L("Minimum detail resolution, used to simplify the input file for speeding up "
"the slicing job and reducing memory usage. High-resolution models often carry " "the slicing job and reducing memory usage. High-resolution models often carry "
"more detail than printers can render. Set to zero to disable any simplification " "more detail than printers can render. Set to zero to disable any simplification "
"and use full resolution from input. " "and use full resolution from input. "
"\nNote: slic3r simplify the geometry with a treshold of 0.0125mm and has an internal resolution of 0.0001mm."); "\nNote: slic3r simplify the geometry with a treshold of 0.0125mm and has an internal resolution of 0.0001mm.");
def->sidetext = L("mm"); def->sidetext = L("mm");
def->cli = "resolution=f"; def->cli = "resolution=f";
def->min = 0; def->min = 0;
@ -2254,6 +2254,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comExpert; def->mode = comExpert;
def->default_value = new ConfigOptionStrings { "; Filament gcode\n" }; def->default_value = new ConfigOptionStrings { "; Filament gcode\n" };
def = this->add("model_precision", coFloat);
def->label = L("Model rounding precision");
def->full_label = L("Model rounding precision");
def->category = L("Advanced");
def->tooltip = L("This is the rounding error of the input object."
" It's used to align points that should be in the same line."
" Put 0 to disable.");
def->sidetext = L("mm");
def->cli = "model-precision=f";
def->min = 0;
def->mode = comExpert;
def->default_value = new ConfigOptionFloat(0.0001);
def = this->add("single_extruder_multi_material", coBool); def = this->add("single_extruder_multi_material", coBool);
def->label = L("Single Extruder Multi Material"); def->label = L("Single Extruder Multi Material");
def->tooltip = L("The printer multiplexes filaments into a single hot end."); def->tooltip = L("The printer multiplexes filaments into a single hot end.");

View File

@ -453,13 +453,14 @@ public:
ConfigOptionBool clip_multipart_objects; ConfigOptionBool clip_multipart_objects;
ConfigOptionBool dont_support_bridges; ConfigOptionBool dont_support_bridges;
ConfigOptionFloat elefant_foot_compensation; ConfigOptionFloat elefant_foot_compensation;
ConfigOptionBool exact_last_layer_height;
ConfigOptionFloatOrPercent extrusion_width; ConfigOptionFloatOrPercent extrusion_width;
ConfigOptionFloatOrPercent first_layer_height; ConfigOptionFloatOrPercent first_layer_height;
ConfigOptionBool infill_only_where_needed; ConfigOptionBool infill_only_where_needed;
// Force the generation of solid shells between adjacent materials/volumes. // Force the generation of solid shells between adjacent materials/volumes.
ConfigOptionBool interface_shells; ConfigOptionBool interface_shells;
ConfigOptionFloat layer_height; ConfigOptionFloat layer_height;
ConfigOptionBool exact_last_layer_height; ConfigOptionFloat model_precision;
ConfigOptionInt raft_layers; ConfigOptionInt raft_layers;
ConfigOptionEnum<SeamPosition> seam_position; ConfigOptionEnum<SeamPosition> seam_position;
ConfigOptionBool seam_travel; ConfigOptionBool seam_travel;
@ -505,12 +506,13 @@ protected:
OPT_PTR(clip_multipart_objects); OPT_PTR(clip_multipart_objects);
OPT_PTR(dont_support_bridges); OPT_PTR(dont_support_bridges);
OPT_PTR(elefant_foot_compensation); OPT_PTR(elefant_foot_compensation);
OPT_PTR(exact_last_layer_height);
OPT_PTR(extrusion_width); OPT_PTR(extrusion_width);
OPT_PTR(first_layer_height); OPT_PTR(first_layer_height);
OPT_PTR(infill_only_where_needed); OPT_PTR(infill_only_where_needed);
OPT_PTR(interface_shells); OPT_PTR(interface_shells);
OPT_PTR(layer_height); OPT_PTR(layer_height);
OPT_PTR(exact_last_layer_height); OPT_PTR(model_precision);
OPT_PTR(raft_layers); OPT_PTR(raft_layers);
OPT_PTR(seam_position); OPT_PTR(seam_position);
OPT_PTR(seam_travel); OPT_PTR(seam_travel);

View File

@ -813,7 +813,6 @@ void PrintObject::tag_under_bridge() {
for (ExPolygon poly_inter : dense_polys) area_dense += poly_inter.area(); for (ExPolygon poly_inter : dense_polys) area_dense += poly_inter.area();
double area_sparse = 0; double area_sparse = 0;
for (ExPolygon poly_inter : sparse_polys) area_sparse += poly_inter.area(); for (ExPolygon poly_inter : sparse_polys) area_sparse += poly_inter.area();
std::cout << "need to split? " << area_sparse << " > " << area_dense << " * " << COEFF_SPLIT << "\n";
if (area_sparse > area_dense * COEFF_SPLIT) { if (area_sparse > area_dense * COEFF_SPLIT) {
//split //split
dense_polys = union_ex(dense_polys); dense_polys = union_ex(dense_polys);
@ -1913,27 +1912,27 @@ end:
void PrintObject::_offsetHoles(float hole_delta, LayerRegion *layerm) { void PrintObject::_offsetHoles(float hole_delta, LayerRegion *layerm) {
if (hole_delta != 0.f) { if (hole_delta != 0.f) {
std::cout << "offset_hole z="<<layerm->layer()->id()<<"\n";
ExPolygons polys = to_expolygons(std::move(layerm->slices.surfaces)); ExPolygons polys = to_expolygons(std::move(layerm->slices.surfaces));
ExPolygons new_polys; ExPolygons new_polys;
for (ExPolygon ex_poly : polys) { for (const ExPolygon &ex_poly : polys) {
ExPolygon new_ex_poly(ex_poly); ExPolygon new_ex_poly(ex_poly);
new_ex_poly.holes.clear(); new_ex_poly.holes.clear();
for (Polygon hole : ex_poly.holes) { for (const Polygon &hole : ex_poly.holes) {
//check if convex to reduce it //check if convex to reduce it
// check whether first point forms a convex angle // check whether first point forms a convex angle
//note: we allow a deviation of 5.7° (0.01rad = 0.57°)
bool ok = true; bool ok = true;
ok = (hole.points.front().ccw_angle(hole.points.back(), *(hole.points.begin() + 1)) <= PI + 0.001); ok = (hole.points.front().ccw_angle(hole.points.back(), *(hole.points.begin() + 1)) <= PI + 0.1);
// check whether points 1..(n-1) form convex angles // check whether points 1..(n-1) form convex angles
if (ok) if (ok)
for (Points::const_iterator p = hole.points.begin() + 1; p != hole.points.end() - 1; ++p) { for (Points::const_iterator p = hole.points.begin() + 1; p != hole.points.end() - 1; ++p) {
ok = (p->ccw_angle(*(p - 1), *(p + 1)) <= PI + 0.001); ok = (p->ccw_angle(*(p - 1), *(p + 1)) <= PI + 0.1);
if (!ok) break; if (!ok) break;
} }
// check whether last point forms a convex angle // check whether last point forms a convex angle
ok &= (hole.points.back().ccw_angle(*(hole.points.end() - 2), hole.points.front()) <= PI + 0.001); ok &= (hole.points.back().ccw_angle(*(hole.points.end() - 2), hole.points.front()) <= PI + 0.1);
if (ok) { if (ok) {
for (Polygon newHole : offset(hole, -hole_delta)) { for (Polygon newHole : offset(hole, -hole_delta)) {
@ -1941,8 +1940,9 @@ void PrintObject::_offsetHoles(float hole_delta, LayerRegion *layerm) {
newHole.make_clockwise(); newHole.make_clockwise();
new_ex_poly.holes.push_back(newHole); new_ex_poly.holes.push_back(newHole);
} }
} else } else {
new_ex_poly.holes.push_back(hole); new_ex_poly.holes.push_back(hole);
}
} }
new_polys.push_back(new_ex_poly); new_polys.push_back(new_ex_poly);
} }
@ -2007,11 +2007,11 @@ std::vector<ExPolygons> PrintObject::_slice_volumes(const std::vector<float> &z,
// apply XY shift // apply XY shift
mesh.translate(- unscale<float>(m_copies_shift(0)), - unscale<float>(m_copies_shift(1)), 0); mesh.translate(- unscale<float>(m_copies_shift(0)), - unscale<float>(m_copies_shift(1)), 0);
// perform actual slicing // perform actual slicing
TriangleMeshSlicer mslicer; TriangleMeshSlicer mslicer(float(m_config.slice_closing_radius.value), float(m_config.model_precision.value));
const Print *print = this->print(); const Print *print = this->print();
auto callback = TriangleMeshSlicer::throw_on_cancel_callback_type([print](){print->throw_if_canceled();}); auto callback = TriangleMeshSlicer::throw_on_cancel_callback_type([print](){print->throw_if_canceled();});
mslicer.init(&mesh, callback); mslicer.init(&mesh, callback);
mslicer.slice(z, float(m_config.slice_closing_radius.value), &layers, callback); mslicer.slice(z, &layers, callback);
m_print->throw_if_canceled(); m_print->throw_if_canceled();
} }
} }

View File

@ -561,7 +561,7 @@ void base_plate(const TriangleMesh &mesh, ExPolygons &output, float h,
heights.emplace_back(hi); heights.emplace_back(hi);
std::vector<ExPolygons> out; out.reserve(size_t(std::ceil(h/layerh))); std::vector<ExPolygons> out; out.reserve(size_t(std::ceil(h/layerh)));
slicer.slice(heights, 0.f, &out, thrfn); slicer.slice(heights, &out, thrfn);
size_t count = 0; for(auto& o : out) count += o.size(); size_t count = 0; for(auto& o : out) count += o.size();

View File

@ -2105,7 +2105,7 @@ SlicedSupports SLASupportTree::slice(float layerh, float init_layerh) const
fullmesh.merge(get_pad()); fullmesh.merge(get_pad());
TriangleMeshSlicer slicer(&fullmesh); TriangleMeshSlicer slicer(&fullmesh);
SlicedSupports ret; SlicedSupports ret;
slicer.slice(heights, 0.f, &ret, get().ctl().cancelfn); slicer.slice(heights, &ret, get().ctl().cancelfn);
return ret; return ret;
} }

View File

@ -629,7 +629,8 @@ void SLAPrint::process()
double lh = po.m_config.layer_height.getFloat(); double lh = po.m_config.layer_height.getFloat();
TriangleMesh mesh = po.transformed_mesh(); TriangleMesh mesh = po.transformed_mesh();
TriangleMeshSlicer slicer(&mesh); TriangleMeshSlicer slicer(float(po.config().slice_closing_radius.value),0);
slicer.init(&mesh, [](){});
// The 1D grid heights // The 1D grid heights
std::vector<float> heights = calculate_heights(mesh.bounding_box(), std::vector<float> heights = calculate_heights(mesh.bounding_box(),
@ -637,7 +638,7 @@ void SLAPrint::process()
ilh, float(lh)); ilh, float(lh));
auto& layers = po.m_model_slices; layers.clear(); auto& layers = po.m_model_slices; layers.clear();
slicer.slice(heights, float(po.config().slice_closing_radius.value), &layers, [this](){ throw_if_canceled(); }); slicer.slice(heights, &layers, [this](){ throw_if_canceled(); });
}; };
// In this step we check the slices, identify island and cover them with // In this step we check the slices, identify island and cover them with

View File

@ -852,22 +852,22 @@ void TriangleMeshSlicer::_slice_do(size_t facet_idx, std::vector<IntersectionLin
} }
} }
void TriangleMeshSlicer::slice(const std::vector<float> &z, const float closing_radius, std::vector<ExPolygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const void TriangleMeshSlicer::slice(const std::vector<float> &z, std::vector<ExPolygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const
{ {
std::vector<Polygons> layers_p; std::vector<Polygons> layers_p;
this->slice(z, &layers_p, throw_on_cancel); this->slice(z, &layers_p, throw_on_cancel);
BOOST_LOG_TRIVIAL(debug) << "TriangleMeshSlicer::make_expolygons in parallel - start"; BOOST_LOG_TRIVIAL(debug) << "TriangleMeshSlicer::make_expolygons in parallel - start";
layers->resize(z.size()); layers->resize(z.size());
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, z.size()), tbb::blocked_range<size_t>(0, z.size()),
[&layers_p, closing_radius, layers, throw_on_cancel, this](const tbb::blocked_range<size_t>& range) { [&layers_p, layers, throw_on_cancel, this](const tbb::blocked_range<size_t>& range) {
for (size_t layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { for (size_t layer_id = range.begin(); layer_id < range.end(); ++ layer_id) {
#ifdef SLIC3R_TRIANGLEMESH_DEBUG #ifdef SLIC3R_TRIANGLEMESH_DEBUG
printf("Layer " PRINTF_ZU " (slice_z = %.2f):\n", layer_id, z[layer_id]); printf("Layer " PRINTF_ZU " (slice_z = %.2f):\n", layer_id, z[layer_id]);
#endif #endif
throw_on_cancel(); throw_on_cancel();
this->make_expolygons(layers_p[layer_id], closing_radius, &(*layers)[layer_id]); this->make_expolygons(layers_p[layer_id], &(*layers)[layer_id]);
} }
}); });
BOOST_LOG_TRIVIAL(debug) << "TriangleMeshSlicer::make_expolygons in parallel - end"; BOOST_LOG_TRIVIAL(debug) << "TriangleMeshSlicer::make_expolygons in parallel - end";
@ -1002,7 +1002,7 @@ TriangleMeshSlicer::FacetSliceType TriangleMeshSlicer::slice_facet(
line_out->a_id = points[1].point_id; line_out->a_id = points[1].point_id;
line_out->b_id = points[0].point_id; line_out->b_id = points[0].point_id;
line_out->edge_a_id = points[1].edge_id; line_out->edge_a_id = points[1].edge_id;
line_out->edge_b_id = points[0].edge_id; line_out->edge_b_id = points[0].edge_id;
// Not a zero lenght edge. // Not a zero lenght edge.
//FIXME slice_facet() may create zero length edges due to rounding of doubles into coord_t. //FIXME slice_facet() may create zero length edges due to rounding of doubles into coord_t.
//assert(line_out->a != line_out->b); //assert(line_out->a != line_out->b);
@ -1184,7 +1184,7 @@ static void chain_lines_by_triangle_connectivity(std::vector<IntersectionLine> &
if (next_line == nullptr) { if (next_line == nullptr) {
// Check whether we closed this loop. // Check whether we closed this loop.
if ((first_line->edge_a_id != -1 && first_line->edge_a_id == last_line->edge_b_id) || if ((first_line->edge_a_id != -1 && first_line->edge_a_id == last_line->edge_b_id) ||
(first_line->a_id != -1 && first_line->a_id == last_line->b_id)) { (first_line->a_id != -1 && first_line->a_id == last_line->b_id)) {
// The current loop is complete. Add it to the output. // The current loop is complete. Add it to the output.
loops.emplace_back(std::move(loop_pts)); loops.emplace_back(std::move(loop_pts));
#ifdef SLIC3R_TRIANGLEMESH_DEBUG #ifdef SLIC3R_TRIANGLEMESH_DEBUG
@ -1600,7 +1600,7 @@ void TriangleMeshSlicer::make_expolygons_simple(std::vector<IntersectionLine> &l
#endif #endif
} }
void TriangleMeshSlicer::make_expolygons(const Polygons &loops, const float closing_radius, ExPolygons* slices) const void TriangleMeshSlicer::make_expolygons(const Polygons &loops, ExPolygons* slices) const
{ {
/* /*
Input loops are not suitable for evenodd nor nonzero fill types, as we might get Input loops are not suitable for evenodd nor nonzero fill types, as we might get
@ -1651,11 +1651,19 @@ void TriangleMeshSlicer::make_expolygons(const Polygons &loops, const float clos
// p_slices = diff(p_slices, *loop); // p_slices = diff(p_slices, *loop);
//} //}
//remove point in the same plane (have to do that before the safety offset to avoid workgin on a distored polygon)
Polygons filered_polys = loops;
if (this->model_precision > 0){
for (Polygon &hole : filered_polys){
hole.remove_colinear_points(scale_(this->model_precision));
}
}
// Perform a safety offset to merge very close facets (TODO: find test case for this) // Perform a safety offset to merge very close facets (TODO: find test case for this)
// 0.0499 comes from https://github.com/slic3r/Slic3r/issues/959 // 0.0499 comes from https://github.com/slic3r/Slic3r/issues/959
// double safety_offset = scale_(0.0499); // double safety_offset = scale_(0.0499);
// 0.0001 is set to satisfy GH #520, #1029, #1364 // 0.0001 is set to satisfy GH #520, #1029, #1364
double safety_offset = scale_(closing_radius); double safety_offset = scale_(this->closing_radius);
/* The following line is commented out because it can generate wrong polygons, /* The following line is commented out because it can generate wrong polygons,
see for example issue #661 */ see for example issue #661 */
@ -1671,16 +1679,16 @@ void TriangleMeshSlicer::make_expolygons(const Polygons &loops, const float clos
// append to the supplied collection // append to the supplied collection
if (safety_offset > 0) if (safety_offset > 0)
expolygons_append(*slices, offset2_ex(union_(loops, false), +safety_offset, -safety_offset)); expolygons_append(*slices, offset2_ex(union_(filered_polys, false), +safety_offset, -safety_offset));
else else
expolygons_append(*slices, union_ex(loops, false)); expolygons_append(*slices, union_ex(filered_polys, false));
} }
void TriangleMeshSlicer::make_expolygons(std::vector<IntersectionLine> &lines, const float closing_radius, ExPolygons* slices) const void TriangleMeshSlicer::make_expolygons(std::vector<IntersectionLine> &lines, ExPolygons* slices) const
{ {
Polygons pp; Polygons pp;
this->make_loops(lines, &pp); this->make_loops(lines, &pp);
this->make_expolygons(pp, closing_radius, slices); this->make_expolygons(pp, slices);
} }
void TriangleMeshSlicer::cut(float z, TriangleMesh* upper, TriangleMesh* lower) const void TriangleMeshSlicer::cut(float z, TriangleMesh* upper, TriangleMesh* lower) const

View File

@ -159,13 +159,16 @@ typedef std::vector<IntersectionLine*> IntersectionLinePtrs;
class TriangleMeshSlicer class TriangleMeshSlicer
{ {
public: public:
float closing_radius;
float model_precision;
typedef std::function<void()> throw_on_cancel_callback_type; typedef std::function<void()> throw_on_cancel_callback_type;
TriangleMeshSlicer() : mesh(nullptr) {} TriangleMeshSlicer(float closing_radius, float model_precision) : mesh(nullptr), closing_radius(closing_radius), model_precision(model_precision) {}
// Not quite nice, but the constructor and init() methods require non-const mesh pointer to be able to call mesh->require_shared_vertices() // Not quite nice, but the constructor and init() methods require non-const mesh pointer to be able to call mesh->require_shared_vertices()
TriangleMeshSlicer(TriangleMesh* mesh) { this->init(mesh, [](){}); } TriangleMeshSlicer(TriangleMesh* mesh) { this->init(mesh, [](){}); }
void init(TriangleMesh *mesh, throw_on_cancel_callback_type throw_on_cancel); void init(TriangleMesh *mesh, throw_on_cancel_callback_type throw_on_cancel);
void slice(const std::vector<float> &z, std::vector<Polygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const; void slice(const std::vector<float> &z, std::vector<Polygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const;
void slice(const std::vector<float> &z, const float closing_radius, std::vector<ExPolygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const; void slice(const std::vector<float> &z, std::vector<ExPolygons>* layers, throw_on_cancel_callback_type throw_on_cancel) const;
enum FacetSliceType { enum FacetSliceType {
NoSlice = 0, NoSlice = 0,
Slicing = 1, Slicing = 1,
@ -184,9 +187,9 @@ private:
void _slice_do(size_t facet_idx, std::vector<IntersectionLines>* lines, boost::mutex* lines_mutex, const std::vector<float> &z) const; void _slice_do(size_t facet_idx, std::vector<IntersectionLines>* lines, boost::mutex* lines_mutex, const std::vector<float> &z) const;
void make_loops(std::vector<IntersectionLine> &lines, Polygons* loops) const; void make_loops(std::vector<IntersectionLine> &lines, Polygons* loops) const;
void make_expolygons(const Polygons &loops, const float closing_radius, ExPolygons* slices) const; void make_expolygons(const Polygons &loops, ExPolygons* slices) const;
void make_expolygons_simple(std::vector<IntersectionLine> &lines, ExPolygons* slices) const; void make_expolygons_simple(std::vector<IntersectionLine> &lines, ExPolygons* slices) const;
void make_expolygons(std::vector<IntersectionLine> &lines, const float closing_radius, ExPolygons* slices) const; void make_expolygons(std::vector<IntersectionLine> &lines, ExPolygons* slices) const;
}; };
TriangleMesh make_cube(double x, double y, double z); TriangleMesh make_cube(double x, double y, double z);

View File

@ -402,6 +402,7 @@ const std::vector<std::string>& Preset::print_options()
, "first_layer_infill_speed" , "first_layer_infill_speed"
, "thin_walls_min_width" , "thin_walls_min_width"
, "thin_walls_overlap" , "thin_walls_overlap"
, "model_precision"
}; };
return s_opts; return s_opts;
} }

View File

@ -1179,6 +1179,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(_(L("Other"))); optgroup = page->new_optgroup(_(L("Other")));
optgroup->append_single_option_line("clip_multipart_objects"); optgroup->append_single_option_line("clip_multipart_objects");
optgroup->append_single_option_line("resolution"); optgroup->append_single_option_line("resolution");
optgroup->append_single_option_line("model_precision");
page = add_options_page(_(L("Output options")), "page_white_go.png"); page = add_options_page(_(L("Output options")), "page_white_go.png");
optgroup = page->new_optgroup(_(L("Sequential printing"))); optgroup = page->new_optgroup(_(L("Sequential printing")));