diff --git a/CMakeLists.txt b/CMakeLists.txt index e4d9404..383aee0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ enable_testing() find_package(Catch2 CONFIG REQUIRED) find_package(GLEW REQUIRED) +find_package(jsoncons CONFIG REQUIRED) find_package(mdspan CONFIG REQUIRED) find_package(wxWidgets CONFIG REQUIRED COMPONENTS core base net gl) diff --git a/src/pstack/calc/CMakeLists.txt b/src/pstack/calc/CMakeLists.txt index 05cd93d..0f62e9c 100644 --- a/src/pstack/calc/CMakeLists.txt +++ b/src/pstack/calc/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(pstack_calc STATIC mesh.cpp + part.cpp rotations.cpp sinterbox.cpp stacker.cpp @@ -20,6 +21,6 @@ set_target_properties(pstack_calc PROPERTIES PROJECT_LABEL "calc" ) target_link_libraries(pstack_calc - PUBLIC pstack_geo pstack_util + PUBLIC pstack_files pstack_geo pstack_util ) target_include_directories(pstack_calc PUBLIC "${PROJECT_SOURCE_DIR}/src") diff --git a/src/pstack/calc/part.cpp b/src/pstack/calc/part.cpp new file mode 100644 index 0000000..3d50a0d --- /dev/null +++ b/src/pstack/calc/part.cpp @@ -0,0 +1,51 @@ +#include "pstack/calc/part.hpp" +#include "pstack/files/stl.hpp" +#include +#include +#include +#include +#include + +namespace pstack::calc { + +namespace { + +std::optional get_base_quantity(std::string name) { + char looking_for = '.'; + if (name.ends_with(')')) { + name.pop_back(); + looking_for = '('; + } + std::size_t number_length = 0; + while (name.size() > number_length and std::isdigit(name[name.size() - number_length - 1])) { + ++number_length; + } + if (number_length == 0 or not (name.size() > number_length and name[name.size() - number_length - 1] == looking_for)) { + return std::nullopt; + } + std::string_view number{ name.data() + (name.size() - number_length), name.data() + name.size() }; + int out{-1}; + std::from_chars(number.data(), number.data() + number.size(), out); + return out; +} + +} // namespace + +part initialize_part(part_base base) { + part result{ std::move(base) }; + + result.name = std::filesystem::path(result.mesh_file).stem().string(); + result.mesh = files::from_stl(result.mesh_file); + result.mesh.set_baseline({ 0, 0, 0 }); + + result.base_quantity = get_base_quantity(result.name); + + auto volume_and_centroid = result.mesh.volume_and_centroid(); + result.volume = volume_and_centroid.volume; + result.centroid = volume_and_centroid.centroid; + result.triangle_count = result.mesh.triangles().size(); + + return result; +} + +} // namespace pstack::calc diff --git a/src/pstack/calc/part.hpp b/src/pstack/calc/part.hpp index 4142de7..62eac5e 100644 --- a/src/pstack/calc/part.hpp +++ b/src/pstack/calc/part.hpp @@ -7,23 +7,30 @@ namespace pstack::calc { -struct part { +// `part_base` is the part data needed to save and load +struct part_base { std::string mesh_file; + int quantity = 1; + bool mirrored = false; + int min_hole = 1; + int rotation_index = 1; + bool rotate_min_box = false; +}; + +// `part` is everything that can be derived from `part_base` +struct part : part_base { std::string name; mesh mesh; std::optional base_quantity; - int quantity; double volume; geo::point3 centroid; int triangle_count; - bool mirrored; - int min_hole; - int rotation_index; - bool rotate_min_box; }; +part initialize_part(part_base base); + } // namespace pstack::calc #endif // PSTACK_CALC_PART_HPP diff --git a/src/pstack/calc/sinterbox.cpp b/src/pstack/calc/sinterbox.cpp index e311c8f..8905a92 100644 --- a/src/pstack/calc/sinterbox.cpp +++ b/src/pstack/calc/sinterbox.cpp @@ -54,7 +54,8 @@ void append_side(std::vector& triangles, const util::mdspan, std::vector, std::vector> make_positions(const sinterbox_parameters& params) { - const auto [min, max, clearance, thickness, width, desired_spacing] = params; + const auto [clearance, thickness, width, desired_spacing] = params.settings; + const auto [min, max] = params.bounding; const geo::vector3 size = max - min; // Number of bars in the given direction @@ -111,8 +112,8 @@ void append_sinterbox(std::vector& triangles, const sinterbox_par upper_xy[x, y] = { positions_x[x], positions_y[y], upper_bound.z }; } } - append_side(triangles, lower_xy, geo::unit_z, geo::unit_x, geo::unit_y, params.thickness); - append_side(triangles, upper_xy, -geo::unit_z, geo::unit_x, geo::unit_y, params.thickness); + append_side(triangles, lower_xy, geo::unit_z, geo::unit_x, geo::unit_y, params.settings.thickness); + append_side(triangles, upper_xy, -geo::unit_z, geo::unit_x, geo::unit_y, params.settings.thickness); } // ZX sides @@ -126,8 +127,8 @@ void append_sinterbox(std::vector& triangles, const sinterbox_par upper_zx[z, x] = { positions_x[x], upper_bound.y, positions_z[z] }; } } - append_side(triangles, lower_zx, geo::unit_y, geo::unit_z, geo::unit_x, params.thickness); - append_side(triangles, upper_zx, -geo::unit_y, geo::unit_z, geo::unit_x, params.thickness); + append_side(triangles, lower_zx, geo::unit_y, geo::unit_z, geo::unit_x, params.settings.thickness); + append_side(triangles, upper_zx, -geo::unit_y, geo::unit_z, geo::unit_x, params.settings.thickness); } // YZ sides @@ -140,8 +141,8 @@ void append_sinterbox(std::vector& triangles, const sinterbox_par upper_yz[y, z] = { upper_bound.x, positions_y[y], positions_z[z] }; } } - append_side(triangles, lower_yz, geo::unit_x, geo::unit_y, geo::unit_z, params.thickness); - append_side(triangles, upper_yz, -geo::unit_x, geo::unit_y, geo::unit_z, params.thickness); + append_side(triangles, lower_yz, geo::unit_x, geo::unit_y, geo::unit_z, params.settings.thickness); + append_side(triangles, upper_yz, -geo::unit_x, geo::unit_y, geo::unit_z, params.settings.thickness); } } diff --git a/src/pstack/calc/sinterbox.hpp b/src/pstack/calc/sinterbox.hpp index 56b0a9b..6f8af4a 100644 --- a/src/pstack/calc/sinterbox.hpp +++ b/src/pstack/calc/sinterbox.hpp @@ -6,13 +6,21 @@ namespace pstack::calc { -struct sinterbox_parameters { +struct sinterbox_settings { + double clearance = 0.8; + double thickness = 0.8; + double width = 1.1; + double spacing = 6.0; +}; + +struct sinterbox_bounding { geo::point3 min; geo::point3 max; - double clearance; - double thickness; - double width; - double spacing; +}; + +struct sinterbox_parameters { + sinterbox_settings settings; + sinterbox_bounding bounding; }; void append_sinterbox(std::vector& triangles, const sinterbox_parameters& params); diff --git a/src/pstack/calc/stacker.cpp b/src/pstack/calc/stacker.cpp index 916da4f..669d29c 100644 --- a/src/pstack/calc/stacker.cpp +++ b/src/pstack/calc/stacker.cpp @@ -10,6 +10,15 @@ namespace pstack::calc { +void stack_result::reload_mesh() { + this->mesh = {}; + for (const auto& piece : pieces) { + auto m = piece.part->mesh; + m.rotate(piece.rotation); + this->mesh.add(m, piece.translation); + } +} + namespace { struct stack_state { @@ -129,7 +138,7 @@ std::optional stack_impl(const stack_parameters& params, const std state.voxels.assign(state.ordered_parts.size(), {}); double triangles = 0; - const double scale_factor = 1 / params.resolution; + const double scale_factor = 1 / params.settings.resolution; state.total_parts = 0; state.total_placed = 0; for (const std::shared_ptr part : state.ordered_parts) { @@ -227,13 +236,13 @@ std::optional stack_impl(const stack_parameters& params, const std } } - int max_x = static_cast(scale_factor * params.x_min); - int max_y = static_cast(scale_factor * params.y_min); - int max_z = static_cast(scale_factor * params.z_min); + int max_x = static_cast(scale_factor * params.settings.x_min); + int max_y = static_cast(scale_factor * params.settings.y_min); + int max_z = static_cast(scale_factor * params.settings.z_min); state.space = { - std::max(max_x, static_cast(scale_factor * params.x_max)), - std::max(max_y, static_cast(scale_factor * params.y_max)), - std::max(max_z, static_cast(scale_factor * params.z_max)) + std::max(max_x, static_cast(scale_factor * params.settings.x_max)), + std::max(max_y, static_cast(scale_factor * params.settings.y_max)), + std::max(max_z, static_cast(scale_factor * params.settings.z_max)) }; params.set_progress(0, 1); diff --git a/src/pstack/calc/stacker.hpp b/src/pstack/calc/stacker.hpp index f9874f4..2f1d36c 100644 --- a/src/pstack/calc/stacker.hpp +++ b/src/pstack/calc/stacker.hpp @@ -12,33 +12,46 @@ namespace pstack::calc { -struct stack_result { +// `stack_result_base` is the base level information about the result +template +struct stack_result_base { struct piece { - std::shared_ptr part; + Part part; geo::matrix3 rotation; geo::vector3 translation; }; std::vector pieces{}; + std::optional sinterbox{}; +}; +// `stack_result` is everything that can be calculated/derived from the `stack_result_base` +struct stack_result : stack_result_base, sinterbox_parameters> { mesh mesh{}; geo::vector3 size{}; double density{}; - std::optional sinterbox{}; + + void reload_mesh(); +}; + +struct stack_settings { + double resolution = 1.0; + int x_min = 150; + int x_max = 156; + int y_min = 150; + int y_max = 156; + int z_min = 30; + int z_max = 90; }; struct stack_parameters { std::vector> parts; + stack_settings settings; std::function set_progress; std::function)> display_mesh; std::function on_success; std::function on_failure; std::function on_finish; - - double resolution; - int x_min, x_max; - int y_min, y_max; - int z_min, z_max; }; class stacker { diff --git a/src/pstack/files/read.cpp b/src/pstack/files/read.cpp index 8ffd16a..219fd88 100644 --- a/src/pstack/files/read.cpp +++ b/src/pstack/files/read.cpp @@ -1,4 +1,5 @@ #include "pstack/files/read.hpp" +#include #include #include #include @@ -6,10 +7,10 @@ namespace pstack::files { // From StackOverflow https://stackoverflow.com/a/40903508 -std::string read_file(const std::string& file_path) { +std::expected read_file(const std::string& file_path) { std::ifstream file(file_path, std::ios::in | std::ios::binary); if (not file.is_open()) { - return {}; + return std::unexpected("Could not read file: " + file_path); } const auto size = std::filesystem::file_size(file_path); std::string result(size, '\0'); diff --git a/src/pstack/files/read.hpp b/src/pstack/files/read.hpp index 9efe818..7951702 100644 --- a/src/pstack/files/read.hpp +++ b/src/pstack/files/read.hpp @@ -1,11 +1,12 @@ #ifndef PSTACK_FILES_READ_HPP #define PSTACK_FILES_READ_HPP +#include #include namespace pstack::files { -std::string read_file(const std::string& file_path); +std::expected read_file(const std::string& file_path); } // namespace pstack::files diff --git a/src/pstack/files/stl.cpp b/src/pstack/files/stl.cpp index 5b9c725..a70b320 100644 --- a/src/pstack/files/stl.cpp +++ b/src/pstack/files/stl.cpp @@ -10,10 +10,11 @@ namespace pstack::files { calc::mesh from_stl(const std::string& file_path) { - std::string file = read_file(file_path); - if (file.empty()) { + auto file_expected = read_file(file_path); + if (not file_expected.has_value()) { return {}; } + std::string& file = *file_expected; const std::size_t file_size = file.size(); std::istringstream ss(std::move(file)); diff --git a/src/pstack/gui/CMakeLists.txt b/src/pstack/gui/CMakeLists.txt index 2e194b2..0ea20ac 100644 --- a/src/pstack/gui/CMakeLists.txt +++ b/src/pstack/gui/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(pstack_gui WIN32 main_window.cpp parts_list.cpp results_list.cpp + save.cpp viewport.cpp ) target_sources(pstack_gui PUBLIC FILE_SET headers TYPE HEADERS FILES @@ -15,6 +16,7 @@ target_sources(pstack_gui PUBLIC FILE_SET headers TYPE HEADERS FILES parts_list.hpp preferences.hpp results_list.hpp + save.hpp transformation.hpp viewport.hpp ) @@ -23,6 +25,7 @@ set_target_properties(pstack_gui PROPERTIES PROJECT_LABEL "gui" ) target_link_libraries(pstack_gui PRIVATE + jsoncons wx::net wx::core wx::base @@ -34,6 +37,10 @@ target_link_libraries(pstack_gui PRIVATE ) target_include_directories(pstack_gui PRIVATE "${PROJECT_SOURCE_DIR}/src") +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(pstack_gui PRIVATE "/bigobj") +endif() + set(PSTACK_OUTPUT_TYPE "GUI") set(PSTACK_OUTPUT_FILE_NAME "PartStackerGUI") pstack_configure_target_info(pstack_gui) diff --git a/src/pstack/gui/controls.cpp b/src/pstack/gui/controls.cpp index 866d75e..dae0774 100644 --- a/src/pstack/gui/controls.cpp +++ b/src/pstack/gui/controls.cpp @@ -202,24 +202,25 @@ void controls::initialize(main_window* parent) { } void controls::reset_values() { - min_clearance_spinner->SetValue(1); - quantity_spinner->SetValue(1); min_hole_spinner->SetValue(1); minimize_checkbox->SetValue(false); rotation_dropdown->SetSelection(1); - clearance_spinner->SetValue(0.8); - spacing_spinner->SetValue(6.0); - thickness_spinner->SetValue(0.8); - width_spinner->SetValue(1.1); - - initial_x_spinner->SetValue(150); - initial_y_spinner->SetValue(150); - initial_z_spinner->SetValue(30); - maximum_x_spinner->SetValue(156); - maximum_y_spinner->SetValue(156); - maximum_z_spinner->SetValue(90); + calc::sinterbox_settings sinterbox{}; // Get the defaults + clearance_spinner->SetValue(sinterbox.clearance); + spacing_spinner->SetValue(sinterbox.spacing); + thickness_spinner->SetValue(sinterbox.thickness); + width_spinner->SetValue(sinterbox.width); + + calc::stack_settings stack{}; // Get the defaults + min_clearance_spinner->SetValue(stack.resolution); + initial_x_spinner->SetValue(stack.x_min); + initial_y_spinner->SetValue(stack.y_min); + initial_z_spinner->SetValue(stack.z_min); + maximum_x_spinner->SetValue(stack.x_max); + maximum_y_spinner->SetValue(stack.y_max); + maximum_z_spinner->SetValue(stack.z_max); } } // namespace pstack::gui diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index f821f87..1ca40c2 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -1,10 +1,13 @@ +#include "pstack/files/read.hpp" #include "pstack/files/stl.hpp" #include "pstack/gui/constants.hpp" #include "pstack/gui/main_window.hpp" #include "pstack/gui/parts_list.hpp" +#include "pstack/gui/save.hpp" #include "pstack/gui/viewport.hpp" #include +#include #include #include #include @@ -43,6 +46,41 @@ main_window::main_window(const wxString& title) SetSizerAndFit(sizer); } +calc::stack_settings main_window::stack_settings() const { + return calc::stack_settings{ + .resolution = _controls.min_clearance_spinner->GetValue(), + .x_min = _controls.initial_x_spinner->GetValue(), .x_max = _controls.maximum_x_spinner->GetValue(), + .y_min = _controls.initial_y_spinner->GetValue(), .y_max = _controls.maximum_y_spinner->GetValue(), + .z_min = _controls.initial_z_spinner->GetValue(), .z_max = _controls.maximum_z_spinner->GetValue(), + }; +} + +void main_window::stack_settings(const calc::stack_settings& settings) { + _controls.min_clearance_spinner->SetValue(settings.resolution); + _controls.initial_x_spinner->SetValue(settings.x_min); + _controls.maximum_x_spinner->SetValue(settings.x_max); + _controls.initial_y_spinner->SetValue(settings.y_min); + _controls.maximum_y_spinner->SetValue(settings.y_max); + _controls.initial_z_spinner->SetValue(settings.z_min); + _controls.maximum_z_spinner->SetValue(settings.z_max); +} + +calc::sinterbox_settings main_window::sinterbox_settings() const { + return calc::sinterbox_settings{ + .clearance = _controls.clearance_spinner->GetValue(), + .thickness = _controls.thickness_spinner->GetValue(), + .width = _controls.width_spinner->GetValue(), + .spacing = _controls.spacing_spinner->GetValue(), + }; +} + +void main_window::sinterbox_settings(const calc::sinterbox_settings& settings) { + _controls.clearance_spinner->SetValue(settings.clearance); + _controls.thickness_spinner->SetValue(settings.thickness); + _controls.width_spinner->SetValue(settings.width); + _controls.spacing_spinner->SetValue(settings.spacing); +} + void main_window::on_select_parts(const std::vector& indices) { const bool any_selected = not indices.empty(); _controls.delete_part_button->Enable(any_selected); @@ -186,6 +224,7 @@ void main_window::on_stacking_start() { calc::stack_parameters params { .parts = _parts_list.get_all(), + .settings = stack_settings(), .set_progress = [this](double progress, double total) { CallAfter([=] { @@ -214,11 +253,6 @@ void main_window::on_stacking_start() { enable_on_stacking(false); }); }, - - .resolution = _controls.min_clearance_spinner->GetValue(), - .x_min = _controls.initial_x_spinner->GetValue(), .x_max = _controls.maximum_x_spinner->GetValue(), - .y_min = _controls.initial_y_spinner->GetValue(), .y_max = _controls.maximum_y_spinner->GetValue(), - .z_min = _controls.initial_z_spinner->GetValue(), .z_max = _controls.maximum_z_spinner->GetValue(), }; enable_on_stacking(true); _stacker_thread.start(std::move(params)); @@ -305,12 +339,10 @@ wxMenuBar* main_window::make_menu_bar() { return on_new(event); } case menu_item::open: { - wxMessageBox("Not yet implemented", "Error", wxICON_WARNING); - break; + return on_open(event); } case menu_item::save: { - wxMessageBox("Not yet implemented", "Error", wxICON_WARNING); - break; + return on_save(event); } case menu_item::close: { Close(); @@ -467,7 +499,7 @@ void main_window::bind_all_controls() { } void main_window::on_new(wxCommandEvent& event) { - if (_parts_list.rows() == 0 or + if ((_parts_list.rows() == 0 and _results_list.rows() == 0) or wxMessageBox("Clear the current working session?", "Warning", wxYES_NO | wxNO_DEFAULT | wxICON_INFORMATION) == wxYES) @@ -496,6 +528,72 @@ void main_window::on_close(wxCloseEvent& event) { event.Skip(); } +void main_window::on_open(wxCommandEvent& event) { + if ((_parts_list.rows() == 0 and _results_list.rows() == 0) or + wxMessageBox("Clear the current working session?", + "Warning", + wxYES_NO | wxNO_DEFAULT | wxICON_INFORMATION) == wxYES) + { + wxFileDialog dialog(this, "Open project", "", "", + "PartStacker project files (*.pstack.json)|*.pstack.json", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_CANCEL) { + return; + } + + auto file = files::read_file(dialog.GetPath().ToStdString()); + if (not file.has_value()) { + wxMessageBox(wxString::Format("Could not open path: %s\n\n%s", dialog.GetPath(), file.error()), "Error", wxICON_WARNING); + return; + } + + auto state = save_state_from_json(*file); + if (not state.has_value()) { + wxMessageBox(wxString::Format("Could not read project file: %s\n\n%s", dialog.GetPath(), state.error()), "Error", wxICON_WARNING); + return; + } + + _preferences = state->preferences; + _viewport->scroll_direction(_preferences.invert_scroll); + _parts_list.show_extra(_preferences.extra_parts); + _viewport->show_bounding_box(_preferences.show_bounding_box); + stack_settings(state->stack); + sinterbox_settings(state->sinterbox); + _parts_list.replace_all(std::move(state->parts)); + for (auto& result : state->results) { + result.reload_mesh(); + } + _results_list.replace_all(std::move(state->results)); + } + event.Skip(); +} + +void main_window::on_save(wxCommandEvent& event) { + wxFileDialog dialog(this, "Save project", "", "", + "PartStacker project files (*.pstack.json)|*.pstack.json", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (dialog.ShowModal() == wxID_CANCEL) { + return; + } + + const std::string path = dialog.GetPath().ToStdString(); + std::ofstream file(path); + if (not file.is_open()) { + wxMessageBox(wxString::Format("Could not open path: %s", path), "Error", wxICON_WARNING); + return; + } + file << save_state_to_json(out_save_state{ + .preferences = _preferences, + .stack = stack_settings(), + .sinterbox = sinterbox_settings(), + .parts = _parts_list.get_all(), + .results = _results_list.get_all(), + }); + event.Skip(); +} + void main_window::on_import_part(wxCommandEvent& event) { wxFileDialog dialog(this, "Import mesh", "", "", "STL files (*.stl)|*.stl", @@ -590,12 +688,11 @@ void main_window::on_sinterbox_result(wxCommandEvent& event) { const auto bounding = result.mesh.bounding(); result.mesh.set_baseline(geo::origin3 + offset); result.sinterbox = calc::sinterbox_parameters{ - .min = bounding.min + offset, - .max = bounding.max + offset, - .clearance = _controls.clearance_spinner->GetValue(), - .thickness = _controls.thickness_spinner->GetValue(), - .width = _controls.width_spinner->GetValue(), - .spacing = _controls.spacing_spinner->GetValue() + 0.00013759, + .settings = sinterbox_settings(), + .bounding{ + .min = bounding.min + offset, + .max = bounding.max + offset, + }, }; result.mesh.add_sinterbox(*result.sinterbox); diff --git a/src/pstack/gui/main_window.hpp b/src/pstack/gui/main_window.hpp index d6b7462..e26ddf5 100644 --- a/src/pstack/gui/main_window.hpp +++ b/src/pstack/gui/main_window.hpp @@ -11,6 +11,7 @@ #include #include #include "pstack/calc/stacker_thread.hpp" +#include "pstack/calc/stacker.hpp" #include "pstack/gui/controls.hpp" #include "pstack/gui/parts_list.hpp" #include "pstack/gui/preferences.hpp" @@ -28,6 +29,10 @@ class main_window : public wxFrame { viewport* _viewport = nullptr; controls _controls; preferences _preferences; + calc::stack_settings stack_settings() const; + void stack_settings(const calc::stack_settings&); + calc::sinterbox_settings sinterbox_settings() const; + void sinterbox_settings(const calc::sinterbox_settings&); void on_select_parts(const std::vector& indices); parts_list _parts_list{}; @@ -59,6 +64,8 @@ class main_window : public wxFrame { void bind_all_controls(); void on_new(wxCommandEvent& event); void on_close(wxCloseEvent& event); + void on_open(wxCommandEvent& event); + void on_save(wxCommandEvent& event); void on_import_part(wxCommandEvent& event); void on_delete_part(wxCommandEvent& event); void on_reload_part(wxCommandEvent& event); diff --git a/src/pstack/gui/parts_list.cpp b/src/pstack/gui/parts_list.cpp index fe8847c..117b08b 100644 --- a/src/pstack/gui/parts_list.cpp +++ b/src/pstack/gui/parts_list.cpp @@ -1,49 +1,16 @@ -#include "pstack/files/stl.hpp" #include "pstack/gui/parts_list.hpp" #include -#include -#include -#include namespace pstack::gui { namespace { calc::part make_part(std::string mesh_file, bool mirrored) { - calc::part part; - part.mesh_file = std::move(mesh_file); - part.name = std::filesystem::path(part.mesh_file).stem().string(); - part.mesh = files::from_stl(part.mesh_file); - part.mesh.set_baseline({ 0, 0, 0 }); - - part.base_quantity = [name = part.name]() mutable -> std::optional { - char looking_for = '.'; - if (name.ends_with(')')) { - name.pop_back(); - looking_for = '('; - } - std::size_t number_length = 0; - while (name.size() > number_length and std::isdigit(name[name.size() - number_length - 1])) { - ++number_length; - } - if (number_length == 0 or not (name.size() > number_length and name[name.size() - number_length - 1] == looking_for)) { - return std::nullopt; - } - std::string_view number{ name.data() + (name.size() - number_length), name.data() + name.size() }; - int out{-1}; - std::from_chars(number.data(), number.data() + number.size(), out); - return out; - }(); + calc::part_base base; + base.mesh_file = std::move(mesh_file); + base.mirrored = mirrored; + calc::part part = calc::initialize_part(std::move(base)); part.quantity = part.base_quantity.value_or(1); - - auto volume_and_centroid = part.mesh.volume_and_centroid(); - part.volume = volume_and_centroid.volume; - part.centroid = volume_and_centroid.centroid; - part.triangle_count = part.mesh.triangles().size(); - part.mirrored = mirrored; - part.min_hole = 1; - part.rotation_index = 1; - part.rotate_min_box = false; return part; } @@ -144,6 +111,21 @@ std::vector> parts_list::get_all() const { return out; } +void parts_list::replace_all(std::vector>&& parts) { + list_view::delete_all(); + _parts = std::move(parts); + for (auto& ppart : _parts) { + auto& part = *ppart; + list_view::append({ + part.name, + quantity_string(part, _show_extra), + wxString::Format("%.2f", part.volume / 1000), + std::to_string(part.triangle_count), + (part.mirrored ? "Mirrored" : "") + }); + } +} + void parts_list::update_label() { _total_parts = 0; _total_volume = 0; diff --git a/src/pstack/gui/parts_list.hpp b/src/pstack/gui/parts_list.hpp index 2f1fb01..8e700e2 100644 --- a/src/pstack/gui/parts_list.hpp +++ b/src/pstack/gui/parts_list.hpp @@ -32,6 +32,7 @@ class parts_list : public list_view { return *_parts.at(row); } std::vector> get_all() const; + void replace_all(std::vector>&& parts); void update_label(); wxWindow* label() const { diff --git a/src/pstack/gui/results_list.cpp b/src/pstack/gui/results_list.cpp index bd9868e..c24de34 100644 --- a/src/pstack/gui/results_list.cpp +++ b/src/pstack/gui/results_list.cpp @@ -24,7 +24,7 @@ void results_list::append(calc::stack_result input) { wxString::Format("%.1fx%.1fx%.1f", result.size.x, result.size.y, result.size.z), std::to_string(result.mesh.triangles().size()), (not result.sinterbox.has_value()) ? wxString("none") - : wxString::Format("%.1f,%.1f,%.1f,%.1f", result.sinterbox->clearance, result.sinterbox->spacing, result.sinterbox->thickness, result.sinterbox->width), + : wxString::Format("%.1f,%.1f,%.1f,%.1f", result.sinterbox->settings.clearance, result.sinterbox->settings.spacing, result.sinterbox->settings.thickness, result.sinterbox->settings.width), }); } @@ -37,4 +37,11 @@ void results_list::delete_selected() { list_view::delete_selected(_results); } +void results_list::replace_all(std::vector&& results) { + list_view::delete_all(); + for (auto& result : results) { + append(std::move(result)); + } +} + } // namespace pstack::gui diff --git a/src/pstack/gui/results_list.hpp b/src/pstack/gui/results_list.hpp index 45b9aa5..eb68a8e 100644 --- a/src/pstack/gui/results_list.hpp +++ b/src/pstack/gui/results_list.hpp @@ -22,6 +22,10 @@ class results_list : public list_view { calc::stack_result& at(std::size_t row) { return _results.at(row); } + const std::vector& get_all() const { + return _results; + } + void replace_all(std::vector&& results); private: std::vector _results; diff --git a/src/pstack/gui/save.cpp b/src/pstack/gui/save.cpp new file mode 100644 index 0000000..e06083b --- /dev/null +++ b/src/pstack/gui/save.cpp @@ -0,0 +1,307 @@ +#include "pstack/gui/save.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pstack::gui { +namespace { + +struct save_part : calc::part_base { + bool in_parts_list; +}; + +struct save_sinterbox_parameters : calc::sinterbox_settings, calc::sinterbox_bounding {}; + +using save_result = calc::stack_result_base; + +using internal_save_state = basic_save_state; + +} // namespace +} // namespace pstack::gui + +JSONCONS_N_MEMBER_TRAITS(pstack::gui::preferences, 0, // Nothing is required + invert_scroll, extra_parts, show_bounding_box +); +JSONCONS_N_MEMBER_TRAITS(pstack::calc::stack_settings, 0, // Nothing is required + resolution, x_min, x_max, y_min, y_max, z_min, z_max +); +JSONCONS_N_MEMBER_TRAITS(pstack::calc::sinterbox_settings, 0, // Nothing is required + clearance, thickness, width, spacing +); +JSONCONS_ALL_MEMBER_TRAITS(pstack::gui::save_part, + mesh_file, quantity, mirrored, min_hole, rotation_index, rotate_min_box, in_parts_list +); + +JSONCONS_ALL_MEMBER_TRAITS(pstack::geo::point3, + x, y, z +); +JSONCONS_ALL_MEMBER_TRAITS(pstack::geo::vector3, + x, y, z +); +JSONCONS_ALL_MEMBER_TRAITS(pstack::geo::matrix3, + xx, xy, xz, yx, yy, yz, zx, zy, zz +); +JSONCONS_ALL_MEMBER_TRAITS(pstack::gui::save_result::piece, + part, rotation, translation +); +JSONCONS_ALL_MEMBER_TRAITS(pstack::gui::save_sinterbox_parameters, + clearance, thickness, width, spacing, min, max +); +JSONCONS_N_MEMBER_TRAITS(pstack::gui::save_result, 1, + pieces, // mandatory + sinterbox // optional +); + +JSONCONS_ALL_MEMBER_TRAITS(pstack::gui::internal_save_state, + preferences, stack, sinterbox, parts, results +); + +namespace { + +const std::string_view the_schema = R"the_schema({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A representation of the state of the PartStacker project", + "type": "object", + "$defs": { + "unsigned_int": { + "type": "integer", + "minimum": 0 + }, + "point3": { + "type": "object", + "properties": { + "x": { "type": "number" }, + "y": { "type": "number" }, + "z": { "type": "number" } + }, + "required": ["x", "y", "z"] + }, + "matrix3": { + "type": "object", + "properties": { + "xx": { "type": "number" }, + "xy": { "type": "number" }, + "xz": { "type": "number" }, + "yx": { "type": "number" }, + "yy": { "type": "number" }, + "yz": { "type": "number" }, + "zx": { "type": "number" }, + "zy": { "type": "number" }, + "zz": { "type": "number" } + }, + "required": ["xx", "xy", "xz", "yx", "yy", "yz", "zx", "zy", "zz"] + } + }, + "properties": { + "preferences": { + "type": "object", + "properties": { + "invert_scroll": { "type": "boolean" }, + "extra_parts": { "type": "boolean" }, + "show_bounding_box": { "type": "boolean" } + } + }, + "stack": { + "type": "object", + "properties": { + "resolution": { "type": "number" }, + "x_min": { "$ref": "#/$defs/unsigned_int" }, + "x_max": { "$ref": "#/$defs/unsigned_int" }, + "y_min": { "$ref": "#/$defs/unsigned_int" }, + "y_max": { "$ref": "#/$defs/unsigned_int" }, + "z_min": { "$ref": "#/$defs/unsigned_int" }, + "z_max": { "$ref": "#/$defs/unsigned_int" } + } + }, + "sinterbox": { + "type": "object", + "properties": { + "clearance": { "type": "number" }, + "thickness": { "type": "number" }, + "width": { "type": "number" }, + "spacing": { "type": "number" } + } + }, + "parts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "mesh_file": { "type": "string" }, + "quantity": { "$ref": "#/$defs/unsigned_int" }, + "mirrored": { "type": "boolean" }, + "min_hole": { "$ref": "#/$defs/unsigned_int" }, + "rotation_index": { "type": "integer", "minimum": 0, "maximum": 2 }, + "rotate_min_box": { "type": "boolean" }, + "in_parts_list": { "type": "boolean" } + }, + "required": ["mesh_file", "quantity", "mirrored", "min_hole", "rotation_index", "rotate_min_box", "in_parts_list"] + } + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "pieces": { + "type": "array", + "items": { + "type": "object", + "properties": { + "part": { "$ref": "#/$defs/unsigned_int" }, + "rotation": { "$ref": "#/$defs/matrix3" }, + "translation": { "$ref": "#/$defs/point3" } + }, + "required": ["rotation", "translation", "part"] + } + }, + "sinterbox": { + "type": "object", + "properties": { + "clearance": { "type": "number" }, + "thickness": { "type": "number" }, + "width": { "type": "number" }, + "spacing": { "type": "number" }, + "min": { "$ref": "#/$defs/point3" }, + "max": { "$ref": "#/$defs/point3" } + }, + "required": ["clearance", "thickness", "width", "spacing", "min", "max"] + } + }, + "required": ["pieces"] + } + } + }, + "required": ["preferences", "stack", "sinterbox", "parts", "results"] +})the_schema"; + +} // namespace + +namespace pstack::gui { + +namespace { + +in_save_state from_internal(const internal_save_state& state) { + in_save_state s{}; + s.preferences = state.preferences; + s.stack = state.stack; + s.sinterbox = state.sinterbox; + + for (const save_part& part : state.parts) { + auto& list = part.in_parts_list ? s.parts : s.extra_parts; + calc::part p = calc::initialize_part(static_cast(part)); + list.push_back(std::make_shared(std::move(p))); + } + + for (const save_result& result : state.results) { + auto& r = s.results.emplace_back(); + if (result.sinterbox.has_value()) { + r.sinterbox.emplace(); + r.sinterbox->settings = static_cast(*result.sinterbox); + r.sinterbox->bounding = static_cast(*result.sinterbox); + } + for (const auto& piece : result.pieces) { + auto& p = r.pieces.emplace_back(); + p.rotation = piece.rotation; + p.translation = piece.translation; + if (piece.part < s.parts.size()) { + p.part = s.parts[piece.part]; + } else { + p.part = s.extra_parts[piece.part - s.parts.size()]; + } + } + } + + return s; +}; + +internal_save_state to_internal(const out_save_state& state) { + std::vector all_parts = state.parts; // Copy + + std::vector results{}; + for (const auto& in_result : state.results) { + auto& out_result = results.emplace_back(); + if (in_result.sinterbox.has_value()) { + out_result.sinterbox.emplace(); + static_cast(*out_result.sinterbox) = in_result.sinterbox->settings; + static_cast(*out_result.sinterbox) = in_result.sinterbox->bounding; + } + for (const auto& in_piece : in_result.pieces) { + auto& out_piece = out_result.pieces.emplace_back(); + out_piece.rotation = in_piece.rotation; + out_piece.translation = in_piece.translation; + auto it = std::ranges::find(all_parts, in_piece.part); + if (it != all_parts.end()) { + out_piece.part = std::ranges::distance(all_parts.begin(), it); + } else { + out_piece.part = all_parts.size(); + all_parts.push_back(in_piece.part); + } + } + } + + std::vector parts{}; + for (std::size_t i = 0; i != all_parts.size(); ++i) { + const calc::part_base& part = *(all_parts[i]); + parts.emplace_back(part); + parts.back().in_parts_list = (i < state.parts.size()); + } + + return internal_save_state{ + .preferences = state.preferences, + .stack = state.stack, + .sinterbox = state.sinterbox, + .parts = std::move(parts), + .results = std::move(results), + }; +}; + +} // namespace + +std::expected save_state_from_json(std::string_view str) { + try { + + const jsoncons::json j = jsoncons::json::parse(str); + + static const auto schema = []{ + auto schema = jsoncons::json::parse(the_schema); + return jsoncons::jsonschema::make_json_schema(std::move(schema)); + }(); + + std::vector errors; + const auto schema_reporter = [&](const jsoncons::jsonschema::validation_message& message) { + errors.push_back(std::format("{}: {}", message.instance_location().string(), message.message())); + return jsoncons::jsonschema::walk_result::advance; + }; + schema.validate(j, schema_reporter); + if (not errors.empty()) { + errors.insert(errors.begin(), "File does not conform to schema:"); + auto view = errors | std::views::join_with(std::string_view("\n ")); + std::string full_error{view.begin(), view.end()}; + return std::unexpected(std::move(full_error)); + } + + return from_internal(j.as()); + + } catch (const jsoncons::ser_error& e) { + return std::unexpected(std::string("Failed to parse JSON with ser_error: ") + e.what()); + } catch (const jsoncons::json_exception& e) { + return std::unexpected(std::string("Failed to parse JSON with json_exception: ") + e.what()); + } catch (const std::exception& e) { + return std::unexpected(std::string("Failed to parse JSON with std::exception: ") + e.what()); + } catch (...) { + return std::unexpected("Failed to parse JSON with unknown error"); + } +} + +std::string save_state_to_json(const out_save_state& state) { + return jsoncons::json(to_internal(state)).to_string(); +} + +} // namespace pstack::gui diff --git a/src/pstack/gui/save.hpp b/src/pstack/gui/save.hpp new file mode 100644 index 0000000..5f7bd6b --- /dev/null +++ b/src/pstack/gui/save.hpp @@ -0,0 +1,41 @@ +#ifndef PSTACK_GUI_SAVE_HPP +#define PSTACK_GUI_SAVE_HPP + +#include "pstack/calc/part.hpp" +#include "pstack/calc/stacker.hpp" +#include "pstack/gui/preferences.hpp" +#include +#include +#include +#include +#include + +namespace pstack::gui { + +template +struct basic_save_state { + preferences preferences; + calc::stack_settings stack; + calc::sinterbox_settings sinterbox; + std::vector parts; + std::vector results; +}; + +using out_save_state = basic_save_state< + std::shared_ptr, + calc::stack_result +>; + +struct in_save_state : basic_save_state< + std::shared_ptr, + calc::stack_result +> { + std::vector> extra_parts; +}; + +std::expected save_state_from_json(std::string_view str); +std::string save_state_to_json(const out_save_state& state); + +} // namespace pstack::gui + +#endif // PSTACK_GUI_SAVE_HPP diff --git a/vcpkg.json b/vcpkg.json index a6b97c3..505bce8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,6 +3,7 @@ "dependencies": [ "catch2", "glew", + "jsoncons", "mdspan", "wxwidgets" ]