Merge branch 'master_250' (NO CONFLICTS FIXED):
CONFLICT (content): Merge conflict in resources/profiles/PrusaResearch.idx CONFLICT (content): Merge conflict in resources/profiles/PrusaResearch.ini CONFLICT (content): Merge conflict in src/libslic3r/CMakeLists.txt CONFLICT (content): Merge conflict in src/libslic3r/Fill/Fill.cpp CONFLICT (content): Merge conflict in src/libslic3r/GCode.cpp CONFLICT (content): Merge conflict in src/libslic3r/GCode.hpp CONFLICT (content): Merge conflict in src/libslic3r/GCode/GCodeProcessor.cpp CONFLICT (content): Merge conflict in src/libslic3r/GCode/GCodeProcessor.hpp CONFLICT (content): Merge conflict in src/libslic3r/GCode/SeamPlacer.cpp CONFLICT (content): Merge conflict in src/libslic3r/GCode/SeamPlacer.hpp CONFLICT (add/add): Merge conflict in src/libslic3r/Geometry/Curves.hpp CONFLICT (content): Merge conflict in src/libslic3r/PerimeterGenerator.cpp CONFLICT (content): Merge conflict in src/libslic3r/Point.hpp CONFLICT (content): Merge conflict in src/libslic3r/PrintConfig.hpp CONFLICT (content): Merge conflict in src/slic3r/GUI/ConfigWizard.cpp CONFLICT (content): Merge conflict in src/slic3r/GUI/GCodeViewer.cpp CONFLICT (content): Merge conflict in src/slic3r/GUI/GLCanvas3D.cpp CONFLICT (content): Merge conflict in src/slic3r/GUI/GUI_App.cpp CONFLICT (content): Merge conflict in src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp CONFLICT (content): Merge conflict in src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp CONFLICT (content): Merge conflict in src/slic3r/Utils/FixModelByWin10.cpp CONFLICT (modify/delete): t/perimeters.t deleted in HEAD and modified in master_250. Version master_250 of t/perimeters.t left in tree. CONFLICT (content): Merge conflict in tests/fff_print/CMakeLists.txt CONFLICT (content): Merge conflict in tests/fff_print/test_fill.cpp CONFLICT (content): Merge conflict in version.inc CONFLICT (modify/delete): xs/xsp/PerimeterGenerator.xsp deleted in HEAD and modified in master_250. Version master_250 of xs/xsp/PerimeterGenerator.xsp left in tree.
This commit is contained in:
commit
b61714bb3e
152 changed files with 86092 additions and 45493 deletions
src/libslic3r
|
@ -3,7 +3,6 @@
|
|||
#include "GCode.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "GCode/PrintExtents.hpp"
|
||||
#include "GCode/Thumbnails.hpp"
|
||||
|
@ -1106,14 +1105,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||
|
||||
if (print.config().spiral_vase.value)
|
||||
m_spiral_vase = make_unique<SpiralVase>(print.config());
|
||||
#ifdef HAS_PRESSURE_EQUALIZER
|
||||
|
||||
if (print.config().max_volumetric_extrusion_rate_slope_positive.value > 0 ||
|
||||
print.config().max_volumetric_extrusion_rate_slope_negative.value > 0)
|
||||
m_pressure_equalizer = make_unique<PressureEqualizer>(&print.config());
|
||||
m_pressure_equalizer = make_unique<PressureEqualizer>(print.config());
|
||||
m_enable_extrusion_role_markers = (bool)m_pressure_equalizer;
|
||||
#else /* HAS_PRESSURE_EQUALIZER */
|
||||
m_enable_extrusion_role_markers = false;
|
||||
#endif /* HAS_PRESSURE_EQUALIZER */
|
||||
|
||||
// Write information on the generator.
|
||||
file.write_format("; %s\n\n", Slic3r::header_slic3r_generated().c_str());
|
||||
|
@ -1299,7 +1295,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||
print.throw_if_canceled();
|
||||
|
||||
// Collect custom seam data from all objects.
|
||||
m_seam_placer.init(print);
|
||||
std::function<void(void)> throw_if_canceled_func = [&print]() { print.throw_if_canceled();};
|
||||
m_seam_placer.init(print, throw_if_canceled_func);
|
||||
|
||||
if (! (has_wipe_tower && print.config().single_extruder_multi_material_priming)) {
|
||||
// Set initial extruder only after custom start G-code.
|
||||
|
@ -1352,10 +1349,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||
// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser
|
||||
// and export G-code into file.
|
||||
this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file);
|
||||
#ifdef HAS_PRESSURE_EQUALIZER
|
||||
if (m_pressure_equalizer)
|
||||
file.write(m_pressure_equalizer->process("", true));
|
||||
#endif /* HAS_PRESSURE_EQUALIZER */
|
||||
++ finished_objects;
|
||||
// Flag indicating whether the nozzle temperature changes from 1st to 2nd layer were performed.
|
||||
// Reset it when starting another object from 1st layer.
|
||||
|
@ -1412,10 +1405,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||
// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser
|
||||
// and export G-code into file.
|
||||
this->process_layers(print, tool_ordering, print_object_instances_ordering, layers_to_print, file);
|
||||
#ifdef HAS_PRESSURE_EQUALIZER
|
||||
if (m_pressure_equalizer)
|
||||
file.write(m_pressure_equalizer->process("", true));
|
||||
#endif /* HAS_PRESSURE_EQUALIZER */
|
||||
if (m_wipe_tower)
|
||||
// Purge the extruder, pull out the active filament.
|
||||
file.write(m_wipe_tower->finalize(*this));
|
||||
|
@ -1528,11 +1517,18 @@ void GCode::process_layers(
|
|||
{
|
||||
// The pipeline is variable: The vase mode filter is optional.
|
||||
size_t layer_to_print_idx = 0;
|
||||
const auto generator = tbb::make_filter<void, GCode::LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> GCode::LayerResult {
|
||||
if (layer_to_print_idx == layers_to_print.size()) {
|
||||
fc.stop();
|
||||
return {};
|
||||
const auto generator = tbb::make_filter<void, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> LayerResult {
|
||||
if (layer_to_print_idx >= layers_to_print.size()) {
|
||||
if ((!m_pressure_equalizer && layer_to_print_idx == layers_to_print.size()) || (m_pressure_equalizer && layer_to_print_idx == (layers_to_print.size() + 1))) {
|
||||
fc.stop();
|
||||
return {};
|
||||
} else {
|
||||
// Pressure equalizer need insert empty input. Because it returns one layer back.
|
||||
// Insert NOP (no operation) layer;
|
||||
++layer_to_print_idx;
|
||||
return LayerResult::make_nop_layer_result();
|
||||
}
|
||||
} else {
|
||||
const std::pair<coordf_t, std::vector<LayerToPrint>>& layer = layers_to_print[layer_to_print_idx++];
|
||||
const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first);
|
||||
|
@ -1542,17 +1538,27 @@ void GCode::process_layers(
|
|||
return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1));
|
||||
}
|
||||
});
|
||||
const auto spiral_vase = tbb::make_filter<GCode::LayerResult, GCode::LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&spiral_vase = *this->m_spiral_vase.get()](GCode::LayerResult in) -> GCode::LayerResult {
|
||||
const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&spiral_vase = *this->m_spiral_vase](LayerResult in) -> LayerResult {
|
||||
if (in.nop_layer_result)
|
||||
return in;
|
||||
|
||||
spiral_vase.enable(in.spiral_vase_enable);
|
||||
return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush };
|
||||
return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush};
|
||||
});
|
||||
const auto cooling = tbb::make_filter<GCode::LayerResult, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in) -> std::string {
|
||||
return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush);
|
||||
const auto pressure_equalizer = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&pressure_equalizer = *this->m_pressure_equalizer](LayerResult in) -> LayerResult {
|
||||
return pressure_equalizer.process_layer(std::move(in));
|
||||
});
|
||||
const auto cooling = tbb::make_filter<LayerResult, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&cooling_buffer = *this->m_cooling_buffer](LayerResult in) -> std::string {
|
||||
if (in.nop_layer_result)
|
||||
return in.gcode;
|
||||
|
||||
return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush);
|
||||
});
|
||||
const auto find_replace = tbb::make_filter<std::string, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&self = *this->m_find_replace.get()](std::string s) -> std::string {
|
||||
[&self = *this->m_find_replace](std::string s) -> std::string {
|
||||
return self.process_layer(std::move(s));
|
||||
});
|
||||
const auto output = tbb::make_filter<std::string, void>(slic3r_tbb_filtermode::serial_in_order,
|
||||
|
@ -1565,14 +1571,22 @@ void GCode::process_layers(
|
|||
|
||||
// The pipeline elements are joined using const references, thus no copying is performed.
|
||||
output_stream.find_replace_supress();
|
||||
if (m_spiral_vase && m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
|
||||
if (m_spiral_vase && m_find_replace && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & find_replace & output);
|
||||
else if (m_spiral_vase && m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
|
||||
else if (m_spiral_vase && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & output);
|
||||
else if (m_find_replace && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & find_replace & output);
|
||||
else if (m_spiral_vase)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
|
||||
else if (m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
|
||||
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
|
||||
else if (m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & output);
|
||||
else
|
||||
tbb::parallel_pipeline(12, generator & cooling & output);
|
||||
tbb::parallel_pipeline(12, generator & cooling & output);
|
||||
output_stream.find_replace_enable();
|
||||
}
|
||||
|
||||
|
@ -1588,28 +1602,43 @@ void GCode::process_layers(
|
|||
{
|
||||
// The pipeline is variable: The vase mode filter is optional.
|
||||
size_t layer_to_print_idx = 0;
|
||||
const auto generator = tbb::make_filter<void, GCode::LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx](tbb::flow_control& fc) -> GCode::LayerResult {
|
||||
if (layer_to_print_idx == layers_to_print.size()) {
|
||||
fc.stop();
|
||||
return {};
|
||||
const auto generator = tbb::make_filter<void, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx](tbb::flow_control& fc) -> LayerResult {
|
||||
if (layer_to_print_idx >= layers_to_print.size()) {
|
||||
if ((!m_pressure_equalizer && layer_to_print_idx == layers_to_print.size()) || (m_pressure_equalizer && layer_to_print_idx == (layers_to_print.size() + 1))) {
|
||||
fc.stop();
|
||||
return {};
|
||||
} else {
|
||||
// Pressure equalizer need insert empty input. Because it returns one layer back.
|
||||
// Insert NOP (no operation) layer;
|
||||
++layer_to_print_idx;
|
||||
return LayerResult::make_nop_layer_result();
|
||||
}
|
||||
} else {
|
||||
LayerToPrint &layer = layers_to_print[layer_to_print_idx ++];
|
||||
print.throw_if_canceled();
|
||||
return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, single_object_idx);
|
||||
}
|
||||
});
|
||||
const auto spiral_vase = tbb::make_filter<GCode::LayerResult, GCode::LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&spiral_vase = *this->m_spiral_vase.get()](GCode::LayerResult in)->GCode::LayerResult {
|
||||
const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&spiral_vase = *this->m_spiral_vase](LayerResult in)->LayerResult {
|
||||
if (in.nop_layer_result)
|
||||
return in;
|
||||
spiral_vase.enable(in.spiral_vase_enable);
|
||||
return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush };
|
||||
});
|
||||
const auto cooling = tbb::make_filter<GCode::LayerResult, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in)->std::string {
|
||||
const auto pressure_equalizer = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&pressure_equalizer = *this->m_pressure_equalizer](LayerResult in) -> LayerResult {
|
||||
return pressure_equalizer.process_layer(std::move(in));
|
||||
});
|
||||
const auto cooling = tbb::make_filter<LayerResult, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&cooling_buffer = *this->m_cooling_buffer](LayerResult in)->std::string {
|
||||
if (in.nop_layer_result)
|
||||
return in.gcode;
|
||||
return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush);
|
||||
});
|
||||
const auto find_replace = tbb::make_filter<std::string, std::string>(slic3r_tbb_filtermode::serial_in_order,
|
||||
[&self = *this->m_find_replace.get()](std::string s) -> std::string {
|
||||
[&self = *this->m_find_replace](std::string s) -> std::string {
|
||||
return self.process_layer(std::move(s));
|
||||
});
|
||||
const auto output = tbb::make_filter<std::string, void>(slic3r_tbb_filtermode::serial_in_order,
|
||||
|
@ -1622,14 +1651,22 @@ void GCode::process_layers(
|
|||
|
||||
// The pipeline elements are joined using const references, thus no copying is performed.
|
||||
output_stream.find_replace_supress();
|
||||
if (m_spiral_vase && m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
|
||||
if (m_spiral_vase && m_find_replace && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & find_replace & output);
|
||||
else if (m_spiral_vase && m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
|
||||
else if (m_spiral_vase && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & output);
|
||||
else if (m_find_replace && m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & find_replace & output);
|
||||
else if (m_spiral_vase)
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
|
||||
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
|
||||
else if (m_find_replace)
|
||||
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
|
||||
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
|
||||
else if (m_pressure_equalizer)
|
||||
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & output);
|
||||
else
|
||||
tbb::parallel_pipeline(12, generator & cooling & output);
|
||||
tbb::parallel_pipeline(12, generator & cooling & output);
|
||||
output_stream.find_replace_enable();
|
||||
}
|
||||
|
||||
|
@ -2056,7 +2093,7 @@ namespace Skirt {
|
|||
// In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated.
|
||||
// For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths
|
||||
// and performing the extruder specific extrusions together.
|
||||
GCode::LayerResult GCode::process_layer(
|
||||
LayerResult GCode::process_layer(
|
||||
const Print &print,
|
||||
// Set of object & print layers of the same PrintObject and with the same print_z.
|
||||
const std::vector<LayerToPrint> &layers,
|
||||
|
@ -2087,7 +2124,7 @@ GCode::LayerResult GCode::process_layer(
|
|||
}
|
||||
}
|
||||
const Layer &layer = (object_layer != nullptr) ? *object_layer : *support_layer;
|
||||
GCode::LayerResult result { {}, layer.id(), false, last_layer };
|
||||
LayerResult result { {}, layer.id(), false, last_layer, false};
|
||||
if (layer_tools.extruders.empty())
|
||||
// Nothing to extrude.
|
||||
return result;
|
||||
|
@ -2470,13 +2507,11 @@ GCode::LayerResult GCode::process_layer(
|
|||
// Flush the cooling buffer at each object layer or possibly at the last layer, even if it contains just supports (This should not happen).
|
||||
object_layer || last_layer);
|
||||
|
||||
#ifdef HAS_PRESSURE_EQUALIZER
|
||||
// Apply pressure equalization if enabled;
|
||||
// printf("G-code before filter:\n%s\n", gcode.c_str());
|
||||
if (m_pressure_equalizer)
|
||||
gcode = m_pressure_equalizer->process(gcode.c_str(), false);
|
||||
// printf("G-code after filter:\n%s\n", out.c_str());
|
||||
#endif /* HAS_PRESSURE_EQUALIZER */
|
||||
|
||||
file.write(gcode);
|
||||
#endif
|
||||
|
@ -2578,6 +2613,7 @@ std::string GCode::change_layer(coordf_t print_z)
|
|||
return gcode;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
static const auto comment_perimeter = "perimeter"sv;
|
||||
// Comparing string_view pointer & length for speed.
|
||||
static inline bool comment_is_perimeter(const std::string_view comment) {
|
||||
|
@ -2585,6 +2621,9 @@ static inline bool comment_is_perimeter(const std::string_view comment) {
|
|||
}
|
||||
|
||||
std::string GCode::extrude_loop(ExtrusionLoop loop, const std::string_view description, double speed)
|
||||
=======
|
||||
std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, double speed)
|
||||
>>>>>>> master_250
|
||||
{
|
||||
// get a copy; don't modify the orientation of the original loop object otherwise
|
||||
// next copies (if any) would not detect the correct orientation
|
||||
|
@ -2595,11 +2634,21 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, const std::string_view descr
|
|||
// find the point of the loop that is closest to the current extruder position
|
||||
// or randomize if requested
|
||||
Point last_pos = this->last_pos();
|
||||
<<<<<<< HEAD
|
||||
if (! m_config.spiral_vase && comment_is_perimeter(description)) {
|
||||
assert(m_layer != nullptr);
|
||||
m_seam_placer.place_seam(m_layer, loop, m_config.external_perimeters_first, this->last_pos());
|
||||
} else
|
||||
loop.split_at(last_pos, false);
|
||||
=======
|
||||
if (! m_config.spiral_vase && description == "perimeter") {
|
||||
assert(m_layer != nullptr);
|
||||
m_seam_placer.place_seam(m_layer, loop, m_config.external_perimeters_first, this->last_pos());
|
||||
} else
|
||||
// Because the G-code export has 1um resolution, don't generate segments shorter than 1.5 microns,
|
||||
// thus empty path segments will not be produced by G-code export.
|
||||
loop.split_at(last_pos, false, scaled<double>(0.0015));
|
||||
>>>>>>> master_250
|
||||
|
||||
// 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
|
||||
|
@ -2686,7 +2735,11 @@ std::string GCode::extrude_multi_path(ExtrusionMultiPath multipath, const std::s
|
|||
return gcode;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
std::string GCode::extrude_entity(const ExtrusionEntity &entity, const std::string_view description, double speed)
|
||||
=======
|
||||
std::string GCode::extrude_entity(const ExtrusionEntity &entity, std::string description, double speed)
|
||||
>>>>>>> master_250
|
||||
{
|
||||
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
|
||||
return this->extrude_path(*path, description, speed);
|
||||
|
@ -2721,7 +2774,11 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
|
|||
m_config.apply(print.get_print_region(®ion - &by_region.front()).config());
|
||||
|
||||
for (const ExtrusionEntity* ee : region.perimeters)
|
||||
<<<<<<< HEAD
|
||||
gcode += this->extrude_entity(*ee, comment_perimeter, -1.);
|
||||
=======
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1.);
|
||||
>>>>>>> master_250
|
||||
}
|
||||
return gcode;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue