mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-11 16:57:53 -06:00
ENABLE_SHADERS_MANAGER set as default
This commit is contained in:
parent
4d05ec0856
commit
314995fa0b
18 changed files with 57 additions and 1198 deletions
|
@ -156,16 +156,8 @@ GLCanvas3D::LayersEditing::~LayersEditing()
|
|||
|
||||
const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f;
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
void GLCanvas3D::LayersEditing::init()
|
||||
{
|
||||
#else
|
||||
bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename)
|
||||
{
|
||||
if (!m_shader.init(vertex_shader_filename, fragment_shader_filename))
|
||||
return false;
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP));
|
||||
|
@ -174,10 +166,6 @@ bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename,
|
|||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
|
||||
#if !ENABLE_SHADERS_MANAGER
|
||||
return true;
|
||||
#endif // !ENABLE_SHADERS_MANAGER
|
||||
}
|
||||
|
||||
void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config)
|
||||
|
@ -210,11 +198,7 @@ void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id)
|
|||
|
||||
bool GLCanvas3D::LayersEditing::is_allowed() const
|
||||
{
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0;
|
||||
#else
|
||||
return m_shader.is_initialized() && m_shader.get_shader()->shader_program_id > 0 && m_z_texture_id > 0;
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
}
|
||||
|
||||
bool GLCanvas3D::LayersEditing::is_enabled() const
|
||||
|
@ -372,11 +356,7 @@ Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas)
|
|||
|
||||
bool GLCanvas3D::LayersEditing::is_initialized() const
|
||||
{
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
return wxGetApp().get_shader("variable_layer_height") != nullptr;
|
||||
#else
|
||||
return m_shader.is_initialized();
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
}
|
||||
|
||||
std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const
|
||||
|
@ -410,7 +390,6 @@ std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) con
|
|||
|
||||
void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const
|
||||
{
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
@ -422,15 +401,6 @@ void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3
|
|||
shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas));
|
||||
shader->set_uniform("z_cursor_band_width", band_width);
|
||||
shader->set_uniform("object_max_z", m_object_max_z);
|
||||
#else
|
||||
m_shader.start_using();
|
||||
|
||||
m_shader.set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z));
|
||||
m_shader.set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height);
|
||||
m_shader.set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas));
|
||||
m_shader.set_uniform("z_cursor_band_width", band_width);
|
||||
m_shader.set_uniform("object_max_z", m_object_max_z);
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
|
||||
|
@ -450,11 +420,7 @@ void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3
|
|||
glsafe(::glEnd());
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
shader->stop_using();
|
||||
#else
|
||||
m_shader.stop_using();
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
}
|
||||
|
||||
void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect) const
|
||||
|
@ -488,7 +454,6 @@ void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const G
|
|||
{
|
||||
assert(this->is_allowed());
|
||||
assert(this->last_object_id != -1);
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
@ -508,85 +473,31 @@ void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const G
|
|||
shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height));
|
||||
shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas)));
|
||||
shader->set_uniform("z_cursor_band_width", float(this->band_width));
|
||||
#else
|
||||
GLint shader_id = m_shader.get_shader()->shader_program_id;
|
||||
assert(shader_id > 0);
|
||||
|
||||
GLint current_program_id;
|
||||
glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id));
|
||||
if (shader_id > 0 && shader_id != current_program_id)
|
||||
// The layer editing shader is not yet active. Activate it.
|
||||
glsafe(::glUseProgram(shader_id));
|
||||
else
|
||||
// The layer editing shader was already active.
|
||||
current_program_id = -1;
|
||||
// Initialize the layer height texture mapping.
|
||||
GLsizei w = (GLsizei)m_layers_texture.width;
|
||||
GLsizei h = (GLsizei)m_layers_texture.height;
|
||||
GLsizei half_w = w / 2;
|
||||
GLsizei half_h = h / 2;
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
|
||||
glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data()));
|
||||
glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4));
|
||||
for (const GLVolume* glvolume : volumes.volumes) {
|
||||
// Render the object using the layer editing shader and texture.
|
||||
if (! glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier)
|
||||
continue;
|
||||
|
||||
GLint z_to_texture_row_id = ::glGetUniformLocation(shader_id, "z_to_texture_row");
|
||||
GLint z_texture_row_to_normalized_id = ::glGetUniformLocation(shader_id, "z_texture_row_to_normalized");
|
||||
GLint z_cursor_id = ::glGetUniformLocation(shader_id, "z_cursor");
|
||||
GLint z_cursor_band_width_id = ::glGetUniformLocation(shader_id, "z_cursor_band_width");
|
||||
GLint world_matrix_id = ::glGetUniformLocation(shader_id, "volume_world_matrix");
|
||||
GLint object_max_z_id = ::glGetUniformLocation(shader_id, "object_max_z");
|
||||
glcheck();
|
||||
|
||||
if (z_to_texture_row_id != -1 && z_texture_row_to_normalized_id != -1 && z_cursor_id != -1 && z_cursor_band_width_id != -1 && world_matrix_id != -1)
|
||||
{
|
||||
const_cast<LayersEditing*>(this)->generate_layer_height_texture();
|
||||
|
||||
// Uniforms were resolved, go ahead using the layer editing shader.
|
||||
glsafe(::glUniform1f(z_to_texture_row_id, GLfloat(m_layers_texture.cells - 1) / (GLfloat(m_layers_texture.width) * GLfloat(m_object_max_z))));
|
||||
glsafe(::glUniform1f(z_texture_row_to_normalized_id, GLfloat(1.0f / m_layers_texture.height)));
|
||||
glsafe(::glUniform1f(z_cursor_id, GLfloat(m_object_max_z) * GLfloat(this->get_cursor_z_relative(canvas))));
|
||||
glsafe(::glUniform1f(z_cursor_band_width_id, GLfloat(this->band_width)));
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
// Initialize the layer height texture mapping.
|
||||
GLsizei w = (GLsizei)m_layers_texture.width;
|
||||
GLsizei h = (GLsizei)m_layers_texture.height;
|
||||
GLsizei half_w = w / 2;
|
||||
GLsizei half_h = h / 2;
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
|
||||
glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data()));
|
||||
glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4));
|
||||
for (const GLVolume* glvolume : volumes.volumes) {
|
||||
// Render the object using the layer editing shader and texture.
|
||||
if (! glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier)
|
||||
continue;
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
shader->set_uniform("volume_world_matrix", glvolume->world_matrix());
|
||||
shader->set_uniform("object_max_z", GLfloat(0));
|
||||
#else
|
||||
if (world_matrix_id != -1)
|
||||
glsafe(::glUniformMatrix4fv(world_matrix_id, 1, GL_FALSE, (const GLfloat*)glvolume->world_matrix().cast<float>().data()));
|
||||
if (object_max_z_id != -1)
|
||||
glsafe(::glUniform1f(object_max_z_id, GLfloat(0)));
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
glvolume->render();
|
||||
}
|
||||
// Revert back to the previous shader.
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
if (current_shader != nullptr)
|
||||
current_shader->start_using();
|
||||
#else
|
||||
if (current_program_id > 0)
|
||||
glsafe(::glUseProgram(current_program_id));
|
||||
shader->set_uniform("volume_world_matrix", glvolume->world_matrix());
|
||||
shader->set_uniform("object_max_z", GLfloat(0));
|
||||
glvolume->render();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Something went wrong. Just render the object.
|
||||
assert(false);
|
||||
for (const GLVolume* glvolume : volumes.volumes) {
|
||||
// Render the object using the layer editing shader and texture.
|
||||
if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier)
|
||||
continue;
|
||||
glsafe(::glUniformMatrix4fv(world_matrix_id, 1, GL_FALSE, (const GLfloat*)glvolume->world_matrix().cast<float>().data()));
|
||||
glvolume->render();
|
||||
}
|
||||
}
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
// Revert back to the previous shader.
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (current_shader != nullptr)
|
||||
current_shader->start_using();
|
||||
}
|
||||
|
||||
void GLCanvas3D::LayersEditing::adjust_layer_height_profile()
|
||||
|
@ -1710,22 +1621,8 @@ bool GLCanvas3D::init()
|
|||
if (m_multisample_allowed)
|
||||
glsafe(::glEnable(GL_MULTISAMPLE));
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
if (m_main_toolbar.is_enabled())
|
||||
m_layers_editing.init();
|
||||
#else
|
||||
if (!m_shader.init("gouraud.vs", "gouraud.fs"))
|
||||
{
|
||||
std::cout << "Unable to initialize gouraud shader: please, check that the files gouraud.vs and gouraud.fs are available" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_main_toolbar.is_enabled() && !m_layers_editing.init("variable_layer_height.vs", "variable_layer_height.fs"))
|
||||
{
|
||||
std::cout << "Unable to initialize variable_layer_height shader: please, check that the files variable_layer_height.vs and variable_layer_height.fs are available" << std::endl;
|
||||
return false;
|
||||
}
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
#if ENABLE_GCODE_VIEWER
|
||||
if (!m_main_toolbar.is_enabled())
|
||||
|
@ -4585,13 +4482,8 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool
|
|||
return ret;
|
||||
};
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
static const std::array<float, 4> orange = { 0.923f, 0.504f, 0.264f, 1.0f };
|
||||
static const std::array<float, 4> gray = { 0.64f, 0.64f, 0.64f, 1.0f };
|
||||
#else
|
||||
static const GLfloat orange[] = { 0.923f, 0.504f, 0.264f, 1.0f };
|
||||
static const GLfloat gray[] = { 0.64f, 0.64f, 0.64f, 1.0f };
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
GLVolumePtrs visible_volumes;
|
||||
|
||||
|
@ -4635,7 +4527,6 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool
|
|||
|
||||
camera.apply_projection(box, near_z, far_z);
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
@ -4648,43 +4539,14 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool
|
|||
|
||||
shader->start_using();
|
||||
shader->set_uniform("print_box.volume_detection", 0);
|
||||
#else
|
||||
if (transparent_background)
|
||||
glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
|
||||
glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
|
||||
m_shader.start_using();
|
||||
|
||||
GLint shader_id = m_shader.get_shader_program_id();
|
||||
GLint color_id = ::glGetUniformLocation(shader_id, "uniform_color");
|
||||
GLint print_box_detection_id = ::glGetUniformLocation(shader_id, "print_box.volume_detection");
|
||||
glcheck();
|
||||
|
||||
if (print_box_detection_id != -1)
|
||||
glsafe(::glUniform1i(print_box_detection_id, 0));
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
for (const GLVolume* vol : visible_volumes)
|
||||
{
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? orange : gray);
|
||||
#else
|
||||
if (color_id >= 0)
|
||||
glsafe(::glUniform4fv(color_id, 1, (vol->printable && !vol->is_outside) ? orange : gray));
|
||||
else
|
||||
glsafe(::glColor4fv((vol->printable && !vol->is_outside) ? orange : gray));
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
vol->render();
|
||||
}
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
shader->stop_using();
|
||||
#else
|
||||
m_shader.stop_using();
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
|
||||
|
@ -5610,13 +5472,11 @@ void GLCanvas3D::_render_objects() const
|
|||
|
||||
m_camera_clipping_plane = m_gizmos.get_clipping_plane();
|
||||
|
||||
if (m_picking_enabled)
|
||||
{
|
||||
if (m_picking_enabled) {
|
||||
// Update the layer editing selection to the first object selected, update the current object maximum Z.
|
||||
const_cast<LayersEditing&>(m_layers_editing).select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1);
|
||||
|
||||
if (m_config != nullptr)
|
||||
{
|
||||
if (m_config != nullptr) {
|
||||
const BoundingBoxf3& bed_bb = wxGetApp().plater()->get_bed().get_bounding_box(false);
|
||||
m_volumes.set_print_box((float)bed_bb.min(0), (float)bed_bb.min(1), 0.0f, (float)bed_bb.max(0), (float)bed_bb.max(1), (float)m_config->opt_float("max_print_height"));
|
||||
m_volumes.check_outside_state(m_config, nullptr);
|
||||
|
@ -5630,37 +5490,28 @@ void GLCanvas3D::_render_objects() const
|
|||
|
||||
m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data());
|
||||
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
|
||||
if (shader != nullptr)
|
||||
{
|
||||
if (shader != nullptr) {
|
||||
shader->start_using();
|
||||
#else
|
||||
m_shader.start_using();
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) {
|
||||
int object_id = m_layers_editing.last_object_id;
|
||||
m_volumes.render(GLVolumeCollection::Opaque, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) {
|
||||
// Which volume to paint without the layer height profile shader?
|
||||
return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
|
||||
});
|
||||
// Let LayersEditing handle rendering of the active object using the layer height profile shader.
|
||||
m_layers_editing.render_volumes(*this, this->m_volumes);
|
||||
}
|
||||
else {
|
||||
|
||||
if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) {
|
||||
int object_id = m_layers_editing.last_object_id;
|
||||
m_volumes.render(GLVolumeCollection::Opaque, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) {
|
||||
// Which volume to paint without the layer height profile shader?
|
||||
return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
|
||||
});
|
||||
// Let LayersEditing handle rendering of the active object using the layer height profile shader.
|
||||
m_layers_editing.render_volumes(*this, this->m_volumes);
|
||||
} else {
|
||||
// do not cull backfaces to show broken geometry, if any
|
||||
m_volumes.render(GLVolumeCollection::Opaque, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) {
|
||||
return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
m_volumes.render(GLVolumeCollection::Transparent, false, wxGetApp().plater()->get_camera().get_view_matrix());
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
shader->stop_using();
|
||||
m_volumes.render(GLVolumeCollection::Transparent, false, wxGetApp().plater()->get_camera().get_view_matrix());
|
||||
shader->stop_using();
|
||||
}
|
||||
#else
|
||||
m_shader.stop_using();
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
|
||||
m_camera_clipping_plane = ClippingPlane::ClipsNothing();
|
||||
}
|
||||
|
@ -6078,15 +5929,7 @@ void GLCanvas3D::_render_sla_slices() const
|
|||
|
||||
void GLCanvas3D::_render_selection_sidebar_hints() const
|
||||
{
|
||||
#if ENABLE_GCODE_VIEWER
|
||||
m_selection.render_sidebar_hints(m_sidebar_field);
|
||||
#else
|
||||
#if ENABLE_SHADERS_MANAGER
|
||||
m_selection.render_sidebar_hints(m_sidebar_field);
|
||||
#else
|
||||
m_selection.render_sidebar_hints(m_sidebar_field, m_shader);
|
||||
#endif // ENABLE_SHADERS_MANAGER
|
||||
#endif // ENABLE_GCODE_VIEWER
|
||||
}
|
||||
|
||||
void GLCanvas3D::_update_volumes_hover_state() const
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue