diff options
author | Lukas Matena <lukasmatena@seznam.cz> | 2019-12-12 13:37:33 +0300 |
---|---|---|
committer | Lukas Matena <lukasmatena@seznam.cz> | 2019-12-12 13:37:33 +0300 |
commit | 537260494df5736b5a36677b86cf04c364fbf831 (patch) | |
tree | 838db423c8bc7f4d3594267398c6ad2b2c591d89 /src | |
parent | 135660decfd2cdb6d8b26059c05fa084e399bff2 (diff) | |
parent | c284a65caaa6d859164c33f858512d7d8d3d0f5c (diff) |
Merge branch 'master' into lm_tm_hollowing
Diffstat (limited to 'src')
103 files changed, 9887 insertions, 1376 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1bf278722..54ebf33b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,4 @@ +cmake_minimum_required(VERSION 3.8) project(PrusaSlicer-native) add_subdirectory(build-utils) @@ -22,6 +23,8 @@ add_subdirectory(libslic3r) if (SLIC3R_GUI) add_subdirectory(imgui) + add_subdirectory(hidapi) + include_directories(hidapi/include) if(WIN32) message(STATUS "WXWIN environment set to: $ENV{WXWIN}") @@ -131,13 +134,13 @@ target_link_libraries(PrusaSlicer libslic3r_gui ${wxWidgets_LIBRARIES}) if (MSVC) # Generate debug symbols even in release mode. target_link_options(PrusaSlicer PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>") - target_link_libraries(PrusaSlicer user32.lib Setupapi.lib OpenGL32.Lib GlU32.Lib) + target_link_libraries(PrusaSlicer user32.lib Setupapi.lib) elseif (MINGW) - target_link_libraries(PrusaSlicer opengl32 ws2_32 uxtheme setupapi) + target_link_libraries(PrusaSlicer ws2_32 uxtheme setupapi) elseif (APPLE) target_link_libraries(PrusaSlicer "-framework OpenGL") else () - target_link_libraries(PrusaSlicer -ldl -lGL -lGLU) + target_link_libraries(PrusaSlicer -ldl) endif () endif () @@ -193,6 +196,11 @@ if (WIN32) VERBATIM ) endif () + + # This has to be a separate target due to the windows command line lenght limits + add_custom_target(PrusaSlicerDllsCopy ALL DEPENDS PrusaSlicer) + prusaslicer_copy_dlls(PrusaSlicerDllsCopy) + elseif (XCODE) # Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level add_custom_command(TARGET PrusaSlicer POST_BUILD diff --git a/src/admesh/connect.cpp b/src/admesh/connect.cpp index d6de6ce6a..c266c724c 100644 --- a/src/admesh/connect.cpp +++ b/src/admesh/connect.cpp @@ -601,11 +601,12 @@ void stl_remove_unconnected_facets(stl_file *stl) stl->neighbors_start[facet].which_vertex_not[edge[1]], stl->neighbors_start[facet].which_vertex_not[edge[2]] }; + // Update statistics on edge connectivity. - if (neighbor[0] == -1) - stl_update_connects_remove_1(neighbor[1]); - if (neighbor[1] == -1) - stl_update_connects_remove_1(neighbor[0]); + if ((neighbor[0] == -1) && (neighbor[1] != -1)) + stl_update_connects_remove_1(neighbor[1]); + if ((neighbor[1] == -1) && (neighbor[0] != -1)) + stl_update_connects_remove_1(neighbor[0]); if (neighbor[0] >= 0) { if (neighbor[1] >= 0) { diff --git a/src/hidapi/CMakeLists.txt b/src/hidapi/CMakeLists.txt new file mode 100644 index 000000000..1f53c9b69 --- /dev/null +++ b/src/hidapi/CMakeLists.txt @@ -0,0 +1,19 @@ + +if (WIN32) + set(HIDAPI_IMPL win/hid.c) +elseif (APPLE) + set(HIDAPI_IMPL mac/hid.c) +else () + # Assume Linux or Unix other than Mac OS + set(HIDAPI_IMPL linux/hid.c) +endif() + +include_directories(include) + +add_library(hidapi STATIC ${HIDAPI_IMPL}) + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Don't link the udev library, as there are two versions out there (libudev.so.0, libudev.so.1), so they are linked explicitely. +# target_link_libraries(hidapi udev) + target_link_libraries(hidapi) +endif() diff --git a/src/hidapi/include/hidapi.h b/src/hidapi/include/hidapi.h new file mode 100644 index 000000000..1819f8de0 --- /dev/null +++ b/src/hidapi/include/hidapi.h @@ -0,0 +1,395 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/** @file + * @defgroup API hidapi API + */ + +#ifndef HIDAPI_H__ +#define HIDAPI_H__ + +#include <wchar.h> + +#ifdef _WIN32 + #define HID_API_EXPORT __declspec(dllexport) + #define HID_API_CALL +#else + #define HID_API_EXPORT /**< API export macro */ + #define HID_API_CALL /**< API call macro */ +#endif + +#define HID_API_EXPORT_CALL HID_API_EXPORT HID_API_CALL /**< API export and call macro*/ + +#ifdef __cplusplus +extern "C" { +#endif + struct hid_device_; + typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ + + /** hidapi info structure */ + struct hid_device_info { + /** Platform-specific device path */ + char *path; + /** Device Vendor ID */ + unsigned short vendor_id; + /** Device Product ID */ + unsigned short product_id; + /** Serial Number */ + wchar_t *serial_number; + /** Device Release Number in binary-coded decimal, + also known as Device Version Number */ + unsigned short release_number; + /** Manufacturer String */ + wchar_t *manufacturer_string; + /** Product string */ + wchar_t *product_string; + /** Usage Page for this Device/Interface + (Windows/Mac only). */ + unsigned short usage_page; + /** Usage for this Device/Interface + (Windows/Mac only).*/ + unsigned short usage; + /** The USB interface which this logical device + represents. + + * Valid on both Linux implementations in all cases. + * Valid on the Windows implementation only if the device + contains more than one interface. + * Valid on the Mac implementation if and only if the device + is a USB HID device. */ + int interface_number; + + /** Pointer to the next device */ + struct hid_device_info *next; + }; + + + /** @brief Initialize the HIDAPI library. + + This function initializes the HIDAPI library. Calling it is not + strictly necessary, as it will be called automatically by + hid_enumerate() and any of the hid_open_*() functions if it is + needed. This function should be called at the beginning of + execution however, if there is a chance of HIDAPI handles + being opened by different threads simultaneously. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_init(void); + + /** @brief Finalize the HIDAPI library. + + This function frees all of the static data associated with + HIDAPI. It should be called at the end of execution to avoid + memory leaks. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_exit(void); + + /** @brief Enumerate the HID Devices. + + This function returns a linked list of all the HID devices + attached to the system which match vendor_id and product_id. + If @p vendor_id is set to 0 then any vendor matches. + If @p product_id is set to 0 then any product matches. + If @p vendor_id and @p product_id are both set to 0, then + all HID devices will be returned. + + @ingroup API + @param vendor_id The Vendor ID (VID) of the types of device + to open. + @param product_id The Product ID (PID) of the types of + device to open. + + @returns + This function returns a pointer to a linked list of type + struct #hid_device_info, containing information about the HID devices + attached to the system, or NULL in the case of failure. Free + this linked list by calling hid_free_enumeration(). + */ + struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id); + + /** @brief Free an enumeration Linked List + + This function frees a linked list created by hid_enumerate(). + + @ingroup API + @param devs Pointer to a list of struct_device returned from + hid_enumerate(). + */ + void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); + + /** @brief Open a HID device using a Vendor ID (VID), Product ID + (PID) and optionally a serial number. + + If @p serial_number is NULL, the first device with the + specified VID and PID is opened. + + @ingroup API + @param vendor_id The Vendor ID (VID) of the device to open. + @param product_id The Product ID (PID) of the device to open. + @param serial_number The Serial Number of the device to open + (Optionally NULL). + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); + + /** @brief Open a HID device by its path name. + + The path name be determined by calling hid_enumerate(), or a + platform-specific path name can be used (eg: /dev/hidraw0 on + Linux). + + @ingroup API + @param path The path name of the device to open + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path); + + /** @brief Write an Output report to a HID device. + + The first byte of @p data[] must contain the Report ID. For + devices which only support a single report, this must be set + to 0x0. The remaining bytes contain the report data. Since + the Report ID is mandatory, calls to hid_write() will always + contain one more byte than the report contains. For example, + if a hid report is 16 bytes long, 17 bytes must be passed to + hid_write(), the Report ID (or 0x0, for devices with a + single report), followed by the report data (16 bytes). In + this example, the length passed in would be 17. + + hid_write() will send the data on the first OUT endpoint, if + one exists. If it does not, it will send the data through + the Control Endpoint (Endpoint 0). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length); + + /** @brief Read an Input report from a HID device with timeout. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + @param milliseconds timeout in milliseconds or -1 for blocking wait. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read within + the timeout period, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds); + + /** @brief Read an Input report from a HID device. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read and + the handle is in non-blocking mode, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Set the device handle to be non-blocking. + + In non-blocking mode calls to hid_read() will return + immediately with a value of 0 if there is no data to be + read. In blocking mode, hid_read() will wait (block) until + there is data to read before returning. + + Nonblocking can be turned on and off at any time. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param nonblock enable or not the nonblocking reads + - 1 to enable nonblocking + - 0 to disable nonblocking. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); + + /** @brief Send a Feature report to the device. + + Feature reports are sent over the Control endpoint as a + Set_Report transfer. The first byte of @p data[] must + contain the Report ID. For devices which only support a + single report, this must be set to 0x0. The remaining bytes + contain the report data. Since the Report ID is mandatory, + calls to hid_send_feature_report() will always contain one + more byte than the report contains. For example, if a hid + report is 16 bytes long, 17 bytes must be passed to + hid_send_feature_report(): the Report ID (or 0x0, for + devices which do not use numbered reports), followed by the + report data (16 bytes). In this example, the length passed + in would be 17. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send, including + the report number. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length); + + /** @brief Get a feature report from a HID device. + + Set the first byte of @p data[] to the Report ID of the + report to be read. Make sure to allow space for this + extra byte in @p data[]. Upon return, the first byte will + still contain the Report ID, and the report data will + start in data[1]. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into, including + the Report ID. Set the first byte of @p data[] to the + Report ID of the report to be read, or set it to zero + if your device does not use numbered reports. + @param length The number of bytes to read, including an + extra byte for the report ID. The buffer can be longer + than the actual report. + + @returns + This function returns the number of bytes read plus + one for the report ID (which is still in the first + byte), or -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Close a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + */ + void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev); + + /** @brief Get The Manufacturer String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get The Product String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get The Serial Number String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get a string from a HID device, based on its string index. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string_index The index of the string to get. + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen); + + /** @brief Get a string describing the last error which occurred. + + @ingroup API + @param dev A device handle returned from hid_open(). + + @returns + This function returns a string containing the last error + which occurred or NULL if none has occurred. + */ + HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/src/hidapi/linux/hid.c b/src/hidapi/linux/hid.c new file mode 100644 index 000000000..a62bcffb6 --- /dev/null +++ b/src/hidapi/linux/hid.c @@ -0,0 +1,918 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + Linux Version - 6/2/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/* C */ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <locale.h> +#include <errno.h> + +/* Unix */ +#include <dlfcn.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <sys/utsname.h> +#include <fcntl.h> +#include <poll.h> + +/* Linux */ +#include <linux/hidraw.h> +#include <linux/version.h> +#include <linux/input.h> +#include <libudev.h> + +#include "hidapi.h" + +typedef const char* (*hid_wrapper_udev_device_get_devnode_type)(struct udev_device *udev_device); +typedef struct udev_device* (*hid_wrapper_udev_device_get_parent_with_subsystem_devtype_type)(struct udev_device *udev_device, const char *subsystem, const char *devtype); +typedef const char* (*hid_wrapper_udev_device_get_sysattr_value_type)(struct udev_device *udev_device, const char *sysattr); +typedef struct udev_device* (*hid_wrapper_udev_device_new_from_devnum_type)(struct udev *udev, char type, dev_t devnum); +typedef struct udev_device* (*hid_wrapper_udev_device_new_from_syspath_type)(struct udev *udev, const char *syspath); +typedef struct udev_device* (*hid_wrapper_udev_device_unref_type)(struct udev_device *udev_device); +typedef int (*hid_wrapper_udev_enumerate_add_match_subsystem_type)(struct udev_enumerate *udev_enumerate, const char *subsystem); +typedef struct udev_list_entry* (*hid_wrapper_udev_enumerate_get_list_entry_type)(struct udev_enumerate *udev_enumerate); +typedef struct udev_enumerate* (*hid_wrapper_udev_enumerate_new_type)(struct udev *udev); +typedef int (*hid_wrapper_udev_enumerate_scan_devices_type)(struct udev_enumerate *udev_enumerate); +typedef struct udev_enumerate* (*hid_wrapper_udev_enumerate_unref_type)(struct udev_enumerate *udev_enumerate); +typedef const char* (*hid_wrapper_udev_list_entry_get_name_type)(struct udev_list_entry *list_entry); +typedef struct udev_list_entry* (*hid_wrapper_udev_list_entry_get_next_type)(struct udev_list_entry *list_entry); +typedef struct udev* (*hid_wrapper_udev_new_type)(void); +typedef struct udev* (*hid_wrapper_udev_unref_type)(struct udev *udev); + +void *hid_wrapper_handle = NULL; +static hid_wrapper_udev_device_get_devnode_type hid_wrapper_udev_device_get_devnode = NULL; +static hid_wrapper_udev_device_get_parent_with_subsystem_devtype_type hid_wrapper_udev_device_get_parent_with_subsystem_devtype = NULL; +static hid_wrapper_udev_device_get_sysattr_value_type hid_wrapper_udev_device_get_sysattr_value = NULL; +static hid_wrapper_udev_device_new_from_devnum_type hid_wrapper_udev_device_new_from_devnum = NULL; +static hid_wrapper_udev_device_new_from_syspath_type hid_wrapper_udev_device_new_from_syspath = NULL; +static hid_wrapper_udev_device_unref_type hid_wrapper_udev_device_unref = NULL; +static hid_wrapper_udev_enumerate_add_match_subsystem_type hid_wrapper_udev_enumerate_add_match_subsystem = NULL; +static hid_wrapper_udev_enumerate_get_list_entry_type hid_wrapper_udev_enumerate_get_list_entry = NULL; +static hid_wrapper_udev_enumerate_new_type hid_wrapper_udev_enumerate_new = NULL; +static hid_wrapper_udev_enumerate_scan_devices_type hid_wrapper_udev_enumerate_scan_devices = NULL; +static hid_wrapper_udev_enumerate_unref_type hid_wrapper_udev_enumerate_unref = NULL; +static hid_wrapper_udev_list_entry_get_name_type hid_wrapper_udev_list_entry_get_name = NULL; +static hid_wrapper_udev_list_entry_get_next_type hid_wrapper_udev_list_entry_get_next = NULL; +static hid_wrapper_udev_new_type hid_wrapper_udev_new = NULL; +static hid_wrapper_udev_unref_type hid_wrapper_udev_unref = NULL; + +static void hid_wrapper_udev_close() +{ + if (hid_wrapper_handle) + dlclose(hid_wrapper_handle); + hid_wrapper_handle = NULL; + hid_wrapper_udev_device_get_devnode = NULL; + hid_wrapper_udev_device_get_parent_with_subsystem_devtype = NULL; + hid_wrapper_udev_device_get_sysattr_value = NULL; + hid_wrapper_udev_device_new_from_devnum = NULL; + hid_wrapper_udev_device_new_from_syspath = NULL; + hid_wrapper_udev_device_unref = NULL; + hid_wrapper_udev_enumerate_add_match_subsystem = NULL; + hid_wrapper_udev_enumerate_get_list_entry = NULL; + hid_wrapper_udev_enumerate_new = NULL; + hid_wrapper_udev_enumerate_scan_devices = NULL; + hid_wrapper_udev_enumerate_unref = NULL; + hid_wrapper_udev_list_entry_get_name = NULL; + hid_wrapper_udev_list_entry_get_next = NULL; + hid_wrapper_udev_new = NULL; + hid_wrapper_udev_unref = NULL; +} + +static const char *hid_wrapper_libudev_paths[] = { + "libudev.so.1", "libudev.so.0" +}; + +static int hid_wrapper_udev_init() +{ + int i; + + hid_wrapper_udev_close(); + + // Search for the libudev0 or libudev1 library. + for (i = 0; i < sizeof(hid_wrapper_libudev_paths) / sizeof(hid_wrapper_libudev_paths[0]); ++ i) + if ((hid_wrapper_handle = dlopen(hid_wrapper_libudev_paths[i], RTLD_NOW | RTLD_GLOBAL)) != NULL) + break; + + if (hid_wrapper_handle == NULL) { + // Error, close the shared library handle and finish. + hid_wrapper_udev_close(); + return -1; + } + + // Resolve the functions. + hid_wrapper_udev_device_get_devnode = (hid_wrapper_udev_device_get_devnode_type) dlsym(hid_wrapper_handle, "udev_device_get_devnode"); + hid_wrapper_udev_device_get_parent_with_subsystem_devtype = (hid_wrapper_udev_device_get_parent_with_subsystem_devtype_type) dlsym(hid_wrapper_handle, "udev_device_get_parent_with_subsystem_devtype"); + hid_wrapper_udev_device_get_sysattr_value = (hid_wrapper_udev_device_get_sysattr_value_type) dlsym(hid_wrapper_handle, "udev_device_get_sysattr_value"); + hid_wrapper_udev_device_new_from_devnum = (hid_wrapper_udev_device_new_from_devnum_type) dlsym(hid_wrapper_handle, "udev_device_new_from_devnum"); + hid_wrapper_udev_device_new_from_syspath = (hid_wrapper_udev_device_new_from_syspath_type) dlsym(hid_wrapper_handle, "udev_device_new_from_syspath"); + hid_wrapper_udev_device_unref = (hid_wrapper_udev_device_unref_type) dlsym(hid_wrapper_handle, "udev_device_unref"); + hid_wrapper_udev_enumerate_add_match_subsystem = (hid_wrapper_udev_enumerate_add_match_subsystem_type) dlsym(hid_wrapper_handle, "udev_enumerate_add_match_subsystem"); + hid_wrapper_udev_enumerate_get_list_entry = (hid_wrapper_udev_enumerate_get_list_entry_type) dlsym(hid_wrapper_handle, "udev_enumerate_get_list_entry"); + hid_wrapper_udev_enumerate_new = (hid_wrapper_udev_enumerate_new_type) dlsym(hid_wrapper_handle, "udev_enumerate_new"); + hid_wrapper_udev_enumerate_scan_devices = (hid_wrapper_udev_enumerate_scan_devices_type) dlsym(hid_wrapper_handle, "udev_enumerate_scan_devices"); + hid_wrapper_udev_enumerate_unref = (hid_wrapper_udev_enumerate_unref_type) dlsym(hid_wrapper_handle, "udev_enumerate_unref"); + hid_wrapper_udev_list_entry_get_name = (hid_wrapper_udev_list_entry_get_name_type) dlsym(hid_wrapper_handle, "udev_list_entry_get_name"); + hid_wrapper_udev_list_entry_get_next = (hid_wrapper_udev_list_entry_get_next_type) dlsym(hid_wrapper_handle, "udev_list_entry_get_next"); + hid_wrapper_udev_new = (hid_wrapper_udev_new_type) dlsym(hid_wrapper_handle, "udev_new"); + hid_wrapper_udev_unref = (hid_wrapper_udev_unref_type) dlsym(hid_wrapper_handle, "udev_unref"); + + // Were all the funcions resolved? + if (hid_wrapper_handle == NULL || + hid_wrapper_udev_device_get_devnode == NULL || + hid_wrapper_udev_device_get_parent_with_subsystem_devtype == NULL || + hid_wrapper_udev_device_get_sysattr_value == NULL || + hid_wrapper_udev_device_new_from_devnum == NULL || + hid_wrapper_udev_device_new_from_syspath == NULL || + hid_wrapper_udev_device_unref == NULL || + hid_wrapper_udev_enumerate_add_match_subsystem == NULL || + hid_wrapper_udev_enumerate_get_list_entry == NULL || + hid_wrapper_udev_enumerate_new == NULL || + hid_wrapper_udev_enumerate_scan_devices == NULL || + hid_wrapper_udev_enumerate_unref == NULL || + hid_wrapper_udev_list_entry_get_name == NULL || + hid_wrapper_udev_list_entry_get_next == NULL || + hid_wrapper_udev_new == NULL || + hid_wrapper_udev_unref == NULL) + { + // Error, close the shared library handle and finish. + hid_wrapper_udev_close(); + return -1; + } + + // Success. + return 0; +} + +/* Definitions from linux/hidraw.h. Since these are new, some distros + may not have header files which contain them. */ +#ifndef HIDIOCSFEATURE +#define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len) +#endif +#ifndef HIDIOCGFEATURE +#define HIDIOCGFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x07, len) +#endif + + +/* USB HID device property names */ +const char *device_string_names[] = { + "manufacturer", + "product", + "serial", +}; + +/* Symbolic names for the properties above */ +enum device_string_id { + DEVICE_STRING_MANUFACTURER, + DEVICE_STRING_PRODUCT, + DEVICE_STRING_SERIAL, + + DEVICE_STRING_COUNT, +}; + +struct hid_device_ { + int device_handle; + int blocking; + int uses_numbered_reports; +}; + + +static __u32 kernel_version = 0; + +static __u32 detect_kernel_version(void) +{ + struct utsname name; + int major, minor, release; + int ret; + + uname(&name); + ret = sscanf(name.release, "%d.%d.%d", &major, &minor, &release); + if (ret == 3) { + return KERNEL_VERSION(major, minor, release); + } + + ret = sscanf(name.release, "%d.%d", &major, &minor); + if (ret == 2) { + return KERNEL_VERSION(major, minor, 0); + } + + printf("Couldn't determine kernel version from version string \"%s\"\n", name.release); + return 0; +} + +static hid_device *new_hid_device(void) +{ + hid_device *dev = calloc(1, sizeof(hid_device)); + dev->device_handle = -1; + dev->blocking = 1; + dev->uses_numbered_reports = 0; + + return dev; +} + + +/* The caller must free the returned string with free(). */ +static wchar_t *utf8_to_wchar_t(const char *utf8) +{ + wchar_t *ret = NULL; + + if (utf8) { + size_t wlen = mbstowcs(NULL, utf8, 0); + if ((size_t) -1 == wlen) { + return wcsdup(L""); + } + ret = calloc(wlen+1, sizeof(wchar_t)); + mbstowcs(ret, utf8, wlen+1); + ret[wlen] = 0x0000; + } + + return ret; +} + +/* Get an attribute value from a udev_device and return it as a whar_t + string. The returned string must be freed with free() when done.*/ +static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name) +{ + return utf8_to_wchar_t(hid_wrapper_udev_device_get_sysattr_value(dev, udev_name)); +} + +/* uses_numbered_reports() returns 1 if report_descriptor describes a device + which contains numbered reports. */ +static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) { + unsigned int i = 0; + int size_code; + int data_len, key_size; + + while (i < size) { + int key = report_descriptor[i]; + + /* Check for the Report ID key */ + if (key == 0x85/*Report ID*/) { + /* This device has a Report ID, which means it uses + numbered reports. */ + return 1; + } + + //printf("key: %02hhx\n", key); + + if ((key & 0xf0) == 0xf0) { + /* This is a Long Item. The next byte contains the + length of the data section (value) for this key. + See the HID specification, version 1.11, section + 6.2.2.3, titled "Long Items." */ + if (i+1 < size) + data_len = report_descriptor[i+1]; + else + data_len = 0; /* malformed report */ + key_size = 3; + } + else { + /* This is a Short Item. The bottom two bits of the + key contain the size code for the data section + (value) for this key. Refer to the HID + specification, version 1.11, section 6.2.2.2, + titled "Short Items." */ + size_code = key & 0x3; + switch (size_code) { + case 0: + case 1: + case 2: + data_len = size_code; + break; + case 3: + data_len = 4; + break; + default: + /* Can't ever happen since size_code is & 0x3 */ + data_len = 0; + break; + }; + key_size = 1; + } + + /* Skip over this key and it's associated data */ + i += data_len + key_size; + } + + /* Didn't find a Report ID key. Device doesn't use numbered reports. */ + return 0; +} + +/* + * The caller is responsible for free()ing the (newly-allocated) character + * strings pointed to by serial_number_utf8 and product_name_utf8 after use. + */ +static int +parse_uevent_info(const char *uevent, int *bus_type, + unsigned short *vendor_id, unsigned short *product_id, + char **serial_number_utf8, char **product_name_utf8) +{ + char *tmp = strdup(uevent); + char *saveptr = NULL; + char *line; + char *key; + char *value; + + int found_id = 0; + int found_serial = 0; + int found_name = 0; + + line = strtok_r(tmp, "\n", &saveptr); + while (line != NULL) { + /* line: "KEY=value" */ + key = line; + value = strchr(line, '='); + if (!value) { + goto next_line; + } + *value = '\0'; + value++; + + if (strcmp(key, "HID_ID") == 0) { + /** + * type vendor product + * HID_ID=0003:000005AC:00008242 + **/ + int ret = sscanf(value, "%x:%hx:%hx", bus_type, vendor_id, product_id); + if (ret == 3) { + found_id = 1; + } + } else if (strcmp(key, "HID_NAME") == 0) { + /* The caller has to free the product name */ + *product_name_utf8 = strdup(value); + found_name = 1; + } else if (strcmp(key, "HID_UNIQ") == 0) { + /* The caller has to free the serial number */ + *serial_number_utf8 = strdup(value); + found_serial = 1; + } + +next_line: + line = strtok_r(NULL, "\n", &saveptr); + } + + free(tmp); + return (found_id && found_name && found_serial); +} + + +static int get_device_string(hid_device *dev, enum device_string_id key, wchar_t *string, size_t maxlen) +{ + struct udev *udev; + struct udev_device *udev_dev, *parent, *hid_dev; + struct stat s; + int ret = -1; + char *serial_number_utf8 = NULL; + char *product_name_utf8 = NULL; + + /* Create the udev object */ + udev = hid_wrapper_udev_new(); + if (!udev) { + printf("Can't create udev\n"); + return -1; + } + + /* Get the dev_t (major/minor numbers) from the file handle. */ + ret = fstat(dev->device_handle, &s); + if (-1 == ret) + return ret; + /* Open a udev device from the dev_t. 'c' means character device. */ + udev_dev = hid_wrapper_udev_device_new_from_devnum(udev, 'c', s.st_rdev); + if (udev_dev) { + hid_dev = hid_wrapper_udev_device_get_parent_with_subsystem_devtype( + udev_dev, + "hid", + NULL); + if (hid_dev) { + unsigned short dev_vid; + unsigned short dev_pid; + int bus_type; + size_t retm; + + ret = parse_uevent_info( + hid_wrapper_udev_device_get_sysattr_value(hid_dev, "uevent"), + &bus_type, + &dev_vid, + &dev_pid, + &serial_number_utf8, + &product_name_utf8); + + if (bus_type == BUS_BLUETOOTH) { + switch (key) { + case DEVICE_STRING_MANUFACTURER: + wcsncpy(string, L"", maxlen); + ret = 0; + break; + case DEVICE_STRING_PRODUCT: + retm = mbstowcs(string, product_name_utf8, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + break; + case DEVICE_STRING_SERIAL: + retm = mbstowcs(string, serial_number_utf8, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + break; + case DEVICE_STRING_COUNT: + default: + ret = -1; + break; + } + } + else { + /* This is a USB device. Find its parent USB Device node. */ + parent = hid_wrapper_udev_device_get_parent_with_subsystem_devtype( + udev_dev, + "usb", + "usb_device"); + if (parent) { + const char *str; + const char *key_str = NULL; + + if (key >= 0 && key < DEVICE_STRING_COUNT) { + key_str = device_string_names[key]; + } else { + ret = -1; + goto end; + } + + str = hid_wrapper_udev_device_get_sysattr_value(parent, key_str); + if (str) { + /* Convert the string from UTF-8 to wchar_t */ + retm = mbstowcs(string, str, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + goto end; + } + } + } + } + } + +end: + free(serial_number_utf8); + free(product_name_utf8); + + hid_wrapper_udev_device_unref(udev_dev); + /* parent and hid_dev don't need to be (and can't be) unref'd. + I'm not sure why, but they'll throw double-free() errors. */ + hid_wrapper_udev_unref(udev); + + return ret; +} + +int HID_API_EXPORT hid_init(void) +{ + const char *locale; + + /* Set the locale if it's not set. */ + locale = setlocale(LC_CTYPE, NULL); + if (!locale) + setlocale(LC_CTYPE, ""); + + kernel_version = detect_kernel_version(); + + return hid_wrapper_udev_init(); +} + +int HID_API_EXPORT hid_exit(void) +{ + /* Nothing to do for this in the Linux/hidraw implementation. */ + hid_wrapper_udev_close(); + return 0; +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + struct udev *udev; + struct udev_enumerate *enumerate; + struct udev_list_entry *devices, *dev_list_entry; + + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + struct hid_device_info *prev_dev = NULL; /* previous device */ + + hid_init(); + + /* Create the udev object */ + udev = hid_wrapper_udev_new(); + if (!udev) { + printf("Can't create udev\n"); + return NULL; + } + + /* Create a list of the devices in the 'hidraw' subsystem. */ + enumerate = hid_wrapper_udev_enumerate_new(udev); + hid_wrapper_udev_enumerate_add_match_subsystem(enumerate, "hidraw"); + hid_wrapper_udev_enumerate_scan_devices(enumerate); + devices = hid_wrapper_udev_enumerate_get_list_entry(enumerate); + /* For each item, see if it matches the vid/pid, and if so + create a udev_device record for it */ + for (dev_list_entry = devices; dev_list_entry; dev_list_entry = hid_wrapper_udev_list_entry_get_next(dev_list_entry)) { + const char *sysfs_path; + const char *dev_path; + const char *str; + struct udev_device *raw_dev; /* The device's hidraw udev node. */ + struct udev_device *hid_dev; /* The device's HID udev node. */ + struct udev_device *usb_dev; /* The device's USB udev node. */ + struct udev_device *intf_dev; /* The device's interface (in the USB sense). */ + unsigned short dev_vid; + unsigned short dev_pid; + char *serial_number_utf8 = NULL; + char *product_name_utf8 = NULL; + int bus_type; + int result; + + /* Get the filename of the /sys entry for the device + and create a udev_device object (dev) representing it */ + sysfs_path = hid_wrapper_udev_list_entry_get_name(dev_list_entry); + raw_dev = hid_wrapper_udev_device_new_from_syspath(udev, sysfs_path); + dev_path = hid_wrapper_udev_device_get_devnode(raw_dev); + + hid_dev = hid_wrapper_udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "hid", + NULL); + + if (!hid_dev) { + /* Unable to find parent hid device. */ + goto next; + } + + result = parse_uevent_info( + hid_wrapper_udev_device_get_sysattr_value(hid_dev, "uevent"), + &bus_type, + &dev_vid, + &dev_pid, + &serial_number_utf8, + &product_name_utf8); + + if (!result) { + /* parse_uevent_info() failed for at least one field. */ + goto next; + } + + if (bus_type != BUS_USB && bus_type != BUS_BLUETOOTH) { + /* We only know how to handle USB and BT devices. */ + goto next; + } + + /* Check the VID/PID against the arguments */ + if ((vendor_id == 0x0 || vendor_id == dev_vid) && + (product_id == 0x0 || product_id == dev_pid)) { + struct hid_device_info *tmp; + + /* VID/PID match. Create the record. */ + tmp = malloc(sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + prev_dev = cur_dev; + cur_dev = tmp; + + /* Fill out the record */ + cur_dev->next = NULL; + cur_dev->path = dev_path? strdup(dev_path): NULL; + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Serial Number */ + cur_dev->serial_number = utf8_to_wchar_t(serial_number_utf8); + + /* Release Number */ + cur_dev->release_number = 0x0; + + /* Interface Number */ + cur_dev->interface_number = -1; + + switch (bus_type) { + case BUS_USB: + /* The device pointed to by raw_dev contains information about + the hidraw device. In order to get information about the + USB device, get the parent device with the + subsystem/devtype pair of "usb"/"usb_device". This will + be several levels up the tree, but the function will find + it. */ + usb_dev = hid_wrapper_udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_device"); + + if (!usb_dev) { + /* Free this device */ + free(cur_dev->serial_number); + free(cur_dev->path); + free(cur_dev); + + /* Take it off the device list. */ + if (prev_dev) { + prev_dev->next = NULL; + cur_dev = prev_dev; + } + else { + cur_dev = root = NULL; + } + + goto next; + } + + /* Manufacturer and Product strings */ + cur_dev->manufacturer_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_MANUFACTURER]); + cur_dev->product_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_PRODUCT]); + + /* Release Number */ + str = hid_wrapper_udev_device_get_sysattr_value(usb_dev, "bcdDevice"); + cur_dev->release_number = (str)? strtol(str, NULL, 16): 0x0; + + /* Get a handle to the interface's udev node. */ + intf_dev = hid_wrapper_udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_interface"); + if (intf_dev) { + str = hid_wrapper_udev_device_get_sysattr_value(intf_dev, "bInterfaceNumber"); + cur_dev->interface_number = (str)? strtol(str, NULL, 16): -1; + } + + break; + + case BUS_BLUETOOTH: + /* Manufacturer and Product strings */ + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + + break; + + default: + /* Unknown device type - this should never happen, as we + * check for USB and Bluetooth devices above */ + break; + } + } + + next: + free(serial_number_utf8); + free(product_name_utf8); + hid_wrapper_udev_device_unref(raw_dev); + /* hid_dev, usb_dev and intf_dev don't need to be (and can't be) + unref()d. It will cause a double-free() error. I'm not + sure why. */ + } + /* Free the enumerator and udev objects. */ + hid_wrapper_udev_enumerate_unref(enumerate); + hid_wrapper_udev_unref(udev); + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + +hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + + hid_init(); + + dev = new_hid_device(); + + /* OPEN HERE */ + dev->device_handle = open(path, O_RDWR); + + /* If we have a good handle, return it. */ + if (dev->device_handle > 0) { + + /* Get the report descriptor */ + int res, desc_size = 0; + struct hidraw_report_descriptor rpt_desc; + + memset(&rpt_desc, 0x0, sizeof(rpt_desc)); + + /* Get Report Descriptor Size */ + res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size); + if (res < 0) + perror("HIDIOCGRDESCSIZE"); + + + /* Get Report Descriptor */ + rpt_desc.size = desc_size; + res = ioctl(dev->device_handle, HIDIOCGRDESC, &rpt_desc); + if (res < 0) { + perror("HIDIOCGRDESC"); + } else { + /* Determine if this device uses numbered reports. */ + dev->uses_numbered_reports = + uses_numbered_reports(rpt_desc.value, + rpt_desc.size); + } + + return dev; + } + else { + /* Unable to open any devices. */ + free(dev); + return NULL; + } +} + + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + int bytes_written; + + bytes_written = write(dev->device_handle, data, length); + + return bytes_written; +} + + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + int bytes_read; + + if (milliseconds >= 0) { + /* Milliseconds is either 0 (non-blocking) or > 0 (contains + a valid timeout). In both cases we want to call poll() + and wait for data to arrive. Don't rely on non-blocking + operation (O_NONBLOCK) since some kernels don't seem to + properly report device disconnection through read() when + in non-blocking mode. */ + int ret; + struct pollfd fds; + + fds.fd = dev->device_handle; + fds.events = POLLIN; + fds.revents = 0; + ret = poll(&fds, 1, milliseconds); + if (ret == -1 || ret == 0) { + /* Error or timeout */ + return ret; + } + else { + /* Check for errors on the file descriptor. This will + indicate a device disconnection. */ + if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) + return -1; + } + } + + bytes_read = read(dev->device_handle, data, length); + if (bytes_read < 0 && (errno == EAGAIN || errno == EINPROGRESS)) + bytes_read = 0; + + if (bytes_read >= 0 && + kernel_version != 0 && + kernel_version < KERNEL_VERSION(2,6,34) && + dev->uses_numbered_reports) { + /* Work around a kernel bug. Chop off the first byte. */ + memmove(data, data+1, bytes_read); + bytes_read--; + } + + return bytes_read; +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* Do all non-blocking in userspace using poll(), since it looks + like there's a bug in the kernel in some versions where + read() will not return -1 on disconnection of the USB device */ + + dev->blocking = !nonblock; + return 0; /* Success */ +} + + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + int res; + + res = ioctl(dev->device_handle, HIDIOCSFEATURE(length), data); + if (res < 0) + perror("ioctl (SFEATURE)"); + + return res; +} + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + int res; + + res = ioctl(dev->device_handle, HIDIOCGFEATURE(length), data); + if (res < 0) + perror("ioctl (GFEATURE)"); + + + return res; +} + + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + close(dev->device_handle); + free(dev); +} + + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_MANUFACTURER, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_PRODUCT, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_SERIAL, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + return -1; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return NULL; +} diff --git a/src/hidapi/mac/hid.c b/src/hidapi/mac/hid.c new file mode 100644 index 000000000..ca10a9cca --- /dev/null +++ b/src/hidapi/mac/hid.c @@ -0,0 +1,1121 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 2010-07-03 + + Copyright 2010, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/* See Apple Technical Note TN2187 for details on IOHidManager. */ + +#include <IOKit/hid/IOHIDManager.h> +#include <IOKit/hid/IOHIDKeys.h> +#include <IOKit/IOKitLib.h> +#include <IOKit/usb/USBSpec.h> +#include <CoreFoundation/CoreFoundation.h> +#include <wchar.h> +#include <locale.h> +#include <pthread.h> +#include <sys/time.h> +#include <unistd.h> +#include <dlfcn.h> + +#include "hidapi.h" + +/* Barrier implementation because Mac OSX doesn't have pthread_barrier. + It also doesn't have clock_gettime(). So much for POSIX and SUSv2. + This implementation came from Brent Priddy and was posted on + StackOverflow. It is used with his permission. */ +typedef int pthread_barrierattr_t; +typedef struct pthread_barrier { + pthread_mutex_t mutex; + pthread_cond_t cond; + int count; + int trip_count; +} pthread_barrier_t; + +static int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count) +{ + if(count == 0) { + errno = EINVAL; + return -1; + } + + if(pthread_mutex_init(&barrier->mutex, 0) < 0) { + return -1; + } + if(pthread_cond_init(&barrier->cond, 0) < 0) { + pthread_mutex_destroy(&barrier->mutex); + return -1; + } + barrier->trip_count = count; + barrier->count = 0; + + return 0; +} + +static int pthread_barrier_destroy(pthread_barrier_t *barrier) +{ + pthread_cond_destroy(&barrier->cond); + pthread_mutex_destroy(&barrier->mutex); + return 0; +} + +static int pthread_barrier_wait(pthread_barrier_t *barrier) +{ + pthread_mutex_lock(&barrier->mutex); + ++(barrier->count); + if(barrier->count >= barrier->trip_count) + { + barrier->count = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + return 1; + } + else + { + pthread_cond_wait(&barrier->cond, &(barrier->mutex)); + pthread_mutex_unlock(&barrier->mutex); + return 0; + } +} + +static int return_data(hid_device *dev, unsigned char *data, size_t length); + +/* Linked List of input reports received from the device. */ +struct input_report { + uint8_t *data; + size_t len; + struct input_report *next; +}; + +struct hid_device_ { + IOHIDDeviceRef device_handle; + int blocking; + int uses_numbered_reports; + int disconnected; + CFStringRef run_loop_mode; + CFRunLoopRef run_loop; + CFRunLoopSourceRef source; + uint8_t *input_report_buf; + CFIndex max_input_report_len; + struct input_report *input_reports; + + pthread_t thread; + pthread_mutex_t mutex; /* Protects input_reports */ + pthread_cond_t condition; + pthread_barrier_t barrier; /* Ensures correct startup sequence */ + pthread_barrier_t shutdown_barrier; /* Ensures correct shutdown sequence */ + int shutdown_thread; +}; + +static hid_device *new_hid_device(void) +{ + hid_device *dev = calloc(1, sizeof(hid_device)); + dev->device_handle = NULL; + dev->blocking = 1; + dev->uses_numbered_reports = 0; + dev->disconnected = 0; + dev->run_loop_mode = NULL; + dev->run_loop = NULL; + dev->source = NULL; + dev->input_report_buf = NULL; + dev->input_reports = NULL; + dev->shutdown_thread = 0; + + /* Thread objects */ + pthread_mutex_init(&dev->mutex, NULL); + pthread_cond_init(&dev->condition, NULL); + pthread_barrier_init(&dev->barrier, NULL, 2); + pthread_barrier_init(&dev->shutdown_barrier, NULL, 2); + + return dev; +} + +static void free_hid_device(hid_device *dev) +{ + if (!dev) + return; + + /* Delete any input reports still left over. */ + struct input_report *rpt = dev->input_reports; + while (rpt) { + struct input_report *next = rpt->next; + free(rpt->data); + free(rpt); + rpt = next; + } + + /* Free the string and the report buffer. The check for NULL + is necessary here as CFRelease() doesn't handle NULL like + free() and others do. */ + if (dev->run_loop_mode) + CFRelease(dev->run_loop_mode); + if (dev->source) + CFRelease(dev->source); + free(dev->input_report_buf); + + /* Clean up the thread objects */ + pthread_barrier_destroy(&dev->shutdown_barrier); + pthread_barrier_destroy(&dev->barrier); + pthread_cond_destroy(&dev->condition); + pthread_mutex_destroy(&dev->mutex); + + /* Free the structure itself. */ + free(dev); +} + +static IOHIDManagerRef hid_mgr = 0x0; + + +#if 0 +static void register_error(hid_device *dev, const char *op) +{ + +} +#endif + + +static int32_t get_int_property(IOHIDDeviceRef device, CFStringRef key) +{ + CFTypeRef ref; + int32_t value; + + ref = IOHIDDeviceGetProperty(device, key); + if (ref) { + if (CFGetTypeID(ref) == CFNumberGetTypeID()) { + CFNumberGetValue((CFNumberRef) ref, kCFNumberSInt32Type, &value); + return value; + } + } + return 0; +} + +static unsigned short get_vendor_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDVendorIDKey)); +} + +static unsigned short get_product_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDProductIDKey)); +} + +static int32_t get_max_report_length(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDMaxInputReportSizeKey)); +} + +static int get_string_property(IOHIDDeviceRef device, CFStringRef prop, wchar_t *buf, size_t len) +{ + CFStringRef str; + + if (!len) + return 0; + + str = IOHIDDeviceGetProperty(device, prop); + + buf[0] = 0; + + if (str) { + CFIndex str_len = CFStringGetLength(str); + CFRange range; + CFIndex used_buf_len; + CFIndex chars_copied; + + len --; + + range.location = 0; + range.length = ((size_t)str_len > len)? len: (size_t)str_len; + chars_copied = CFStringGetBytes(str, + range, + kCFStringEncodingUTF32LE, + (char)'?', + FALSE, + (UInt8*)buf, + len * sizeof(wchar_t), + &used_buf_len); + + if (chars_copied == len) + buf[len] = 0; /* len is decremented above */ + else + buf[chars_copied] = 0; + + return 0; + } + else + return -1; + +} + +static int get_serial_number(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDSerialNumberKey), buf, len); +} + +static int get_manufacturer_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDManufacturerKey), buf, len); +} + +static int get_product_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDProductKey), buf, len); +} + + +/* Implementation of wcsdup() for Mac. */ +static wchar_t *dup_wcs(const wchar_t *s) +{ + size_t len = wcslen(s); + wchar_t *ret = malloc((len+1)*sizeof(wchar_t)); + wcscpy(ret, s); + + return ret; +} + +/* hidapi_IOHIDDeviceGetService() + * + * Return the io_service_t corresponding to a given IOHIDDeviceRef, either by: + * - on OS X 10.6 and above, calling IOHIDDeviceGetService() + * - on OS X 10.5, extract it from the IOHIDDevice struct + */ +static io_service_t hidapi_IOHIDDeviceGetService(IOHIDDeviceRef device) +{ + static void *iokit_framework = NULL; + static io_service_t (*dynamic_IOHIDDeviceGetService)(IOHIDDeviceRef device) = NULL; + + /* Use dlopen()/dlsym() to get a pointer to IOHIDDeviceGetService() if it exists. + * If any of these steps fail, dynamic_IOHIDDeviceGetService will be left NULL + * and the fallback method will be used. + */ + if (iokit_framework == NULL) { + iokit_framework = dlopen("/System/Library/IOKit.framework/IOKit", RTLD_LAZY); + + if (iokit_framework != NULL) + dynamic_IOHIDDeviceGetService = dlsym(iokit_framework, "IOHIDDeviceGetService"); + } + + if (dynamic_IOHIDDeviceGetService != NULL) { + /* Running on OS X 10.6 and above: IOHIDDeviceGetService() exists */ + return dynamic_IOHIDDeviceGetService(device); + } + else + { + /* Running on OS X 10.5: IOHIDDeviceGetService() doesn't exist. + * + * Be naughty and pull the service out of the IOHIDDevice. + * IOHIDDevice is an opaque struct not exposed to applications, but its + * layout is stable through all available versions of OS X. + * Tested and working on OS X 10.5.8 i386, x86_64, and ppc. + */ + struct IOHIDDevice_internal { + /* The first field of the IOHIDDevice struct is a + * CFRuntimeBase (which is a private CF struct). + * + * a, b, and c are the 3 fields that make up a CFRuntimeBase. + * See http://opensource.apple.com/source/CF/CF-476.18/CFRuntime.h + * + * The second field of the IOHIDDevice is the io_service_t we're looking for. + */ + uintptr_t a; + uint8_t b[4]; +#if __LP64__ + uint32_t c; +#endif + io_service_t service; + }; + struct IOHIDDevice_internal *tmp = (struct IOHIDDevice_internal *)device; + + return tmp->service; + } +} + +/* Initialize the IOHIDManager. Return 0 for success and -1 for failure. */ +static int init_hid_manager(void) +{ + /* Initialize all the HID Manager Objects */ + hid_mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (hid_mgr) { + IOHIDManagerSetDeviceMatching(hid_mgr, NULL); + IOHIDManagerScheduleWithRunLoop(hid_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + return 0; + } + + return -1; +} + +/* Initialize the IOHIDManager if necessary. This is the public function, and + it is safe to call this function repeatedly. Return 0 for success and -1 + for failure. */ +int HID_API_EXPORT hid_init(void) +{ + if (!hid_mgr) { + return init_hid_manager(); + } + + /* Already initialized. */ + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ + if (hid_mgr) { + /* Close the HID manager. */ + IOHIDManagerClose(hid_mgr, kIOHIDOptionsTypeNone); + CFRelease(hid_mgr); + hid_mgr = NULL; + } + + return 0; +} + +static void process_pending_events(void) { + SInt32 res; + do { + res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.001, FALSE); + } while(res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + CFIndex num_devices; + int i; + + /* Set up the HID Manager if it hasn't been done */ + if (hid_init() < 0) + return NULL; + + /* give the IOHIDManager a chance to update itself */ + process_pending_events(); + + /* Get a list of the Devices */ + IOHIDManagerSetDeviceMatching(hid_mgr, NULL); + CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr); + + /* Convert the list into a C array so we can iterate easily. */ + num_devices = CFSetGetCount(device_set); + IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); + CFSetGetValues(device_set, (const void **) device_array); + + /* Iterate over each device, making an entry for it. */ + for (i = 0; i < num_devices; i++) { + unsigned short dev_vid; + unsigned short dev_pid; + #define BUF_LEN 256 + wchar_t buf[BUF_LEN]; + + IOHIDDeviceRef dev = device_array[i]; + + if (!dev) { + continue; + } + dev_vid = get_vendor_id(dev); + dev_pid = get_product_id(dev); + + /* Check the VID/PID against the arguments */ + if ((vendor_id == 0x0 || vendor_id == dev_vid) && + (product_id == 0x0 || product_id == dev_pid)) { + struct hid_device_info *tmp; + bool is_usb_hid; /* Is this an actual HID usb device */ + io_object_t iokit_dev; + kern_return_t res; + io_string_t path; + + /* VID/PID match. Create the record. */ + tmp = malloc(sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + is_usb_hid = get_int_property(dev, CFSTR(kUSBInterfaceClass)) == kUSBHIDClass; + + /* Get the Usage Page and Usage for this device. */ + cur_dev->usage_page = get_int_property(dev, CFSTR(kIOHIDPrimaryUsagePageKey)); + cur_dev->usage = get_int_property(dev, CFSTR(kIOHIDPrimaryUsageKey)); + + /* Fill out the record */ + cur_dev->next = NULL; + + /* Fill in the path (IOService plane) */ + iokit_dev = hidapi_IOHIDDeviceGetService(dev); + res = IORegistryEntryGetPath(iokit_dev, kIOServicePlane, path); + if (res == KERN_SUCCESS) + cur_dev->path = strdup(path); + else + cur_dev->path = strdup(""); + + /* Serial Number */ + get_serial_number(dev, buf, BUF_LEN); + cur_dev->serial_number = dup_wcs(buf); + + /* Manufacturer and Product strings */ + get_manufacturer_string(dev, buf, BUF_LEN); + cur_dev->manufacturer_string = dup_wcs(buf); + get_product_string(dev, buf, BUF_LEN); + cur_dev->product_string = dup_wcs(buf); + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Release Number */ + cur_dev->release_number = get_int_property(dev, CFSTR(kIOHIDVersionNumberKey)); + + /* We can only retrieve the interface number for USB HID devices. + * IOKit always seems to return 0 when querying a standard USB device + * for its interface. */ + if (is_usb_hid) { + /* Get the interface number */ + cur_dev->interface_number = get_int_property(dev, CFSTR(kUSBInterfaceNumber)); + } else { + cur_dev->interface_number = -1; + } + } + } + + free(device_array); + CFRelease(device_set); + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + /* This function is identical to the Linux version. Platform independent. */ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + +hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + /* This function is identical to the Linux version. Platform independent. */ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device * handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +static void hid_device_removal_callback(void *context, IOReturn result, + void *sender) +{ + /* Stop the Run Loop for this device. */ + hid_device *d = context; + + d->disconnected = 1; + CFRunLoopStop(d->run_loop); +} + +/* The Run Loop calls this function for each input report received. + This function puts the data into a linked list to be picked up by + hid_read(). */ +static void hid_report_callback(void *context, IOReturn result, void *sender, + IOHIDReportType report_type, uint32_t report_id, + uint8_t *report, CFIndex report_length) +{ + struct input_report *rpt; + hid_device *dev = context; + + /* Make a new Input Report object */ + rpt = calloc(1, sizeof(struct input_report)); + rpt->data = calloc(1, report_length); + memcpy(rpt->data, report, report_length); + rpt->len = report_length; + rpt->next = NULL; + + /* Lock this section */ + pthread_mutex_lock(&dev->mutex); + + /* Attach the new report object to the end of the list. */ + if (dev->input_reports == NULL) { + /* The list is empty. Put it at the root. */ + dev->input_reports = rpt; + } + else { + /* Find the end of the list and attach. */ + struct input_report *cur = dev->input_reports; + int num_queued = 0; + while (cur->next != NULL) { + cur = cur->next; + num_queued++; + } + cur->next = rpt; + + /* Pop one off if we've reached 30 in the queue. This + way we don't grow forever if the user never reads + anything from the device. */ + if (num_queued > 30) { + return_data(dev, NULL, 0); + } + } + + /* Signal a waiting thread that there is data. */ + pthread_cond_signal(&dev->condition); + + /* Unlock */ + pthread_mutex_unlock(&dev->mutex); + +} + +/* This gets called when the read_thread's run loop gets signaled by + hid_close(), and serves to stop the read_thread's run loop. */ +static void perform_signal_callback(void *context) +{ + hid_device *dev = context; + CFRunLoopStop(dev->run_loop); /*TODO: CFRunLoopGetCurrent()*/ +} + +static void *read_thread(void *param) +{ + hid_device *dev = param; + SInt32 code; + + /* Move the device's run loop to this thread. */ + IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetCurrent(), dev->run_loop_mode); + + /* Create the RunLoopSource which is used to signal the + event loop to stop when hid_close() is called. */ + CFRunLoopSourceContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.version = 0; + ctx.info = dev; + ctx.perform = &perform_signal_callback; + dev->source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); + CFRunLoopAddSource(CFRunLoopGetCurrent(), dev->source, dev->run_loop_mode); + + /* Store off the Run Loop so it can be stopped from hid_close() + and on device disconnection. */ + dev->run_loop = CFRunLoopGetCurrent(); + + /* Notify the main thread that the read thread is up and running. */ + pthread_barrier_wait(&dev->barrier); + + /* Run the Event Loop. CFRunLoopRunInMode() will dispatch HID input + reports into the hid_report_callback(). */ + while (!dev->shutdown_thread && !dev->disconnected) { + code = CFRunLoopRunInMode(dev->run_loop_mode, 1000/*sec*/, FALSE); + /* Return if the device has been disconnected */ + if (code == kCFRunLoopRunFinished) { + dev->disconnected = 1; + break; + } + + + /* Break if The Run Loop returns Finished or Stopped. */ + if (code != kCFRunLoopRunTimedOut && + code != kCFRunLoopRunHandledSource) { + /* There was some kind of error. Setting + shutdown seems to make sense, but + there may be something else more appropriate */ + dev->shutdown_thread = 1; + break; + } + } + + /* Now that the read thread is stopping, Wake any threads which are + waiting on data (in hid_read_timeout()). Do this under a mutex to + make sure that a thread which is about to go to sleep waiting on + the condition actually will go to sleep before the condition is + signaled. */ + pthread_mutex_lock(&dev->mutex); + pthread_cond_broadcast(&dev->condition); + pthread_mutex_unlock(&dev->mutex); + + /* Wait here until hid_close() is called and makes it past + the call to CFRunLoopWakeUp(). This thread still needs to + be valid when that function is called on the other thread. */ + pthread_barrier_wait(&dev->shutdown_barrier); + + return NULL; +} + +/* hid_open_path() + * + * path must be a valid path to an IOHIDDevice in the IOService plane + * Example: "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/EHC1@1D,7/AppleUSBEHCI/PLAYSTATION(R)3 Controller@fd120000/IOUSBInterface@0/IOUSBHIDDriver" + */ +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + io_registry_entry_t entry = MACH_PORT_NULL; + + dev = new_hid_device(); + + /* Set up the HID Manager if it hasn't been done */ + if (hid_init() < 0) + return NULL; + + /* Get the IORegistry entry for the given path */ + entry = IORegistryEntryFromPath(kIOMasterPortDefault, path); + if (entry == MACH_PORT_NULL) { + /* Path wasn't valid (maybe device was removed?) */ + goto return_error; + } + + /* Create an IOHIDDevice for the entry */ + dev->device_handle = IOHIDDeviceCreate(kCFAllocatorDefault, entry); + if (dev->device_handle == NULL) { + /* Error creating the HID device */ + goto return_error; + } + + /* Open the IOHIDDevice */ + IOReturn ret = IOHIDDeviceOpen(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); + if (ret == kIOReturnSuccess) { + char str[32]; + + /* Create the buffers for receiving data */ + dev->max_input_report_len = (CFIndex) get_max_report_length(dev->device_handle); + dev->input_report_buf = calloc(dev->max_input_report_len, sizeof(uint8_t)); + + /* Create the Run Loop Mode for this device. + printing the reference seems to work. */ + sprintf(str, "HIDAPI_%p", dev->device_handle); + dev->run_loop_mode = + CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); + + /* Attach the device to a Run Loop */ + IOHIDDeviceRegisterInputReportCallback( + dev->device_handle, dev->input_report_buf, dev->max_input_report_len, + &hid_report_callback, dev); + IOHIDDeviceRegisterRemovalCallback(dev->device_handle, hid_device_removal_callback, dev); + + /* Start the read thread */ + pthread_create(&dev->thread, NULL, read_thread, dev); + + /* Wait here for the read thread to be initialized. */ + pthread_barrier_wait(&dev->barrier); + + IOObjectRelease(entry); + return dev; + } + else { + goto return_error; + } + +return_error: + if (dev->device_handle != NULL) + CFRelease(dev->device_handle); + + if (entry != MACH_PORT_NULL) + IOObjectRelease(entry); + + free_hid_device(dev); + return NULL; +} + +static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char *data, size_t length) +{ + const unsigned char *data_to_send; + size_t length_to_send; + IOReturn res; + + /* Return if the device has been disconnected. */ + if (dev->disconnected) + return -1; + + if (data[0] == 0x0) { + /* Not using numbered Reports. + Don't send the report number. */ + data_to_send = data+1; + length_to_send = length-1; + } + else { + /* Using numbered Reports. + Send the Report Number */ + data_to_send = data; + length_to_send = length; + } + + if (!dev->disconnected) { + res = IOHIDDeviceSetReport(dev->device_handle, + type, + data[0], /* Report ID*/ + data_to_send, length_to_send); + + if (res == kIOReturnSuccess) { + return length; + } + else + return -1; + } + + return -1; +} + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, kIOHIDReportTypeOutput, data, length); +} + +/* Helper function, so that this isn't duplicated in hid_read(). */ +static int return_data(hid_device *dev, unsigned char *data, size_t length) +{ + /* Copy the data out of the linked list item (rpt) into the + return buffer (data), and delete the liked list item. */ + struct input_report *rpt = dev->input_reports; + size_t len = (length < rpt->len)? length: rpt->len; + memcpy(data, rpt->data, len); + dev->input_reports = rpt->next; + free(rpt->data); + free(rpt); + return len; +} + +static int cond_wait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) +{ + while (!dev->input_reports) { + int res = pthread_cond_wait(cond, mutex); + if (res != 0) + return res; + + /* A res of 0 means we may have been signaled or it may + be a spurious wakeup. Check to see that there's acutally + data in the queue before returning, and if not, go back + to sleep. See the pthread_cond_timedwait() man page for + details. */ + + if (dev->shutdown_thread || dev->disconnected) + return -1; + } + + return 0; +} + +static int cond_timedwait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) +{ + while (!dev->input_reports) { + int res = pthread_cond_timedwait(cond, mutex, abstime); + if (res != 0) + return res; + + /* A res of 0 means we may have been signaled or it may + be a spurious wakeup. Check to see that there's acutally + data in the queue before returning, and if not, go back + to sleep. See the pthread_cond_timedwait() man page for + details. */ + + if (dev->shutdown_thread || dev->disconnected) + return -1; + } + + return 0; + +} + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + int bytes_read = -1; + + /* Lock the access to the report list. */ + pthread_mutex_lock(&dev->mutex); + + /* There's an input report queued up. Return it. */ + if (dev->input_reports) { + /* Return the first one */ + bytes_read = return_data(dev, data, length); + goto ret; + } + + /* Return if the device has been disconnected. */ + if (dev->disconnected) { + bytes_read = -1; + goto ret; + } + + if (dev->shutdown_thread) { + /* This means the device has been closed (or there + has been an error. An error code of -1 should + be returned. */ + bytes_read = -1; + goto ret; + } + + /* There is no data. Go to sleep and wait for data. */ + + if (milliseconds == -1) { + /* Blocking */ + int res; + res = cond_wait(dev, &dev->condition, &dev->mutex); + if (res == 0) + bytes_read = return_data(dev, data, length); + else { + /* There was an error, or a device disconnection. */ + bytes_read = -1; + } + } + else if (milliseconds > 0) { + /* Non-blocking, but called with timeout. */ + int res; + struct timespec ts; + struct timeval tv; + gettimeofday(&tv, NULL); + TIMEVAL_TO_TIMESPEC(&tv, &ts); + ts.tv_sec += milliseconds / 1000; + ts.tv_nsec += (milliseconds % 1000) * 1000000; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + + res = cond_timedwait(dev, &dev->condition, &dev->mutex, &ts); + if (res == 0) + bytes_read = return_data(dev, data, length); + else if (res == ETIMEDOUT) + bytes_read = 0; + else + bytes_read = -1; + } + else { + /* Purely non-blocking */ + bytes_read = 0; + } + +ret: + /* Unlock */ + pthread_mutex_unlock(&dev->mutex); + return bytes_read; +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* All Nonblocking operation is handled by the library. */ + dev->blocking = !nonblock; + + return 0; +} + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, kIOHIDReportTypeFeature, data, length); +} + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + CFIndex len = length - 1; + IOReturn res; + + /* Return if the device has been unplugged. */ + if (dev->disconnected) + return -1; + + res = IOHIDDeviceGetReport(dev->device_handle, + kIOHIDReportTypeFeature, + data[0], /* Report ID */ + data + 1, &len); + if (res == kIOReturnSuccess) + return len + 1; + else + return -1; +} + + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + + /* Disconnect the report callback before close. */ + if (!dev->disconnected) { + IOHIDDeviceRegisterInputReportCallback( + dev->device_handle, dev->input_report_buf, dev->max_input_report_len, + NULL, dev); + IOHIDDeviceRegisterRemovalCallback(dev->device_handle, NULL, dev); + IOHIDDeviceUnscheduleFromRunLoop(dev->device_handle, dev->run_loop, dev->run_loop_mode); + IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetMain(), kCFRunLoopDefaultMode); + } + + /* Cause read_thread() to stop. */ + dev->shutdown_thread = 1; + + /* Wake up the run thread's event loop so that the thread can exit. */ + CFRunLoopSourceSignal(dev->source); + CFRunLoopWakeUp(dev->run_loop); + + /* Notify the read thread that it can shut down now. */ + pthread_barrier_wait(&dev->shutdown_barrier); + + /* Wait for read_thread() to end. */ + pthread_join(dev->thread, NULL); + + /* Close the OS handle to the device, but only if it's not + been unplugged. If it's been unplugged, then calling + IOHIDDeviceClose() will crash. */ + if (!dev->disconnected) { + IOHIDDeviceClose(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); + } + + /* Clear out the queue of received reports. */ + pthread_mutex_lock(&dev->mutex); + while (dev->input_reports) { + return_data(dev, NULL, 0); + } + pthread_mutex_unlock(&dev->mutex); + CFRelease(dev->device_handle); + + free_hid_device(dev); +} + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_manufacturer_string(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_product_string(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_serial_number(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + /* TODO: */ + + return 0; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + /* TODO: */ + + return NULL; +} + + + + + + + +#if 0 +static int32_t get_location_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDLocationIDKey)); +} + +static int32_t get_usage(IOHIDDeviceRef device) +{ + int32_t res; + res = get_int_property(device, CFSTR(kIOHIDDeviceUsageKey)); + if (!res) + res = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); + return res; +} + +static int32_t get_usage_page(IOHIDDeviceRef device) +{ + int32_t res; + res = get_int_property(device, CFSTR(kIOHIDDeviceUsagePageKey)); + if (!res) + res = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); + return res; +} + +static int get_transport(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDTransportKey), buf, len); +} + + +int main(void) +{ + IOHIDManagerRef mgr; + int i; + + mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + IOHIDManagerSetDeviceMatching(mgr, NULL); + IOHIDManagerOpen(mgr, kIOHIDOptionsTypeNone); + + CFSetRef device_set = IOHIDManagerCopyDevices(mgr); + + CFIndex num_devices = CFSetGetCount(device_set); + IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); + CFSetGetValues(device_set, (const void **) device_array); + + for (i = 0; i < num_devices; i++) { + IOHIDDeviceRef dev = device_array[i]; + printf("Device: %p\n", dev); + printf(" %04hx %04hx\n", get_vendor_id(dev), get_product_id(dev)); + + wchar_t serial[256], buf[256]; + char cbuf[256]; + get_serial_number(dev, serial, 256); + + + printf(" Serial: %ls\n", serial); + printf(" Loc: %ld\n", get_location_id(dev)); + get_transport(dev, buf, 256); + printf(" Trans: %ls\n", buf); + make_path(dev, cbuf, 256); + printf(" Path: %s\n", cbuf); + + } + + return 0; +} +#endif diff --git a/src/hidapi/win/hid.c b/src/hidapi/win/hid.c new file mode 100644 index 000000000..4a71e2552 --- /dev/null +++ b/src/hidapi/win/hid.c @@ -0,0 +1,956 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +#include <windows.h> + +#ifndef _NTDEF_ +typedef LONG NTSTATUS; +#endif + +#ifdef __MINGW32__ +#include <ntdef.h> +#include <winbase.h> +#endif + +#ifdef __CYGWIN__ +#include <ntdef.h> +#define _wcsdup wcsdup +#endif + +/* The maximum number of characters that can be passed into the + HidD_Get*String() functions without it failing.*/ +#define MAX_STRING_WCHARS 0xFFF + +/*#define HIDAPI_USE_DDK*/ + +#ifdef __cplusplus +extern "C" { +#endif + #include <setupapi.h> + #include <winioctl.h> + #ifdef HIDAPI_USE_DDK + #include <hidsdi.h> + #endif + + /* Copied from inc/ddk/hidclass.h, part of the Windows DDK. */ + #define HID_OUT_CTL_CODE(id) \ + CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) + #define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include <stdio.h> +#include <stdlib.h> + + +#include "hidapi.h" + +#undef MIN +#define MIN(x,y) ((x) < (y)? (x): (y)) + +#ifdef _MSC_VER + /* Thanks Microsoft, but I know how to use strncpy(). */ + #pragma warning(disable:4996) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef HIDAPI_USE_DDK + /* Since we're not building with the DDK, and the HID header + files aren't part of the SDK, we have to define all this + stuff here. In lookup_functions(), the function pointers + defined below are set. */ + typedef struct _HIDD_ATTRIBUTES{ + ULONG Size; + USHORT VendorID; + USHORT ProductID; + USHORT VersionNumber; + } HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; + + typedef USHORT USAGE; + typedef struct _HIDP_CAPS { + USAGE Usage; + USAGE UsagePage; + USHORT InputReportByteLength; + USHORT OutputReportByteLength; + USHORT FeatureReportByteLength; + USHORT Reserved[17]; + USHORT fields_not_used_by_hidapi[10]; + } HIDP_CAPS, *PHIDP_CAPS; + typedef void* PHIDP_PREPARSED_DATA; + #define HIDP_STATUS_SUCCESS 0x110000 + + typedef BOOLEAN (__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); + typedef BOOLEAN (__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); + typedef BOOLEAN (__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); + typedef NTSTATUS (__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, HIDP_CAPS *caps); + typedef BOOLEAN (__stdcall *HidD_SetNumInputBuffers_)(HANDLE handle, ULONG number_buffers); + + static HidD_GetAttributes_ HidD_GetAttributes; + static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; + static HidD_GetManufacturerString_ HidD_GetManufacturerString; + static HidD_GetProductString_ HidD_GetProductString; + static HidD_SetFeature_ HidD_SetFeature; + static HidD_GetFeature_ HidD_GetFeature; + static HidD_GetIndexedString_ HidD_GetIndexedString; + static HidD_GetPreparsedData_ HidD_GetPreparsedData; + static HidD_FreePreparsedData_ HidD_FreePreparsedData; + static HidP_GetCaps_ HidP_GetCaps; + static HidD_SetNumInputBuffers_ HidD_SetNumInputBuffers; + + static HMODULE lib_handle = NULL; + static BOOLEAN initialized = FALSE; +#endif /* HIDAPI_USE_DDK */ + +struct hid_device_ { + HANDLE device_handle; + BOOL blocking; + USHORT output_report_length; + size_t input_report_length; + void *last_error_str; + DWORD last_error_num; + BOOL read_pending; + char *read_buf; + OVERLAPPED ol; +}; + +static hid_device *new_hid_device() +{ + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + dev->device_handle = INVALID_HANDLE_VALUE; + dev->blocking = TRUE; + dev->output_report_length = 0; + dev->input_report_length = 0; + dev->last_error_str = NULL; + dev->last_error_num = 0; + dev->read_pending = FALSE; + dev->read_buf = NULL; + memset(&dev->ol, 0, sizeof(dev->ol)); + dev->ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*initial state f=nonsignaled*/, NULL); + + return dev; +} + +static void free_hid_device(hid_device *dev) +{ + CloseHandle(dev->ol.hEvent); + CloseHandle(dev->device_handle); + LocalFree(dev->last_error_str); + free(dev->read_buf); + free(dev); +} + +static void register_error(hid_device *dev, const char *op) +{ + WCHAR *ptr, *msg; + + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPVOID)&msg, 0/*sz*/, + NULL); + + /* Get rid of the CR and LF that FormatMessage() sticks at the + end of the message. Thanks Microsoft! */ + ptr = msg; + while (*ptr) { + if (*ptr == '\r') { + *ptr = 0x0000; + break; + } + ptr++; + } + + /* Store the message off in the Device entry so that + the hid_error() function can pick it up. */ + LocalFree(dev->last_error_str); + dev->last_error_str = msg; +} + +#ifndef HIDAPI_USE_DDK +static int lookup_functions() +{ + lib_handle = LoadLibraryA("hid.dll"); + if (lib_handle) { +#define RESOLVE(x) x = (x##_)GetProcAddress(lib_handle, #x); if (!x) return -1; + RESOLVE(HidD_GetAttributes); + RESOLVE(HidD_GetSerialNumberString); + RESOLVE(HidD_GetManufacturerString); + RESOLVE(HidD_GetProductString); + RESOLVE(HidD_SetFeature); + RESOLVE(HidD_GetFeature); + RESOLVE(HidD_GetIndexedString); + RESOLVE(HidD_GetPreparsedData); + RESOLVE(HidD_FreePreparsedData); + RESOLVE(HidP_GetCaps); + RESOLVE(HidD_SetNumInputBuffers); +#undef RESOLVE + } + else + return -1; + + return 0; +} +#endif + +static HANDLE open_device(const char *path, BOOL open_rw) +{ + HANDLE handle; + DWORD desired_access = (open_rw)? (GENERIC_WRITE | GENERIC_READ): 0; + DWORD share_mode = FILE_SHARE_READ|FILE_SHARE_WRITE; + + handle = CreateFileA(path, + desired_access, + share_mode, + NULL, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED,/*FILE_ATTRIBUTE_NORMAL,*/ + 0); + + return handle; +} + +int HID_API_EXPORT hid_init(void) +{ +#ifndef HIDAPI_USE_DDK + if (!initialized) { + if (lookup_functions() < 0) { + hid_exit(); + return -1; + } + initialized = TRUE; + } +#endif + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ +#ifndef HIDAPI_USE_DDK + if (lib_handle) + FreeLibrary(lib_handle); + lib_handle = NULL; + initialized = FALSE; +#endif + return 0; +} + +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + BOOL res; + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + /* Windows objects for interacting with the driver. */ + GUID InterfaceClassGuid = {0x4d1e55b2, 0xf16f, 0x11cf, {0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30} }; + SP_DEVINFO_DATA devinfo_data; + SP_DEVICE_INTERFACE_DATA device_interface_data; + SP_DEVICE_INTERFACE_DETAIL_DATA_A *device_interface_detail_data = NULL; + HDEVINFO device_info_set = INVALID_HANDLE_VALUE; + int device_index = 0; + int i; + + if (hid_init() < 0) + return NULL; + + /* Initialize the Windows objects. */ + memset(&devinfo_data, 0x0, sizeof(devinfo_data)); + devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); + device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + /* Get information for all the devices belonging to the HID class. */ + device_info_set = SetupDiGetClassDevsA(&InterfaceClassGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + /* Iterate over each device in the HID class, looking for the right one. */ + + for (;;) { + HANDLE write_handle = INVALID_HANDLE_VALUE; + DWORD required_size = 0; + HIDD_ATTRIBUTES attrib; + + res = SetupDiEnumDeviceInterfaces(device_info_set, + NULL, + &InterfaceClassGuid, + device_index, + &device_interface_data); + + if (!res) { + /* A return of FALSE from this function means that + there are no more devices. */ + break; + } + + /* Call with 0-sized detail size, and let the function + tell us how long the detail struct needs to be. The + size is put in &required_size. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + NULL, + 0, + &required_size, + NULL); + + /* Allocate a long enough structure for device_interface_detail_data. */ + device_interface_detail_data = (SP_DEVICE_INTERFACE_DETAIL_DATA_A*) malloc(required_size); + device_interface_detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); + + /* Get the detailed data for this device. The detail data gives us + the device path for this device, which is then passed into + CreateFile() to get a handle to the device. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + device_interface_detail_data, + required_size, + NULL, + NULL); + + if (!res) { + /* register_error(dev, "Unable to call SetupDiGetDeviceInterfaceDetail"); + Continue to the next device. */ + goto cont; + } + + /* Make sure this device is of Setup Class "HIDClass" and has a + driver bound to it. */ + for (i = 0; ; i++) { + char driver_name[256]; + + /* Populate devinfo_data. This function will return failure + when there are no more interfaces left. */ + res = SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); + if (!res) + goto cont; + + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_CLASS, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (!res) + goto cont; + + if ((strcmp(driver_name, "HIDClass") == 0) || + (strcmp(driver_name, "Mouse") == 0) || + (strcmp(driver_name, "Keyboard") == 0)) { + /* See if there's a driver bound. */ + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_DRIVER, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (res) + break; + } + } + + //wprintf(L"HandleName: %s\n", device_interface_detail_data->DevicePath); + + /* Open a handle to the device */ + write_handle = open_device(device_interface_detail_data->DevicePath, FALSE); + + /* Check validity of write_handle. */ + if (write_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device. */ + //register_error(dev, "CreateFile"); + goto cont_close; + } + + + /* Get the Vendor ID and Product ID for this device. */ + attrib.Size = sizeof(HIDD_ATTRIBUTES); + HidD_GetAttributes(write_handle, &attrib); + //wprintf(L"Product/Vendor: %x %x\n", attrib.ProductID, attrib.VendorID); + + /* Check the VID/PID to see if we should add this + device to the enumeration list. */ + if ((vendor_id == 0x0 || attrib.VendorID == vendor_id) && + (product_id == 0x0 || attrib.ProductID == product_id)) { + + #define WSTR_LEN 512 + const char *str; + struct hid_device_info *tmp; + PHIDP_PREPARSED_DATA pp_data = NULL; + HIDP_CAPS caps; + BOOLEAN res; + NTSTATUS nt_res; + wchar_t wstr[WSTR_LEN]; /* TODO: Determine Size */ + size_t len; + + /* VID/PID match. Create the record. */ + tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* Get the Usage Page and Usage for this device. */ + res = HidD_GetPreparsedData(write_handle, &pp_data); + if (res) { + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res == HIDP_STATUS_SUCCESS) { + cur_dev->usage_page = caps.UsagePage; + cur_dev->usage = caps.Usage; + } + + HidD_FreePreparsedData(pp_data); + } + + /* Fill out the record */ + cur_dev->next = NULL; + str = device_interface_detail_data->DevicePath; + if (str) { + len = strlen(str); + cur_dev->path = (char*) calloc(len+1, sizeof(char)); + strncpy(cur_dev->path, str, len+1); + cur_dev->path[len] = '\0'; + } + else + cur_dev->path = NULL; + + /* Serial Number */ + res = HidD_GetSerialNumberString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->serial_number = _wcsdup(wstr); + } + + /* Manufacturer String */ + res = HidD_GetManufacturerString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->manufacturer_string = _wcsdup(wstr); + } + + /* Product String */ + res = HidD_GetProductString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->product_string = _wcsdup(wstr); + } + + /* VID/PID */ + cur_dev->vendor_id = attrib.VendorID; + cur_dev->product_id = attrib.ProductID; + + /* Release Number */ + cur_dev->release_number = attrib.VersionNumber; + + /* Interface Number. It can sometimes be parsed out of the path + on Windows if a device has multiple interfaces. See + http://msdn.microsoft.com/en-us/windows/hardware/gg487473 or + search for "Hardware IDs for HID Devices" at MSDN. If it's not + in the path, it's set to -1. */ + cur_dev->interface_number = -1; + if (cur_dev->path) { + char *interface_component = strstr(cur_dev->path, "&mi_"); + if (interface_component) { + char *hex_str = interface_component + 4; + char *endptr = NULL; + cur_dev->interface_number = strtol(hex_str, &endptr, 16); + if (endptr == hex_str) { + /* The parsing failed. Set interface_number to -1. */ + cur_dev->interface_number = -1; + } + } + } + } + +cont_close: + CloseHandle(write_handle); +cont: + /* We no longer need the detail data. It can be freed */ + free(device_interface_detail_data); + + device_index++; + + } + + /* Close the device information handle. */ + SetupDiDestroyDeviceInfoList(device_info_set); + + return root; + +} + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) +{ + /* TODO: Merge this with the Linux version. This function is platform-independent. */ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + + +HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + /* TODO: Merge this functions with the Linux version. This function should be platform independent. */ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) +{ + hid_device *dev; + HIDP_CAPS caps; + PHIDP_PREPARSED_DATA pp_data = NULL; + BOOLEAN res; + NTSTATUS nt_res; + + if (hid_init() < 0) { + return NULL; + } + + dev = new_hid_device(); + + /* Open a handle to the device */ + dev->device_handle = open_device(path, TRUE); + + /* Check validity of write_handle. */ + if (dev->device_handle == INVALID_HANDLE_VALUE) { + /* System devices, such as keyboards and mice, cannot be opened in + read-write mode, because the system takes exclusive control over + them. This is to prevent keyloggers. However, feature reports + can still be sent and received. Retry opening the device, but + without read/write access. */ + dev->device_handle = open_device(path, FALSE); + + /* Check the validity of the limited device_handle. */ + if (dev->device_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device, even without read-write mode. */ + register_error(dev, "CreateFile"); + goto err; + } + } + + /* Set the Input Report buffer size to 64 reports. */ + res = HidD_SetNumInputBuffers(dev->device_handle, 64); + if (!res) { + register_error(dev, "HidD_SetNumInputBuffers"); + goto err; + } + + /* Get the Input Report length for the device. */ + res = HidD_GetPreparsedData(dev->device_handle, &pp_data); + if (!res) { + register_error(dev, "HidD_GetPreparsedData"); + goto err; + } + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res != HIDP_STATUS_SUCCESS) { + register_error(dev, "HidP_GetCaps"); + goto err_pp_data; + } + dev->output_report_length = caps.OutputReportByteLength; + dev->input_report_length = caps.InputReportByteLength; + HidD_FreePreparsedData(pp_data); + + dev->read_buf = (char*) malloc(dev->input_report_length); + + return dev; + +err_pp_data: + HidD_FreePreparsedData(pp_data); +err: + free_hid_device(dev); + return NULL; +} + +int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + DWORD bytes_written; + BOOL res; + + OVERLAPPED ol; + unsigned char *buf; + memset(&ol, 0, sizeof(ol)); + + /* Make sure the right number of bytes are passed to WriteFile. Windows + expects the number of bytes which are in the _longest_ report (plus + one for the report number) bytes even if the data is a report + which is shorter than that. Windows gives us this value in + caps.OutputReportByteLength. If a user passes in fewer bytes than this, + create a temporary buffer which is the proper size. */ + if (length >= dev->output_report_length) { + /* The user passed the right number of bytes. Use the buffer as-is. */ + buf = (unsigned char *) data; + } else { + /* Create a temporary buffer and copy the user's data + into it, padding the rest with zeros. */ + buf = (unsigned char *) malloc(dev->output_report_length); + memcpy(buf, data, length); + memset(buf + length, 0, dev->output_report_length - length); + length = dev->output_report_length; + } + + res = WriteFile(dev->device_handle, buf, length, NULL, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* WriteFile() failed. Return error. */ + register_error(dev, "WriteFile"); + bytes_written = -1; + goto end_of_function; + } + } + + /* Wait here until the write is done. This makes + hid_write() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_written, TRUE/*wait*/); + if (!res) { + /* The Write operation failed. */ + register_error(dev, "WriteFile"); + bytes_written = -1; + goto end_of_function; + } + +end_of_function: + if (buf != data) + free(buf); + + return bytes_written; +} + + +int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + DWORD bytes_read = 0; + size_t copy_len = 0; + BOOL res; + + /* Copy the handle for convenience. */ + HANDLE ev = dev->ol.hEvent; + + if (!dev->read_pending) { + /* Start an Overlapped I/O read. */ + dev->read_pending = TRUE; + memset(dev->read_buf, 0, dev->input_report_length); + ResetEvent(ev); + res = ReadFile(dev->device_handle, dev->read_buf, dev->input_report_length, &bytes_read, &dev->ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* ReadFile() has failed. + Clean up and return error. */ + CancelIo(dev->device_handle); + dev->read_pending = FALSE; + goto end_of_function; + } + } + } + + if (milliseconds >= 0) { + /* See if there is any data yet. */ + res = WaitForSingleObject(ev, milliseconds); + if (res != WAIT_OBJECT_0) { + /* There was no data this time. Return zero bytes available, + but leave the Overlapped I/O running. */ + return 0; + } + } + + /* Either WaitForSingleObject() told us that ReadFile has completed, or + we are in non-blocking mode. Get the number of bytes read. The actual + data has been copied to the data[] array which was passed to ReadFile(). */ + res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, TRUE/*wait*/); + + /* Set pending back to false, even if GetOverlappedResult() returned error. */ + dev->read_pending = FALSE; + + if (res && bytes_read > 0) { + if (dev->read_buf[0] == 0x0) { + /* If report numbers aren't being used, but Windows sticks a report + number (0x0) on the beginning of the report anyway. To make this + work like the other platforms, and to make it work more like the + HID spec, we'll skip over this byte. */ + bytes_read--; + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf+1, copy_len); + } + else { + /* Copy the whole buffer, report number and all. */ + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf, copy_len); + } + } + +end_of_function: + if (!res) { + register_error(dev, "GetOverlappedResult"); + return -1; + } + + return copy_len; +} + +int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) +{ + dev->blocking = !nonblock; + return 0; /* Success */ +} + +int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + BOOL res = HidD_SetFeature(dev->device_handle, (PVOID)data, length); + if (!res) { + register_error(dev, "HidD_SetFeature"); + return -1; + } + + return length; +} + + +int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + BOOL res; +#if 0 + res = HidD_GetFeature(dev->device_handle, data, length); + if (!res) { + register_error(dev, "HidD_GetFeature"); + return -1; + } + return 0; /* HidD_GetFeature() doesn't give us an actual length, unfortunately */ +#else + DWORD bytes_returned; + + OVERLAPPED ol; + memset(&ol, 0, sizeof(ol)); + + res = DeviceIoControl(dev->device_handle, + IOCTL_HID_GET_FEATURE, + data, length, + data, length, + &bytes_returned, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* DeviceIoControl() failed. Return error. */ + register_error(dev, "Send Feature Report DeviceIoControl"); + return -1; + } + } + + /* Wait here until the write is done. This makes + hid_get_feature_report() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); + if (!res) { + /* The operation failed. */ + register_error(dev, "Send Feature Report GetOverLappedResult"); + return -1; + } + + /* bytes_returned does not include the first byte which contains the + report ID. The data buffer actually contains one more byte than + bytes_returned. */ + bytes_returned++; + + return bytes_returned; +#endif +} + +void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) +{ + if (!dev) + return; + CancelIo(dev->device_handle); + free_hid_device(dev); +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetManufacturerString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetManufacturerString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetProductString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetProductString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetSerialNumberString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetSerialNumberString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetIndexedString(dev->device_handle, string_index, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetIndexedString"); + return -1; + } + + return 0; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return (wchar_t*)dev->last_error_str; +} + + +/*#define PICPGM*/ +/*#define S11*/ +#define P32 +#ifdef S11 + unsigned short VendorID = 0xa0a0; + unsigned short ProductID = 0x0001; +#endif + +#ifdef P32 + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x3f; +#endif + + +#ifdef PICPGM + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x0033; +#endif + + +#if 0 +int __cdecl main(int argc, char* argv[]) +{ + int res; + unsigned char buf[65]; + + UNREFERENCED_PARAMETER(argc); + UNREFERENCED_PARAMETER(argv); + + /* Set up the command buffer. */ + memset(buf,0x00,sizeof(buf)); + buf[0] = 0; + buf[1] = 0x81; + + + /* Open the device. */ + int handle = open(VendorID, ProductID, L"12345"); + if (handle < 0) + printf("unable to open device\n"); + + + /* Toggle LED (cmd 0x80) */ + buf[1] = 0x80; + res = write(handle, buf, 65); + if (res < 0) + printf("Unable to write()\n"); + + /* Request state (cmd 0x81) */ + buf[1] = 0x81; + write(handle, buf, 65); + if (res < 0) + printf("Unable to write() (2)\n"); + + /* Read requested state */ + read(handle, buf, 65); + if (res < 0) + printf("Unable to read()\n"); + + /* Print out the returned buffer. */ + for (int i = 0; i < 4; i++) + printf("buf[%d]: %d\n", i, buf[i]); + + return 0; +} +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.cpp b/src/libslic3r/Fill/Fill3DHoneycomb.cpp index 6c4b4d903..8aac6e49c 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.cpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -169,7 +169,7 @@ void Fill3DHoneycomb::_fill_surface_single( if (params.dont_connect) append(polylines_out, std::move(polylines_chained)); else - this->connect_infill(std::move(polylines_chained), expolygon, polylines_out, params); + this->connect_infill(std::move(polylines_chained), expolygon, polylines_out, this->spacing, params); } } diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index 88eba9a51..39679e3d6 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -2,6 +2,7 @@ #include "../ClipperUtils.hpp" #include "../EdgeGrid.hpp" +#include "../Geometry.hpp" #include "../Surface.hpp" #include "../PrintConfig.hpp" #include "../libslic3r.h" @@ -609,16 +610,15 @@ static inline SegmentPoint clip_start_segment_and_point(const Points &polyline, // Initialized to "invalid". SegmentPoint out; if (polyline.size() >= 2) { - const double d2 = distance * distance; Vec2d pt_prev = polyline.front().cast<double>(); for (int i = 1; i < polyline.size(); ++ i) { Vec2d pt = polyline[i].cast<double>(); Vec2d v = pt - pt_prev; double l2 = v.squaredNorm(); - if (l2 > d2) { + if (l2 > distance * distance) { out.idx_segment = i; out.t = distance / sqrt(l2); - out.point = pt + out.t * v; + out.point = pt_prev + out.t * v; break; } distance -= sqrt(l2); @@ -635,16 +635,17 @@ static inline SegmentPoint clip_end_segment_and_point(const Points &polyline, do // Initialized to "invalid". SegmentPoint out; if (polyline.size() >= 2) { - const double d2 = distance * distance; Vec2d pt_next = polyline.back().cast<double>(); for (int i = int(polyline.size()) - 2; i >= 0; -- i) { Vec2d pt = polyline[i].cast<double>(); Vec2d v = pt - pt_next; double l2 = v.squaredNorm(); - if (l2 > d2) { + if (l2 > distance * distance) { out.idx_segment = i; out.t = distance / sqrt(l2); - out.point = pt + out.t * v; + out.point = pt_next + out.t * v; + // Store the parameter referenced to the starting point of a segment. + out.t = 1. - out.t; break; } distance -= sqrt(l2); @@ -654,21 +655,26 @@ static inline SegmentPoint clip_end_segment_and_point(const Points &polyline, do return out; } +// Optimized version with the precalculated v1 = p1b - p1a and l1_2 = v1.squaredNorm(). +// Assumption: l1_2 < EPSILON. +static inline double segment_point_distance_squared(const Vec2d &p1a, const Vec2d &p1b, const Vec2d &v1, const double l1_2, const Vec2d &p2) +{ + assert(l1_2 > EPSILON); + Vec2d v12 = p2 - p1a; + double t = v12.dot(v1); + return (t <= 0. ) ? v12.squaredNorm() : + (t >= l1_2) ? (p2 - p1a).squaredNorm() : + ((t / l1_2) * v1 - v12).squaredNorm(); +} + static inline double segment_point_distance_squared(const Vec2d &p1a, const Vec2d &p1b, const Vec2d &p2) { - const Vec2d v = p1b - p1a; - const Vec2d va = p2 - p1a; - const double l2 = v.squaredNorm(); + const Vec2d v = p1b - p1a; + const double l2 = v.squaredNorm(); if (l2 < EPSILON) // p1a == p1b - return va.squaredNorm(); - // Project p2 onto the (p1a, p1b) segment. - const double t = va.dot(v); - if (t < 0.) - return va.squaredNorm(); - else if (t > l2) - return (p2 - p1b).squaredNorm(); - return ((t / l2) * v - va).squaredNorm(); + return (p2 - p1a).squaredNorm(); + return segment_point_distance_squared(p1a, p1b, v, v.squaredNorm(), p2); } // Distance to the closest point of line. @@ -684,43 +690,11 @@ static inline double min_distance_of_segments(const Vec2d &p1a, const Vec2d &p1b double l2_2 = v2.squaredNorm(); if (l2_2 < EPSILON) // p2a == p2b: Return distance of p2a from the (p1a, p1b) segment. - return segment_point_distance_squared(p1a, p1b, p2a); - - // Project p2a, p2b onto the (p1a, p1b) segment. - auto project_p2a_p2b_onto_seg_p1a_p1b = [](const Vec2d& p1a, const Vec2d& p1b, const Vec2d& p2a, const Vec2d& p2b, const Vec2d& v1, const double l1_2) { - Vec2d v1a2a = p2a - p1a; - Vec2d v1a2b = p2b - p1a; - double t1 = v1a2a.dot(v1); - double t2 = v1a2b.dot(v1); - if (t1 <= 0.) { - if (t2 <= 0.) - // Both p2a and p2b are left of v1. - return (((t1 < t2) ? p2b : p2a) - p1a).squaredNorm(); - else if (t2 < l1_2) - // Project p2b onto the (p1a, p1b) segment. - return ((t2 / l1_2) * v1 - v1a2b).squaredNorm(); - } - else if (t1 >= l1_2) { - if (t2 >= l1_2) - // Both p2a and p2b are right of v1. - return (((t1 < t2) ? p2a : p2b) - p1b).squaredNorm(); - else if (t2 < l1_2) - // Project p2b onto the (p1a, p1b) segment. - return ((t2 / l1_2) * v1 - v1a2b).squaredNorm(); - } - else { - // Project p1b onto the (p1a, p1b) segment. - double dist_min = ((t2 / l1_2) * v1 - v1a2a).squaredNorm(); - if (t2 > 0. && t2 < l1_2) - dist_min = std::min(dist_min, ((t2 / l1_2) * v1 - v1a2b).squaredNorm()); - return dist_min; - } - return std::numeric_limits<double>::max(); - }; + return segment_point_distance_squared(p1a, p1b, v1, l1_2, p2a); return std::min( - project_p2a_p2b_onto_seg_p1a_p1b(p1a, p1b, p2a, p2b, v1, l1_2), - project_p2a_p2b_onto_seg_p1a_p1b(p2a, p2b, p1a, p1b, v2, l2_2)); + std::min(segment_point_distance_squared(p1a, p1b, v1, l1_2, p2a), segment_point_distance_squared(p1a, p1b, v1, l1_2, p2b)), + std::min(segment_point_distance_squared(p2a, p2b, v2, l2_2, p1a), segment_point_distance_squared(p2a, p2b, v2, l2_2, p1b))); } // Mark the segments of split boundary as consumed if they are very close to some of the infill line. @@ -756,11 +730,26 @@ void mark_boundary_segments_touching_infill( const Vec2d seg_pt2 = segment.second.cast<double>(); if (min_distance_of_segments(seg_pt1, seg_pt2, *this->pt1, *this->pt2) < this->dist2_max) { // Mark this boundary segment as touching the infill line. - ContourPointData&bdp = boundary_data[it_contour_and_segment->first][it_contour_and_segment->second]; + ContourPointData &bdp = boundary_data[it_contour_and_segment->first][it_contour_and_segment->second]; bdp.segment_consumed = true; // There is no need for checking seg_pt2 as it will be checked the next time. - if (segment_point_distance_squared(*this->pt1, *this->pt2, seg_pt1) < this->dist2_max) + bool point_touching = false; + if (segment_point_distance_squared(*this->pt1, *this->pt2, seg_pt1) < this->dist2_max) { + point_touching = true; bdp.point_consumed = true; + } +#if 0 + { + static size_t iRun = 0; + ExPolygon expoly(Polygon(*grid.contours().front())); + for (size_t i = 1; i < grid.contours().size(); ++i) + expoly.holes.emplace_back(Polygon(*grid.contours()[i])); + SVG svg(debug_out_path("%s-%d.svg", "FillBase-mark_boundary_segments_touching_infill", iRun ++).c_str(), get_extents(expoly)); + svg.draw(expoly, "green"); + svg.draw(Line(segment.first, segment.second), "red"); + svg.draw(Line(this->pt1->cast<coord_t>(), this->pt2->cast<coord_t>()), "magenta"); + } +#endif } } // Continue traversing the grid along the edge. @@ -777,6 +766,9 @@ void mark_boundary_segments_touching_infill( const Vec2d *pt2; } visitor(grid, boundary, boundary_data, distance_colliding * distance_colliding); + BoundingBoxf bboxf(boundary_bbox.min.cast<double>(), boundary_bbox.max.cast<double>()); + bboxf.offset(- SCALED_EPSILON); + for (const Polyline &polyline : infill) { // Clip the infill polyline by the Eucledian distance along the polyline. SegmentPoint start_point = clip_start_segment_and_point(polyline.points, clip_distance); @@ -806,25 +798,39 @@ void mark_boundary_segments_touching_infill( visitor.init(pt1d, pt2d); grid.visit_cells_intersecting_thick_line(pt1, pt2, distance_colliding, visitor); #else - Vec2d pt1 = (point_idx == start_point.idx_segment) ? start_point.point : polyline.points[point_idx].cast<double>(); - Vec2d pt2 = (point_idx == end_point .idx_segment) ? end_point .point : polyline.points[point_idx].cast<double>(); + Vec2d pt1 = (point_idx == start_point.idx_segment) ? start_point.point : polyline.points[point_idx ].cast<double>(); + Vec2d pt2 = (point_idx == end_point .idx_segment) ? end_point .point : polyline.points[point_idx + 1].cast<double>(); +#if 0 + { + static size_t iRun = 0; + ExPolygon expoly(Polygon(*grid.contours().front())); + for (size_t i = 1; i < grid.contours().size(); ++i) + expoly.holes.emplace_back(Polygon(*grid.contours()[i])); + SVG svg(debug_out_path("%s-%d.svg", "FillBase-mark_boundary_segments_touching_infill0", iRun ++).c_str(), get_extents(expoly)); + svg.draw(expoly, "green"); + svg.draw(polyline, "blue"); + svg.draw(Line(pt1.cast<coord_t>(), pt2.cast<coord_t>()), "magenta", scale_(0.1)); + } +#endif visitor.init(pt1, pt2); // Simulate tracing of a thick line. This only works reliably if distance_colliding <= grid cell size. Vec2d v = (pt2 - pt1).normalized() * distance_colliding; Vec2d vperp(-v.y(), v.x()); Vec2d a = pt1 - v - vperp; Vec2d b = pt1 + v - vperp; - grid.visit_cells_intersecting_line(a.cast<coord_t>(), b.cast<coord_t>(), visitor); + if (Geometry::liang_barsky_line_clipping(a, b, bboxf)) + grid.visit_cells_intersecting_line(a.cast<coord_t>(), b.cast<coord_t>(), visitor); a = pt1 - v + vperp; b = pt1 + v + vperp; - grid.visit_cells_intersecting_line(a.cast<coord_t>(), b.cast<coord_t>(), visitor); + if (Geometry::liang_barsky_line_clipping(a, b, bboxf)) + grid.visit_cells_intersecting_line(a.cast<coord_t>(), b.cast<coord_t>(), visitor); #endif } } } } -void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_src, Polylines &polylines_out, const FillParams ¶ms) +void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_src, Polylines &polylines_out, const double spacing, const FillParams ¶ms) { assert(! infill_ordered.empty()); assert(! boundary_src.contour.points.empty()); @@ -900,16 +906,16 @@ void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_ // Mark the points and segments of split boundary as consumed if they are very close to some of the infill line. { - //const double clip_distance = scale_(this->spacing); - const double clip_distance = 3. * scale_(this->spacing); - const double distance_colliding = scale_(this->spacing); + // @supermerill used 2. * scale_(spacing) + const double clip_distance = 3. * scale_(spacing); + const double distance_colliding = 1.1 * scale_(spacing); mark_boundary_segments_touching_infill(boundary, boundary_data, bbox, infill_ordered, clip_distance, distance_colliding); } // Connection from end of one infill line to the start of another infill line. - //const float length_max = scale_(this->spacing); -// const float length_max = scale_((2. / params.density) * this->spacing); - const float length_max = scale_((1000. / params.density) * this->spacing); + //const float length_max = scale_(spacing); +// const float length_max = scale_((2. / params.density) * spacing); + const float length_max = scale_((1000. / params.density) * spacing); std::vector<size_t> merged_with(infill_ordered.size()); for (size_t i = 0; i < merged_with.size(); ++ i) merged_with[i] = i; @@ -951,12 +957,26 @@ void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_ size_t idx_chain_last = 0; for (ConnectionCost &connection_cost : connections_sorted) { - const std::pair<size_t, size_t> *cp1 = &map_infill_end_point_to_boundary[connection_cost.idx_first * 2 + 1]; - const std::pair<size_t, size_t> *cp2 = &map_infill_end_point_to_boundary[(connection_cost.idx_first + 1) * 2]; + const std::pair<size_t, size_t> *cp1 = &map_infill_end_point_to_boundary[connection_cost.idx_first * 2 + 1]; + const std::pair<size_t, size_t> *cp1prev = cp1 - 1; + const std::pair<size_t, size_t> *cp2 = &map_infill_end_point_to_boundary[(connection_cost.idx_first + 1) * 2]; + const std::pair<size_t, size_t> *cp2next = cp2 + 1; assert(cp1->first == cp2->first); std::vector<ContourPointData> &contour_data = boundary_data[cp1->first]; if (connection_cost.reversed) std::swap(cp1, cp2); + // Mark the the other end points of the segments to be taken as consumed temporarily, so they will not be crossed + // by the new connection line. + bool prev_marked = false; + bool next_marked = false; + if (cp1prev->first == cp1->first && ! contour_data[cp1prev->second].point_consumed) { + contour_data[cp1prev->second].point_consumed = true; + prev_marked = true; + } + if (cp2next->first == cp1->first && ! contour_data[cp2next->second].point_consumed) { + contour_data[cp2next->second].point_consumed = true; + next_marked = true; + } if (could_take(contour_data, cp1->second, cp2->second)) { // Indices of the polygons to be connected. size_t idx_first = connection_cost.idx_first; @@ -975,6 +995,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary_ // Mark the second polygon as merged with the first one. merged_with[idx_second] = merged_with[idx_first]; } + if (prev_marked) + contour_data[cp1prev->second].point_consumed = false; + if (next_marked) + contour_data[cp2next->second].point_consumed = false; } polylines_out.reserve(polylines_out.size() + std::count_if(infill_ordered.begin(), infill_ordered.end(), [](const Polyline &pl) { return ! pl.empty(); })); for (Polyline &pl : infill_ordered) diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index d005f6e4a..517ce8383 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -111,9 +111,9 @@ protected: virtual std::pair<float, Point> _infill_direction(const Surface *surface) const; - void connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary, Polylines &polylines_out, const FillParams ¶ms); - public: + static void connect_infill(Polylines &&infill_ordered, const ExPolygon &boundary, Polylines &polylines_out, double spacing, const FillParams ¶ms); + static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance); // Align a coordinate to a grid. The coordinate may be negative, diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp index ed0586edf..913b0b0c0 100644 --- a/src/libslic3r/Fill/FillGyroid.cpp +++ b/src/libslic3r/Fill/FillGyroid.cpp @@ -185,6 +185,7 @@ void FillGyroid::_fill_surface_single( if (! polylines.empty()) // remove too small bits (larger than longer) polylines.erase( + //FIXME what is the small size? Removing tiny extrusions disconnects walls! std::remove_if(polylines.begin(), polylines.end(), [this](const Polyline &pl) { return pl.length() < scale_(this->spacing * 3); }), polylines.end()); @@ -195,7 +196,7 @@ void FillGyroid::_fill_surface_single( if (params.dont_connect) append(polylines_out, std::move(polylines)); else - this->connect_infill(std::move(polylines), expolygon, polylines_out, params); + this->connect_infill(std::move(polylines), expolygon, polylines_out, this->spacing, params); // new paths must be rotated back if (abs(infill_angle) >= EPSILON) { for (auto it = polylines_out.begin() + polylines_out_first_idx; it != polylines_out.end(); ++ it) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 516dc2919..3cb89bedf 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -52,6 +52,7 @@ const std::string LAYER_HEIGHTS_PROFILE_FILE = "Metadata/Slic3r_PE_layer_heights const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config_ranges.xml"; const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; +const std::string CUSTOM_GCODE_PER_HEIGHT_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_height.xml"; const char* MODEL_TAG = "model"; const char* RESOURCES_TAG = "resources"; @@ -421,6 +422,8 @@ namespace Slic3r { void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_custom_gcode_per_height_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, const std::string& archive_filename); bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); @@ -635,6 +638,11 @@ namespace Slic3r { // extract slic3r print config file _extract_print_config_from_archive(archive, stat, config, filename); } + if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_HEIGHT_FILE)) + { + // extract slic3r layer config ranges file + _extract_custom_gcode_per_height_from_archive(archive, stat); + } else if (boost::algorithm::iequals(name, MODEL_CONFIG_FILE)) { // extract slic3r model config file @@ -1155,6 +1163,43 @@ namespace Slic3r { return true; } + void _3MF_Importer::_extract_custom_gcode_per_height_from_archive(::mz_zip_archive &archive, const mz_zip_archive_file_stat &stat) + { + if (stat.m_uncomp_size > 0) + { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading custom Gcodes per height data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree main_tree; + pt::read_xml(iss, main_tree); + + if (main_tree.front().first != "custom_gcodes_per_height") + return; + pt::ptree code_tree = main_tree.front().second; + + if (!m_model->custom_gcode_per_height.empty()) + m_model->custom_gcode_per_height.clear(); + + for (const auto& code : code_tree) + { + if (code.first != "code") + continue; + pt::ptree tree = code.second; + double height = tree.get<double>("<xmlattr>.height"); + std::string gcode = tree.get<std::string>("<xmlattr>.gcode"); + int extruder = tree.get<int>("<xmlattr>.extruder"); + std::string color = tree.get<std::string>("<xmlattr>.color"); + + m_model->custom_gcode_per_height.push_back(Model::CustomGCode(height, gcode, extruder, color)) ; + } + } + } + void _3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes) { if (m_xml_parser == nullptr) @@ -1568,8 +1613,10 @@ namespace Slic3r { if (m_check_version && (m_version > VERSION_3MF)) { - std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); - throw version_error(msg.c_str()); + // std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); + // throw version_error(msg.c_str()); + const std::string msg = (boost::format(_(L("The selected 3mf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); + throw version_error(msg); } } @@ -1938,6 +1985,7 @@ namespace Slic3r { bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config); bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data); + bool _add_custom_gcode_per_height_file_to_archive(mz_zip_archive& archive, Model& model); }; #if ENABLE_THUMBNAIL_GENERATOR @@ -2048,6 +2096,15 @@ namespace Slic3r { } + // Adds custom gcode per height file ("Metadata/Prusa_Slicer_custom_gcode_per_height.xml"). + // All custom gcode per height of whole Model are stored here + if (!_add_custom_gcode_per_height_file_to_archive(archive, model)) + { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). // This file contains the content of FullPrintConfing / SLAFullPrintConfig. if (config != nullptr) @@ -2432,7 +2489,7 @@ namespace Slic3r { if (!tree.empty()) { std::ostringstream oss; - boost::property_tree::write_xml(oss, tree); + pt::write_xml(oss, tree); out = oss.str(); // Post processing("beautification") of the output string for a better preview @@ -2662,7 +2719,49 @@ namespace Slic3r { return true; } - bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) +bool _3MF_Exporter::_add_custom_gcode_per_height_file_to_archive( mz_zip_archive& archive, Model& model) +{ + std::string out = ""; + + if (!model.custom_gcode_per_height.empty()) + { + pt::ptree tree; + pt::ptree& main_tree = tree.add("custom_gcodes_per_height", ""); + + for (const Model::CustomGCode& code : model.custom_gcode_per_height) + { + pt::ptree& code_tree = main_tree.add("code", ""); + // store minX and maxZ + code_tree.put("<xmlattr>.height" , code.height ); + code_tree.put("<xmlattr>.gcode" , code.gcode ); + code_tree.put("<xmlattr>.extruder" , code.extruder ); + code_tree.put("<xmlattr>.color" , code.color ); + } + + if (!tree.empty()) + { + std::ostringstream oss; + boost::property_tree::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string + boost::replace_all(out, "><", ">\n<"); + } + } + + if (!out.empty()) + { + if (!mz_zip_writer_add_mem(&archive, CUSTOM_GCODE_PER_HEIGHT_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) + { + add_error("Unable to add custom Gcodes per height file to archive"); + return false; + } + } + + return true; +} + +bool load_3mf(const char* path, DynamicPrintConfig* config, Model* model, bool check_version) { if ((path == nullptr) || (config == nullptr) || (model == nullptr)) return false; diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp index 181d6cb99..d50f6e395 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -16,6 +16,10 @@ #include "AMF.hpp" +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/xml_parser.hpp> +namespace pt = boost::property_tree; + #include <boost/filesystem/operations.hpp> #include <boost/algorithm/string.hpp> #include <boost/nowide/fstream.hpp> @@ -147,6 +151,8 @@ struct AMFParserContext NODE_TYPE_MIRRORY, // amf/constellation/instance/mirrory NODE_TYPE_MIRRORZ, // amf/constellation/instance/mirrorz NODE_TYPE_PRINTABLE, // amf/constellation/instance/mirrorz + NODE_TYPE_CUSTOM_GCODE, // amf/custom_code_per_height + NODE_TYPE_GCODE_PER_HEIGHT, // amf/custom_code_per_height/code NODE_TYPE_METADATA, // anywhere under amf/*/metadata }; @@ -227,7 +233,7 @@ struct AMFParserContext // Current instance allocated for an amf/constellation/instance subtree. Instance *m_instance; // Generic string buffer for vertices, face indices, metadata etc. - std::string m_value[3]; + std::string m_value[4]; // Pointer to config to update if config data are stored inside the amf file DynamicPrintConfig *m_config; @@ -268,6 +274,8 @@ void AMFParserContext::startElement(const char *name, const char **atts) } } else if (strcmp(name, "constellation") == 0) { node_type_new = NODE_TYPE_CONSTELLATION; + } else if (strcmp(name, "custom_gcodes_per_height") == 0) { + node_type_new = NODE_TYPE_CUSTOM_GCODE; } break; case 2: @@ -294,6 +302,13 @@ void AMFParserContext::startElement(const char *name, const char **atts) } else this->stop(); + } + else if (strcmp(name, "code") == 0 && m_path[1] == NODE_TYPE_CUSTOM_GCODE) { + node_type_new = NODE_TYPE_GCODE_PER_HEIGHT; + m_value[0] = get_attribute(atts, "height"); + m_value[1] = get_attribute(atts, "gcode"); + m_value[2] = get_attribute(atts, "extruder"); + m_value[3] = get_attribute(atts, "color"); } break; case 3: @@ -616,6 +631,19 @@ void AMFParserContext::endElement(const char * /* name */) m_instance = nullptr; break; + case NODE_TYPE_GCODE_PER_HEIGHT: { + double height = double(atof(m_value[0].c_str())); + const std::string& gcode = m_value[1]; + int extruder = atoi(m_value[2].c_str()); + const std::string& color = m_value[3]; + + m_model.custom_gcode_per_height.push_back(Model::CustomGCode(height, gcode, extruder, color)); + + for (std::string& val: m_value) + val.clear(); + break; + } + case NODE_TYPE_METADATA: if ((m_config != nullptr) && strncmp(m_value[0].c_str(), SLIC3R_CONFIG_TYPE, strlen(SLIC3R_CONFIG_TYPE)) == 0) m_config->load_from_gcode_string(m_value[1].c_str()); @@ -884,8 +912,10 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi if (check_version && (ctx.m_version > VERSION_AMF)) { - std::string msg = _(L("The selected amf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); - throw std::runtime_error(msg.c_str()); + // std::string msg = _(L("The selected amf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); + // throw std::runtime_error(msg.c_str()); + const std::string msg = (boost::format(_(L("The selected amf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); + throw std::runtime_error(msg); } return true; @@ -1190,6 +1220,42 @@ bool store_amf(const char *path, Model *model, const DynamicPrintConfig *config) stream << instances; stream << " </constellation>\n"; } + + if (!model->custom_gcode_per_height.empty()) + { + std::string out = ""; + pt::ptree tree; + + pt::ptree& main_tree = tree.add("custom_gcodes_per_height", ""); + + for (const Model::CustomGCode& code : model->custom_gcode_per_height) + { + pt::ptree& code_tree = main_tree.add("code", ""); + // store minX and maxZ + code_tree.put("<xmlattr>.height", code.height); + code_tree.put("<xmlattr>.gcode", code.gcode); + code_tree.put("<xmlattr>.extruder", code.extruder); + code_tree.put("<xmlattr>.color", code.color); + } + + if (!tree.empty()) + { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + int del_header_pos = out.find("<custom_gcodes_per_height"); + if (del_header_pos != std::string::npos) + out.erase(out.begin(), out.begin() + del_header_pos); + + // Post processing("beautification") of the output string + boost::replace_all(out, "><code", ">\n <code"); + boost::replace_all(out, "><", ">\n<"); + + stream << out << "\n"; + } + } + stream << "</amf>\n"; std::string internal_amf_filename = boost::ireplace_last_copy(boost::filesystem::path(export_path).filename().string(), ".zip.amf", ".amf"); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 56a94b28c..91627631f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6,9 +6,6 @@ #include "Geometry.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/WipeTower.hpp" -#if ENABLE_THUMBNAIL_GENERATOR -#include "GCode/ThumbnailData.hpp" -#endif // ENABLE_THUMBNAIL_GENERATOR #include "ShortestPath.hpp" #include "Utils.hpp" @@ -35,9 +32,7 @@ #include <Shiny/Shiny.h> -#if ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE #include "miniz_extension.hpp" -#endif // ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE #if 0 // Enable debugging and asserts, even in the release build. @@ -432,39 +427,44 @@ std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower: Vec2f pos = tcr.start_pos; Vec2f transformed_pos = pos; Vec2f old_pos(-1000.1f, -1000.1f); + std::string never_skip_tag = WipeTower::never_skip_tag(); while (gcode_str) { std::getline(gcode_str, line); // we read the gcode line by line - // All G1 commands should be translated and rotated + // All G1 commands should be translated and rotated. X and Y coords are + // only pushed to the output when they differ from last time. + // WT generator can override this by appending the never_skip_tag if (line.find("G1 ") == 0) { + bool never_skip = false; + auto it = line.find(never_skip_tag); + if (it != std::string::npos) { + // remove the tag and remember we saw it + never_skip = true; + line.erase(it, it+never_skip_tag.size()); + } std::ostringstream line_out; std::istringstream line_str(line); line_str >> std::noskipws; // don't skip whitespace char ch = 0; while (line_str >> ch) { - if (ch == 'X') - line_str >> pos.x(); + if (ch == 'X' || ch =='Y') + line_str >> (ch == 'X' ? pos.x() : pos.y()); else - if (ch == 'Y') - line_str >> pos.y(); - else - line_out << ch; + line_out << ch; } - transformed_pos = pos; - transformed_pos = Eigen::Rotation2Df(angle) * transformed_pos; - transformed_pos += translation; + transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; - if (transformed_pos != old_pos) { + if (transformed_pos != old_pos || never_skip) { line = line_out.str(); std::ostringstream oss; oss << std::fixed << std::setprecision(3) << "G1 "; - if (transformed_pos.x() != old_pos.x()) + if (transformed_pos.x() != old_pos.x() || never_skip) oss << " X" << transformed_pos.x() - extruder_offset.x(); - if (transformed_pos.y() != old_pos.y()) + if (transformed_pos.y() != old_pos.y() || never_skip) oss << " Y" << transformed_pos.y() - extruder_offset.y(); - + oss << " "; line.replace(line.find("G1 "), 3, oss.str()); old_pos = transformed_pos; } @@ -496,37 +496,37 @@ std::string WipeTowerIntegration::prime(GCode &gcodegen) assert(m_layer_idx == 0); std::string gcode; - if (&m_priming != nullptr) { - // Disable linear advance for the wipe tower operations. - //gcode += (gcodegen.config().gcode_flavor == gcfRepRap ? std::string("M572 D0 S0\n") : std::string("M900 K0\n")); - for (const WipeTower::ToolChangeResult& tcr : m_priming) { - if (!tcr.extrusions.empty()) - gcode += append_tcr(gcodegen, tcr, tcr.new_tool); + // Disable linear advance for the wipe tower operations. + //gcode += (gcodegen.config().gcode_flavor == gcfRepRap ? std::string("M572 D0 S0\n") : std::string("M900 K0\n")); + for (const WipeTower::ToolChangeResult& tcr : m_priming) { + if (!tcr.extrusions.empty()) + gcode += append_tcr(gcodegen, tcr, tcr.new_tool); - // Let the tool change be executed by the wipe tower class. - // Inform the G-code writer about the changes done behind its back. - //gcode += tcr.gcode; - // Let the m_writer know the current extruder_id, but ignore the generated G-code. - // unsigned int current_extruder_id = tcr.extrusions.back().tool; - // gcodegen.writer().toolchange(current_extruder_id); - // gcodegen.placeholder_parser().set("current_extruder", current_extruder_id); - } + // Let the tool change be executed by the wipe tower class. + // Inform the G-code writer about the changes done behind its back. + //gcode += tcr.gcode; + // Let the m_writer know the current extruder_id, but ignore the generated G-code. + // unsigned int current_extruder_id = tcr.extrusions.back().tool; + // gcodegen.writer().toolchange(current_extruder_id); + // gcodegen.placeholder_parser().set("current_extruder", current_extruder_id); - // A phony move to the end position at the wipe tower. - /* gcodegen.writer().travel_to_xy(Vec2d(m_priming.back().end_pos.x, m_priming.back().end_pos.y)); - gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); - // Prepare a future wipe. - gcodegen.m_wipe.path.points.clear(); - // Start the wipe at the current position. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); - // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, - WipeTower::xy((std::abs(m_left - m_priming.back().end_pos.x) < std::abs(m_right - m_priming.back().end_pos.x)) ? m_right : m_left, - m_priming.back().end_pos.y)));*/ } + + // A phony move to the end position at the wipe tower. + /* gcodegen.writer().travel_to_xy(Vec2d(m_priming.back().end_pos.x, m_priming.back().end_pos.y)); + gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); + // Prepare a future wipe. + gcodegen.m_wipe.path.points.clear(); + // Start the wipe at the current position. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); + // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, + WipeTower::xy((std::abs(m_left - m_priming.back().end_pos.x) < std::abs(m_right - m_priming.back().end_pos.x)) ? m_right : m_left, + m_priming.back().end_pos.y)));*/ + return gcode; } @@ -631,7 +631,7 @@ std::vector<GCode::LayerToPrint> GCode::collect_layers_to_print(const PrintObjec if (layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) throw std::runtime_error(_(L("Empty layers detected, the output would not be printable.")) + "\n\n" + - _(L("Object name: ")) + object.model_object()->name + "\n" + _(L("Print z: ")) + + _(L("Object name")) + ": " + object.model_object()->name + "\n" + _(L("Print z")) + ": " + std::to_string(layers_to_print.back().print_z()) + "\n\n" + _(L("This is " "usually caused by negligibly small extrusions or by a faulty model. Try to repair " " the model or change its orientation on the bed."))); @@ -695,7 +695,7 @@ std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> GCode::collec } #if ENABLE_THUMBNAIL_GENERATOR -void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_data, const std::vector<ThumbnailData>* thumbnail_data) +void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb) #else void GCode::do_export(Print *print, const char *path, GCodePreviewData *preview_data) #endif // ENABLE_THUMBNAIL_GENERATOR @@ -725,7 +725,7 @@ void GCode::do_export(Print *print, const char *path, GCodePreviewData *preview_ try { m_placeholder_parser_failed_templates.clear(); #if ENABLE_THUMBNAIL_GENERATOR - this->_do_export(*print, file, thumbnail_data); + this->_do_export(*print, file, thumbnail_cb); #else this->_do_export(*print, file); #endif // ENABLE_THUMBNAIL_GENERATOR @@ -793,9 +793,9 @@ void GCode::do_export(Print *print, const char *path, GCodePreviewData *preview_ } #if ENABLE_THUMBNAIL_GENERATOR -void GCode::_do_export(Print& print, FILE* file, const std::vector<ThumbnailData>* thumbnail_data) +void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thumbnail_cb) #else -void GCode::_do_export(Print &print, FILE *file) +void GCode::_do_export(Print& print, FILE* file) #endif // ENABLE_THUMBNAIL_GENERATOR { PROFILE_FUNC(); @@ -803,6 +803,7 @@ void GCode::_do_export(Print &print, FILE *file) // resets time estimators m_normal_time_estimator.reset(); m_normal_time_estimator.set_dialect(print.config().gcode_flavor); + m_normal_time_estimator.set_extrusion_axis(print.config().get_extrusion_axis()[0]); m_silent_time_estimator_enabled = (print.config().gcode_flavor == gcfMarlin) && print.config().silent_mode; // Until we have a UI support for the other firmwares than the Marlin, use the hardcoded default values @@ -812,46 +813,47 @@ void GCode::_do_export(Print &print, FILE *file) // shall be adjusted as well to produce a G-code block compatible with the particular firmware flavor. if (print.config().gcode_flavor.value == gcfMarlin) { m_normal_time_estimator.set_max_acceleration((float)print.config().machine_max_acceleration_extruding.values[0]); - m_normal_time_estimator.set_retract_acceleration((float)print.config().machine_max_acceleration_retracting.values[0]); - m_normal_time_estimator.set_minimum_feedrate((float)print.config().machine_min_extruding_rate.values[0]); - m_normal_time_estimator.set_minimum_travel_feedrate((float)print.config().machine_min_travel_rate.values[0]); - m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::X, (float)print.config().machine_max_acceleration_x.values[0]); - m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Y, (float)print.config().machine_max_acceleration_y.values[0]); - m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Z, (float)print.config().machine_max_acceleration_z.values[0]); - m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::E, (float)print.config().machine_max_acceleration_e.values[0]); - m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::X, (float)print.config().machine_max_feedrate_x.values[0]); - m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Y, (float)print.config().machine_max_feedrate_y.values[0]); - m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Z, (float)print.config().machine_max_feedrate_z.values[0]); - m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::E, (float)print.config().machine_max_feedrate_e.values[0]); - m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::X, (float)print.config().machine_max_jerk_x.values[0]); - m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Y, (float)print.config().machine_max_jerk_y.values[0]); - m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Z, (float)print.config().machine_max_jerk_z.values[0]); - m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::E, (float)print.config().machine_max_jerk_e.values[0]); + m_normal_time_estimator.set_retract_acceleration((float)print.config().machine_max_acceleration_retracting.values[0]); + m_normal_time_estimator.set_minimum_feedrate((float)print.config().machine_min_extruding_rate.values[0]); + m_normal_time_estimator.set_minimum_travel_feedrate((float)print.config().machine_min_travel_rate.values[0]); + m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::X, (float)print.config().machine_max_acceleration_x.values[0]); + m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Y, (float)print.config().machine_max_acceleration_y.values[0]); + m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Z, (float)print.config().machine_max_acceleration_z.values[0]); + m_normal_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::E, (float)print.config().machine_max_acceleration_e.values[0]); + m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::X, (float)print.config().machine_max_feedrate_x.values[0]); + m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Y, (float)print.config().machine_max_feedrate_y.values[0]); + m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Z, (float)print.config().machine_max_feedrate_z.values[0]); + m_normal_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::E, (float)print.config().machine_max_feedrate_e.values[0]); + m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::X, (float)print.config().machine_max_jerk_x.values[0]); + m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Y, (float)print.config().machine_max_jerk_y.values[0]); + m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Z, (float)print.config().machine_max_jerk_z.values[0]); + m_normal_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::E, (float)print.config().machine_max_jerk_e.values[0]); if (m_silent_time_estimator_enabled) { m_silent_time_estimator.reset(); m_silent_time_estimator.set_dialect(print.config().gcode_flavor); - /* "Stealth mode" values can be just a copy of "normal mode" values + m_silent_time_estimator.set_extrusion_axis(print.config().get_extrusion_axis()[0]); + /* "Stealth mode" values can be just a copy of "normal mode" values * (when they aren't input for a printer preset). * Thus, use back value from values, instead of second one, which could be absent */ - m_silent_time_estimator.set_max_acceleration((float)print.config().machine_max_acceleration_extruding.values.back()); - m_silent_time_estimator.set_retract_acceleration((float)print.config().machine_max_acceleration_retracting.values.back()); - m_silent_time_estimator.set_minimum_feedrate((float)print.config().machine_min_extruding_rate.values.back()); - m_silent_time_estimator.set_minimum_travel_feedrate((float)print.config().machine_min_travel_rate.values.back()); - m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::X, (float)print.config().machine_max_acceleration_x.values.back()); - m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Y, (float)print.config().machine_max_acceleration_y.values.back()); - m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Z, (float)print.config().machine_max_acceleration_z.values.back()); - m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::E, (float)print.config().machine_max_acceleration_e.values.back()); - m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::X, (float)print.config().machine_max_feedrate_x.values.back()); - m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Y, (float)print.config().machine_max_feedrate_y.values.back()); - m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Z, (float)print.config().machine_max_feedrate_z.values.back()); - m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::E, (float)print.config().machine_max_feedrate_e.values.back()); - m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::X, (float)print.config().machine_max_jerk_x.values.back()); - m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Y, (float)print.config().machine_max_jerk_y.values.back()); - m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Z, (float)print.config().machine_max_jerk_z.values.back()); - m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::E, (float)print.config().machine_max_jerk_e.values.back()); + m_silent_time_estimator.set_max_acceleration((float)print.config().machine_max_acceleration_extruding.values.back()); + m_silent_time_estimator.set_retract_acceleration((float)print.config().machine_max_acceleration_retracting.values.back()); + m_silent_time_estimator.set_minimum_feedrate((float)print.config().machine_min_extruding_rate.values.back()); + m_silent_time_estimator.set_minimum_travel_feedrate((float)print.config().machine_min_travel_rate.values.back()); + m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::X, (float)print.config().machine_max_acceleration_x.values.back()); + m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Y, (float)print.config().machine_max_acceleration_y.values.back()); + m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::Z, (float)print.config().machine_max_acceleration_z.values.back()); + m_silent_time_estimator.set_axis_max_acceleration(GCodeTimeEstimator::E, (float)print.config().machine_max_acceleration_e.values.back()); + m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::X, (float)print.config().machine_max_feedrate_x.values.back()); + m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Y, (float)print.config().machine_max_feedrate_y.values.back()); + m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::Z, (float)print.config().machine_max_feedrate_z.values.back()); + m_silent_time_estimator.set_axis_max_feedrate(GCodeTimeEstimator::E, (float)print.config().machine_max_feedrate_e.values.back()); + m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::X, (float)print.config().machine_max_jerk_x.values.back()); + m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Y, (float)print.config().machine_max_jerk_y.values.back()); + m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::Z, (float)print.config().machine_max_jerk_z.values.back()); + m_silent_time_estimator.set_axis_max_jerk(GCodeTimeEstimator::E, (float)print.config().machine_max_jerk_e.values.back()); if (print.config().single_extruder_multi_material) { // As of now the fields are shown at the UI dialog in the same combo box as the ramming values, so they // are considered to be active for the single extruder multi-material printers only. @@ -881,6 +883,9 @@ void GCode::_do_export(Print &print, FILE *file) } m_analyzer.set_extruder_offsets(extruder_offsets); + // tell analyzer about the extrusion axis + m_analyzer.set_extrusion_axis(print.config().get_extrusion_axis()[0]); + // send extruders count to analyzer to allow it to detect invalid extruder idxs const ConfigOptionStrings* extruders_opt = dynamic_cast<const ConfigOptionStrings*>(print.config().option("extruder_colour")); const ConfigOptionStrings* filamemts_opt = dynamic_cast<const ConfigOptionStrings*>(print.config().option("filament_colour")); @@ -909,7 +914,8 @@ void GCode::_do_export(Print &print, FILE *file) std::sort(zs.begin(), zs.end()); m_layer_count += (unsigned int)(object->copies().size() * (std::unique(zs.begin(), zs.end()) - zs.begin())); } - } else { + } + else { // Print all objects with the same print_z together. std::vector<coordf_t> zs; for (auto object : print.objects()) { @@ -927,40 +933,41 @@ void GCode::_do_export(Print &print, FILE *file) m_enable_cooling_markers = true; this->apply_print_config(print.config()); this->set_extruders(print.extruders()); - - // Initialize colorprint. - m_colorprint_heights = cast<float>(print.config().colorprint_heights.values); + + // Initialize custom gcode + Model* model = print.get_object(0)->model_object()->get_model(); + m_custom_g_code_heights = model->custom_gcode_per_height; // Initialize autospeed. { // get the minimum cross-section used in the print std::vector<double> mm3_per_mm; for (auto object : print.objects()) { - for (size_t region_id = 0; region_id < object->region_volumes.size(); ++ region_id) { + for (size_t region_id = 0; region_id < object->region_volumes.size(); ++region_id) { const PrintRegion* region = print.regions()[region_id]; for (auto layer : object->layers()) { const LayerRegion* layerm = layer->regions()[region_id]; - if (region->config().get_abs_value("perimeter_speed" ) == 0 || - region->config().get_abs_value("small_perimeter_speed" ) == 0 || - region->config().get_abs_value("external_perimeter_speed" ) == 0 || - region->config().get_abs_value("bridge_speed" ) == 0) + if (region->config().get_abs_value("perimeter_speed") == 0 || + region->config().get_abs_value("small_perimeter_speed") == 0 || + region->config().get_abs_value("external_perimeter_speed") == 0 || + region->config().get_abs_value("bridge_speed") == 0) mm3_per_mm.push_back(layerm->perimeters.min_mm3_per_mm()); - if (region->config().get_abs_value("infill_speed" ) == 0 || - region->config().get_abs_value("solid_infill_speed" ) == 0 || - region->config().get_abs_value("top_solid_infill_speed" ) == 0 || - region->config().get_abs_value("bridge_speed" ) == 0) + if (region->config().get_abs_value("infill_speed") == 0 || + region->config().get_abs_value("solid_infill_speed") == 0 || + region->config().get_abs_value("top_solid_infill_speed") == 0 || + region->config().get_abs_value("bridge_speed") == 0) mm3_per_mm.push_back(layerm->fills.min_mm3_per_mm()); } } - if (object->config().get_abs_value("support_material_speed" ) == 0 || - object->config().get_abs_value("support_material_interface_speed" ) == 0) + if (object->config().get_abs_value("support_material_speed") == 0 || + object->config().get_abs_value("support_material_interface_speed") == 0) for (auto layer : object->support_layers()) mm3_per_mm.push_back(layer->support_fills.min_mm3_per_mm()); } print.throw_if_canceled(); // filter out 0-width segments mm3_per_mm.erase(std::remove_if(mm3_per_mm.begin(), mm3_per_mm.end(), [](double v) { return v < 0.000001; }), mm3_per_mm.end()); - if (! mm3_per_mm.empty()) { + if (!mm3_per_mm.empty()) { // In order to honor max_print_speed we need to find a target volumetric // speed that we can use throughout the print. So we define this target // volumetric speed as the volumetric speed produced by printing the @@ -973,7 +980,7 @@ void GCode::_do_export(Print &print, FILE *file) } } print.throw_if_canceled(); - + m_cooling_buffer = make_unique<CoolingBuffer>(*this); if (print.config().spiral_vase.value) m_spiral_vase = make_unique<SpiralVase>(print.config()); @@ -991,15 +998,15 @@ void GCode::_do_export(Print &print, FILE *file) #if ENABLE_THUMBNAIL_GENERATOR // Write thumbnails using base64 encoding - if (thumbnail_data != nullptr) + if (thumbnail_cb != nullptr) { const size_t max_row_length = 78; - - for (const ThumbnailData& data : *thumbnail_data) + ThumbnailsList thumbnails; + thumbnail_cb(thumbnails, print.full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true); + for (const ThumbnailData& data : thumbnails) { if (data.is_valid()) { -#if ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE size_t png_size = 0; void* png_data = tdefl_write_image_to_png_file_in_memory_ex((const void*)data.pixels.data(), data.width, data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); if (png_data != nullptr) @@ -1025,39 +1032,6 @@ void GCode::_do_export(Print &print, FILE *file) mz_free(png_data); } -#else - _write_format(file, "\n;\n; thumbnail begin %dx%d\n", data.width, data.height); - - size_t row_size = 4 * data.width; - for (int r = (int)data.height - 1; r >= 0; --r) - { - std::string encoded; - encoded.resize(boost::beast::detail::base64::encoded_size(row_size)); - encoded.resize(boost::beast::detail::base64::encode((void*)&encoded[0], (const void*)(data.pixels.data() + r * row_size), row_size)); - - unsigned int row_count = 0; - while (encoded.size() > max_row_length) - { - if (row_count == 0) - _write_format(file, "; %s\n", encoded.substr(0, max_row_length).c_str()); - else - _write_format(file, ";>%s\n", encoded.substr(0, max_row_length).c_str()); - - encoded = encoded.substr(max_row_length); - ++row_count; - } - - if (encoded.size() > 0) - { - if (row_count == 0) - _write_format(file, "; %s\n", encoded.c_str()); - else - _write_format(file, ";>%s\n", encoded.c_str()); - } - } - - _write(file, "; thumbnail end\n;\n"); -#endif // ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE } print.throw_if_canceled(); } @@ -1150,6 +1124,47 @@ void GCode::_do_export(Print &print, FILE *file) } print.throw_if_canceled(); + /* To avoid change filament for non-used extruder for Multi-material, + * check model->custom_gcode_per_height using tool_ordering values + * */ + if (!m_custom_g_code_heights. empty()) + { + bool delete_executed = false; + auto it = m_custom_g_code_heights.end(); + while (it != m_custom_g_code_heights.begin()) + { + --it; + if (it->gcode != ColorChangeCode) + continue; + + auto it_layer_tools = std::lower_bound(tool_ordering.begin(), tool_ordering.end(), LayerTools(it->height)); + + bool used_extruder = false; + for (; it_layer_tools != tool_ordering.end(); it_layer_tools++) + { + const std::vector<unsigned>& extruders = it_layer_tools->extruders; + if (std::find(extruders.begin(), extruders.end(), (unsigned)(it->extruder-1)) != extruders.end()) + { + used_extruder = true; + break; + } + } + if (used_extruder) + continue; + + /* If we are there, current extruder wouldn't be used, + * so this color change is a redundant move. + * Delete this item from m_custom_g_code_heights + * */ + it = m_custom_g_code_heights.erase(it); + delete_executed = true; + } + + if (delete_executed) + model->custom_gcode_per_height = m_custom_g_code_heights; + } + + m_cooling_buffer->set_current_extruder(initial_extruder_id); // Emit machine envelope limits for the Marlin firmware. @@ -1816,19 +1831,66 @@ void GCode::process_layer( // In case there are more toolchange requests that weren't done yet and should happen simultaneously, erase them all. // (Layers can be close to each other, model could have been resliced with bigger layer height, ...). bool colorprint_change = false; - while (!m_colorprint_heights.empty() && m_colorprint_heights.front()-EPSILON < layer.print_z) { - m_colorprint_heights.erase(m_colorprint_heights.begin()); + + std::string custom_code = ""; + std::string pause_print_msg = ""; + int m600_before_extruder = -1; + while (!m_custom_g_code_heights.empty() && m_custom_g_code_heights.front().height-EPSILON < layer.print_z) { + custom_code = m_custom_g_code_heights.front().gcode; + + if (custom_code == ColorChangeCode && m_custom_g_code_heights.front().extruder > 0) + m600_before_extruder = m_custom_g_code_heights.front().extruder - 1; + if (custom_code == PausePrintCode) + pause_print_msg = m_custom_g_code_heights.front().color; + + m_custom_g_code_heights.erase(m_custom_g_code_heights.begin()); colorprint_change = true; } // we should add or not colorprint_change in respect to nozzle_diameter count instead of really used extruders count - if (colorprint_change && print./*extruders()*/config().nozzle_diameter.size()==1) - { - // add tag for analyzer - gcode += "; " + GCodeAnalyzer::Color_Change_Tag + "\n"; - // add tag for time estimator - gcode += "; " + GCodeTimeEstimator::Color_Change_Tag + "\n"; - gcode += "M600\n"; + + // don't save "tool_change"(ExtruderChangeCode) code to GCode + if (colorprint_change && custom_code != ExtruderChangeCode) { + const bool single_material_print = print.config().nozzle_diameter.size() == 1; + + if (custom_code == ColorChangeCode) // color change + { + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Color_Change_Tag + ",T" + std::to_string(m600_before_extruder) + "\n"; + // add tag for time estimator + gcode += "; " + GCodeTimeEstimator::Color_Change_Tag + "\n"; + + if (!single_material_print && m600_before_extruder >= 0 && first_extruder_id != m600_before_extruder + // && !MMU1 + ) { + //! FIXME_in_fw show message during print pause + gcode += "M601\n"; // pause print + gcode += "M117 Change filament for Extruder " + std::to_string(m600_before_extruder) + "\n"; + } + else + gcode += custom_code + "\n"; + } + else + { + if (custom_code == PausePrintCode) // Pause print + { + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Pause_Print_Tag + "\n"; + //! FIXME_in_fw show message during print pause + if (!pause_print_msg.empty()) + gcode += "M117 " + pause_print_msg + "\n"; + // add tag for time estimator + //gcode += "; " + GCodeTimeEstimator::Pause_Print_Tag + "\n"; + } + else // custom Gcode + { + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Custom_Code_Tag + "\n"; + // add tag for time estimator + //gcode += "; " + GCodeTimeEstimator::Custom_Code_Tag + "\n"; + } + gcode += custom_code + "\n"; + } } @@ -2138,6 +2200,12 @@ void GCode::process_layer( if (m_cooling_buffer) gcode = m_cooling_buffer->process_layer(gcode, layer.id()); + // add tag for analyzer + if (gcode.find(GCodeAnalyzer::Pause_Print_Tag) != gcode.npos) + gcode += "\n; " + GCodeAnalyzer::End_Pause_Print_Or_Custom_Code_Tag + "\n"; + else if (gcode.find(GCodeAnalyzer::Custom_Code_Tag) != gcode.npos) + gcode += "\n; " + GCodeAnalyzer::End_Pause_Print_Or_Custom_Code_Tag + "\n"; + #ifdef HAS_PRESSURE_EQUALIZER // Apply pressure equalization if enabled; // printf("G-code before filter:\n%s\n", gcode.c_str()); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 19ec5be3c..7f009b814 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -17,6 +17,9 @@ #include "GCodeTimeEstimator.hpp" #include "EdgeGrid.hpp" #include "GCode/Analyzer.hpp" +#if ENABLE_THUMBNAIL_GENERATOR +#include "GCode/ThumbnailData.hpp" +#endif // ENABLE_THUMBNAIL_GENERATOR #include <memory> #include <string> @@ -30,9 +33,6 @@ namespace Slic3r { // Forward declarations. class GCode; class GCodePreviewData; -#if ENABLE_THUMBNAIL_GENERATOR -struct ThumbnailData; -#endif // ENABLE_THUMBNAIL_GENERATOR class AvoidCrossingPerimeters { public: @@ -167,7 +167,7 @@ public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). #if ENABLE_THUMBNAIL_GENERATOR - void do_export(Print* print, const char* path, GCodePreviewData* preview_data = nullptr, const std::vector<ThumbnailData>* thumbnail_data = nullptr); + void do_export(Print* print, const char* path, GCodePreviewData* preview_data = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); #else void do_export(Print *print, const char *path, GCodePreviewData *preview_data = nullptr); #endif // ENABLE_THUMBNAIL_GENERATOR @@ -199,7 +199,7 @@ public: protected: #if ENABLE_THUMBNAIL_GENERATOR - void _do_export(Print& print, FILE* file, const std::vector<ThumbnailData>* thumbnail_data); + void _do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thumbnail_cb); #else void _do_export(Print &print, FILE *file); #endif //ENABLE_THUMBNAIL_GENERATOR @@ -362,9 +362,12 @@ protected: bool m_second_layer_things_done; // Index of a last object copy extruded. std::pair<const PrintObject*, Point> m_last_obj_copy; - // Layer heights for colorprint - updated before the export and erased during the process - // so no toolchange occurs twice. - std::vector<float> m_colorprint_heights; + /* Extensions for colorprint - now it's not a just color_print_heights, + * there can be some custom gcode. + * Updated before the export and erased during the process, + * so no toolchange occurs twice. + * */ + std::vector<Model::CustomGCode> m_custom_g_code_heights; // Time estimators GCodeTimeEstimator m_normal_time_estimator; diff --git a/src/libslic3r/GCode/Analyzer.cpp b/src/libslic3r/GCode/Analyzer.cpp index 3f0b8735f..7b8004ab0 100644 --- a/src/libslic3r/GCode/Analyzer.cpp +++ b/src/libslic3r/GCode/Analyzer.cpp @@ -29,6 +29,9 @@ const std::string GCodeAnalyzer::Mm3_Per_Mm_Tag = "_ANALYZER_MM3_PER_MM:"; const std::string GCodeAnalyzer::Width_Tag = "_ANALYZER_WIDTH:"; const std::string GCodeAnalyzer::Height_Tag = "_ANALYZER_HEIGHT:"; const std::string GCodeAnalyzer::Color_Change_Tag = "_ANALYZER_COLOR_CHANGE"; +const std::string GCodeAnalyzer::Pause_Print_Tag = "_ANALYZER_PAUSE_PRINT"; +const std::string GCodeAnalyzer::Custom_Code_Tag = "_ANALYZER_CUSTOM_CODE"; +const std::string GCodeAnalyzer::End_Pause_Print_Or_Custom_Code_Tag = "_ANALYZER_END_PAUSE_PRINT_OR_CUSTOM_CODE"; const double GCodeAnalyzer::Default_mm3_per_mm = 0.0; const float GCodeAnalyzer::Default_Width = 0.0f; @@ -105,24 +108,11 @@ GCodeAnalyzer::GCodeMove::GCodeMove(GCodeMove::EType type, const GCodeAnalyzer:: { } -GCodeAnalyzer::GCodeAnalyzer() -{ - reset(); -} - -void GCodeAnalyzer::set_extruder_offsets(const GCodeAnalyzer::ExtruderOffsetsMap& extruder_offsets) -{ - m_extruder_offsets = extruder_offsets; -} - void GCodeAnalyzer::set_extruders_count(unsigned int count) { m_extruders_count = count; -} - -void GCodeAnalyzer::set_gcode_flavor(const GCodeFlavor& flavor) -{ - m_gcode_flavor = flavor; + for (unsigned int i=0; i<m_extruders_count; i++) + m_extruder_color[i] = i; } void GCodeAnalyzer::reset() @@ -147,6 +137,7 @@ void GCodeAnalyzer::reset() m_moves_map.clear(); m_extruder_offsets.clear(); m_extruders_count = 1; + m_extruder_color.clear(); } const std::string& GCodeAnalyzer::process_gcode(const std::string& gcode) @@ -595,7 +586,11 @@ void GCodeAnalyzer::_processT(const std::string& cmd) BOOST_LOG_TRIVIAL(error) << "GCodeAnalyzer encountered an invalid toolchange, maybe from a custom gcode."; } else + { _set_extruder_id(id); + if (_get_cp_color_id() != INT_MAX) + _set_cp_color_id(m_extruder_color[id]); + } // stores tool change move _store_move(GCodeMove::Tool_change); @@ -648,7 +643,33 @@ bool GCodeAnalyzer::_process_tags(const GCodeReader::GCodeLine& line) pos = comment.find(Color_Change_Tag); if (pos != comment.npos) { - _process_color_change_tag(); + pos = comment.find_last_of(",T"); + int extruder = pos == comment.npos ? 0 : std::atoi(comment.substr(pos + 1, comment.npos).c_str()); + _process_color_change_tag(extruder); + return true; + } + + // color change tag + pos = comment.find(Pause_Print_Tag); + if (pos != comment.npos) + { + _process_pause_print_or_custom_code_tag(); + return true; + } + + // color change tag + pos = comment.find(Custom_Code_Tag); + if (pos != comment.npos) + { + _process_pause_print_or_custom_code_tag(); + return true; + } + + // color change tag + pos = comment.find(End_Pause_Print_Or_Custom_Code_Tag); + if (pos != comment.npos) + { + _process_end_pause_print_or_custom_code_tag(); return true; } @@ -681,10 +702,24 @@ void GCodeAnalyzer::_process_height_tag(const std::string& comment, size_t pos) _set_height((float)::strtod(comment.substr(pos + Height_Tag.length()).c_str(), nullptr)); } -void GCodeAnalyzer::_process_color_change_tag() +void GCodeAnalyzer::_process_color_change_tag(int extruder) +{ + m_extruder_color[extruder] = m_extruders_count + m_state.cp_color_counter; // color_change position in list of color for preview + m_state.cp_color_counter++; + + if (_get_extruder_id() == extruder) + _set_cp_color_id(m_extruder_color[extruder]); +} + +void GCodeAnalyzer::_process_pause_print_or_custom_code_tag() +{ + _set_cp_color_id(INT_MAX); +} + +void GCodeAnalyzer::_process_end_pause_print_or_custom_code_tag() { - m_state.cur_cp_color_id++; - _set_cp_color_id(m_state.cur_cp_color_id); + if (_get_cp_color_id() == INT_MAX) + _set_cp_color_id(m_extruder_color[_get_extruder_id()]); } void GCodeAnalyzer::_set_units(GCodeAnalyzer::EUnits units) diff --git a/src/libslic3r/GCode/Analyzer.hpp b/src/libslic3r/GCode/Analyzer.hpp index df4f6f652..781c3f265 100644 --- a/src/libslic3r/GCode/Analyzer.hpp +++ b/src/libslic3r/GCode/Analyzer.hpp @@ -20,6 +20,9 @@ public: static const std::string Width_Tag; static const std::string Height_Tag; static const std::string Color_Change_Tag; + static const std::string Pause_Print_Tag; + static const std::string Custom_Code_Tag; + static const std::string End_Pause_Print_Or_Custom_Code_Tag; static const double Default_mm3_per_mm; static const float Default_Width; @@ -89,6 +92,7 @@ public: typedef std::vector<GCodeMove> GCodeMovesList; typedef std::map<GCodeMove::EType, GCodeMovesList> TypeToMovesMap; typedef std::map<unsigned int, Vec2d> ExtruderOffsetsMap; + typedef std::map<unsigned int, unsigned int> ExtruderToColorMap; private: struct State @@ -102,7 +106,7 @@ private: float start_extrusion; float position[Num_Axis]; float origin[Num_Axis]; - unsigned int cur_cp_color_id = 0; + unsigned int cp_color_counter = 0; }; private: @@ -113,16 +117,20 @@ private: unsigned int m_extruders_count; GCodeFlavor m_gcode_flavor; + ExtruderToColorMap m_extruder_color; + // The output of process_layer() std::string m_process_output; public: - GCodeAnalyzer(); + GCodeAnalyzer() { reset(); } - void set_extruder_offsets(const ExtruderOffsetsMap& extruder_offsets); + void set_extruder_offsets(const ExtruderOffsetsMap& extruder_offsets) { m_extruder_offsets = extruder_offsets; } void set_extruders_count(unsigned int count); - void set_gcode_flavor(const GCodeFlavor& flavor); + void set_extrusion_axis(char axis) { m_parser.set_extrusion_axis(axis); } + + void set_gcode_flavor(const GCodeFlavor& flavor) { m_gcode_flavor = flavor; } // Reinitialize the analyzer void reset(); @@ -212,7 +220,13 @@ private: void _process_height_tag(const std::string& comment, size_t pos); // Processes color change tag - void _process_color_change_tag(); + void _process_color_change_tag(int extruder); + + // Processes pause print and custom gcode tag + void _process_pause_print_or_custom_code_tag(); + + // Processes new layer tag + void _process_end_pause_print_or_custom_code_tag(); void _set_units(EUnits units); EUnits _get_units() const; diff --git a/src/libslic3r/GCode/PreviewData.cpp b/src/libslic3r/GCode/PreviewData.cpp index 53c13a2f2..e7475089a 100644 --- a/src/libslic3r/GCode/PreviewData.cpp +++ b/src/libslic3r/GCode/PreviewData.cpp @@ -379,7 +379,8 @@ std::string GCodePreviewData::get_legend_title() const return ""; } -GCodePreviewData::LegendItemsList GCodePreviewData::get_legend_items(const std::vector<float>& tool_colors, const std::vector</*double*/std::pair<double, double>>& cp_values) const +GCodePreviewData::LegendItemsList GCodePreviewData::get_legend_items(const std::vector<float>& tool_colors, + const std::vector<std::string>& cp_items) const { struct Helper { @@ -455,31 +456,25 @@ GCodePreviewData::LegendItemsList GCodePreviewData::get_legend_items(const std:: case Extrusion::ColorPrint: { const int color_cnt = (int)tool_colors.size()/4; - - const auto color_print_cnt = (int)cp_values.size(); - for (int i = color_print_cnt; i >= 0 ; --i) + const auto color_print_cnt = (int)cp_items.size(); + if (color_print_cnt == 1) // means "Default print color" { - GCodePreviewData::Color color; - ::memcpy((void*)color.rgba, (const void*)(tool_colors.data() + (i % color_cnt) * 4), 4 * sizeof(float)); - - if (color_print_cnt == 0) { - items.emplace_back(Slic3r::I18N::translate(L("Default print color")), color); - break; - } + Color color; + ::memcpy((void*)color.rgba, (const void*)(tool_colors.data()), 4 * sizeof(float)); - std::string id_str = std::to_string(i + 1) + ": "; + items.emplace_back(cp_items[0], color); + break; + } - if (i == 0) { - items.emplace_back(id_str + (boost::format(Slic3r::I18N::translate(L("up to %.2f mm"))) % cp_values[0].first).str(), color); - break; - } - if (i == color_print_cnt) { - items.emplace_back(id_str + (boost::format(Slic3r::I18N::translate(L("above %.2f mm"))) % cp_values[i - 1].second).str(), color); - continue; - } + if (color_cnt != color_print_cnt) + break; -// items.emplace_back((boost::format(Slic3r::I18N::translate(L("%.2f - %.2f mm"))) % cp_values[i-1] % cp_values[i]).str(), color); - items.emplace_back(id_str + (boost::format(Slic3r::I18N::translate(L("%.2f - %.2f mm"))) % cp_values[i - 1].second% cp_values[i].first).str(), color); + for (int i = 0 ; i < color_print_cnt; ++i) + { + Color color; + ::memcpy((void*)color.rgba, (const void*)(tool_colors.data() + i * 4), 4 * sizeof(float)); + + items.emplace_back(cp_items[i], color); } break; } diff --git a/src/libslic3r/GCode/PreviewData.hpp b/src/libslic3r/GCode/PreviewData.hpp index 70b6edffd..725c0258d 100644 --- a/src/libslic3r/GCode/PreviewData.hpp +++ b/src/libslic3r/GCode/PreviewData.hpp @@ -237,7 +237,7 @@ public: void set_extrusion_paths_colors(const std::vector<std::string>& colors); std::string get_legend_title() const; - LegendItemsList get_legend_items(const std::vector<float>& tool_colors, const std::vector</*double*/std::pair<double, double>>& cp_values) const; + LegendItemsList get_legend_items(const std::vector<float>& tool_colors, const std::vector<std::string>& cp_items) const; // Return an estimate of the memory consumed by the time estimator. size_t memory_used() const; diff --git a/src/libslic3r/GCode/ThumbnailData.hpp b/src/libslic3r/GCode/ThumbnailData.hpp index 9823ffd31..4acfe4374 100644 --- a/src/libslic3r/GCode/ThumbnailData.hpp +++ b/src/libslic3r/GCode/ThumbnailData.hpp @@ -4,6 +4,7 @@ #if ENABLE_THUMBNAIL_GENERATOR #include <vector> +#include "libslic3r/Point.hpp" namespace Slic3r { @@ -20,6 +21,9 @@ struct ThumbnailData bool is_valid() const; }; +typedef std::vector<ThumbnailData> ThumbnailsList; +typedef std::function<void(ThumbnailsList & thumbnails, const Vec2ds & sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background)> ThumbnailsGeneratorCallback; + } // namespace Slic3r #endif // ENABLE_THUMBNAIL_GENERATOR diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index cd326b9a0..5ee577482 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -1004,9 +1004,10 @@ void WipeTower::toolchange_Change( writer.append("[toolchange_gcode]\n"); // Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc) - // gcode could have left the extruder somewhere, we cannot just start extruding. - Vec2f current_pos = writer.pos_rotated(); - writer.append(std::string("G1 X") + std::to_string(current_pos.x()) + " Y" + std::to_string(current_pos.y()) + "\n"); + // gcode could have left the extruder somewhere, we cannot just start extruding. We should also inform the + // postprocessor that we absolutely want to have this in the gcode, even if it thought it is the same as before. + Vec2f current_pos = writer.pos_rotated(); + writer.append(std::string("G1 X") + std::to_string(current_pos.x()) + " Y" + std::to_string(current_pos.y()) + never_skip_tag() + "\n"); // The toolchange Tn command will be inserted later, only in case that the user does // not provide a custom toolchange gcode. diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 311490055..e6832958e 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -17,9 +17,12 @@ class PrintConfig; enum GCodeFlavor : unsigned char; + class WipeTower { public: + static char const* never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; } + struct Extrusion { Extrusion(const Vec2f &pos, float width, unsigned int tool) : pos(pos), width(width), tool(tool) {} @@ -96,6 +99,8 @@ public: + + // Switch to a next layer. void set_layer( // Print height of this layer. diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 24f979267..fea581e20 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -119,6 +119,7 @@ public: float f() const { return m_position[F]; } char extrusion_axis() const { return m_extrusion_axis; } + void set_extrusion_axis(char axis) { m_extrusion_axis = axis; } private: const char* parse_line_internal(const char *ptr, GCodeLine &gline, std::pair<const char*, const char*> &command); diff --git a/src/libslic3r/GCodeTimeEstimator.hpp b/src/libslic3r/GCodeTimeEstimator.hpp index 0219c87d1..bd997a047 100644 --- a/src/libslic3r/GCodeTimeEstimator.hpp +++ b/src/libslic3r/GCodeTimeEstimator.hpp @@ -342,6 +342,8 @@ namespace Slic3r { void increment_g1_line_id(); void reset_g1_line_id(); + void set_extrusion_axis(char axis) { m_parser.set_extrusion_axis(axis); } + void set_extruder_id(unsigned int id); unsigned int get_extruder_id() const; void reset_extruder_id(); diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index d996658f2..283d1edb0 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -137,6 +137,79 @@ inline bool segments_intersect( segments_could_intersect(jp1, jp2, ip1, ip2) <= 0; } +// Based on Liang-Barsky function by Daniel White @ http://www.skytopia.com/project/articles/compsci/clipping.html +template<typename T> +bool liang_barsky_line_clipping( + // Start and end points of the source line, result will be stored there as well. + Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x0, + Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x1, + // Bounding box to clip with. + const BoundingBoxBase<Eigen::Matrix<T, 2, 1, Eigen::DontAlign>> &bbox) +{ + Eigen::Matrix<T, 2, 1, Eigen::DontAlign> v = x1 - x0; + double t0 = 0.0; + double t1 = 1.0; + + // Traverse through left, right, bottom, top edges. + for (int edge = 0; edge < 4; ++ edge) + { + double p, q; + switch (edge) { + case 0: p = - v.x(); q = - bbox.min.x() + x0.x(); break; + case 1: p = v.x(); q = bbox.max.x() - x0.x(); break; + case 2: p = - v.y(); q = - bbox.min.y() + x0.y(); break; + default: p = v.y(); q = bbox.max.y() - x0.y(); break; + } + + if (p == 0) { + if (q < 0) + // Line parallel to the bounding box edge is fully outside of the bounding box. + return false; + // else don't clip + } else { + double r = q / p; + if (p < 0) { + if (r > t1) + // Fully clipped. + return false; + if (r > t0) + // Partially clipped. + t0 = r; + } else { + assert(p > 0); + if (r < t0) + // Fully clipped. + return false; + if (r < t1) + // Partially clipped. + t1 = r; + } + } + } + + // Clipped successfully. + x1 = x0 + t1 * v; + x0 += t0 * v; + return true; +} + +// Based on Liang-Barsky function by Daniel White @ http://www.skytopia.com/project/articles/compsci/clipping.html +template<typename T> +bool liang_barsky_line_clipping( + // Start and end points of the source line. + const Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x0src, + const Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x1src, + // Bounding box to clip with. + const BoundingBoxBase<Eigen::Matrix<T, 2, 1, Eigen::DontAlign>> &bbox, + // Start and end points of the clipped line. + Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x0clip, + Eigen::Matrix<T, 2, 1, Eigen::DontAlign> &x1clip) +{ + x0clip = x0src; + x1clip = x1src; + return liang_barsky_line_clipping(x0clip, x1clip, bbox); +} + Pointf3s convex_hull(Pointf3s points); Polygon convex_hull(Points points); Polygon convex_hull(const Polygons &polygons); diff --git a/src/libslic3r/Line.cpp b/src/libslic3r/Line.cpp index e5f7b8fa9..05cbfee4e 100644 --- a/src/libslic3r/Line.cpp +++ b/src/libslic3r/Line.cpp @@ -107,6 +107,17 @@ bool Line::intersection(const Line &l2, Point *intersection) const return false; // not intersecting } +bool Line::clip_with_bbox(const BoundingBox &bbox) +{ + Vec2d x0clip, x1clip; + bool result = Geometry::liang_barsky_line_clipping<double>(this->a.cast<double>(), this->b.cast<double>(), BoundingBoxf(bbox.min.cast<double>(), bbox.max.cast<double>()), x0clip, x1clip); + if (result) { + this->a = x0clip.cast<coord_t>(); + this->b = x1clip.cast<coord_t>(); + } + return result; +} + Vec3d Linef3::intersect_plane(double z) const { auto v = (this->b - this->a).cast<double>(); diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index 559ca946a..06809c02a 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -6,6 +6,7 @@ namespace Slic3r { +class BoundingBox; class Line; class Line3; class Linef3; @@ -43,6 +44,8 @@ public: Vector normal() const { return Vector((this->b(1) - this->a(1)), -(this->b(0) - this->a(0))); } bool intersection(const Line& line, Point* intersection) const; double ccw(const Point& point) const { return point.ccw(*this); } + // Clip a line with a bounding box. Returns false if the line is completely outside of the bounding box. + bool clip_with_bbox(const BoundingBox &bbox); static double distance_to_squared(const Point &point, const Point &a, const Point &b); static double distance_to(const Point &point, const Point &a, const Point &b) { return sqrt(distance_to_squared(point, a, b)); } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 9a26fbaaf..f218bfa6c 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -41,6 +41,9 @@ Model& Model::assign_copy(const Model &rhs) mo->set_model(this); this->objects.emplace_back(mo); } + + // copy custom code per height + this->custom_gcode_per_height = rhs.custom_gcode_per_height; return *this; } @@ -59,6 +62,9 @@ Model& Model::assign_copy(Model &&rhs) for (ModelObject *model_object : this->objects) model_object->set_model(this); rhs.objects.clear(); + + // copy custom code per height + this->custom_gcode_per_height = rhs.custom_gcode_per_height; return *this; } @@ -586,6 +592,22 @@ std::string Model::propose_export_file_name_and_path(const std::string &new_exte return boost::filesystem::path(this->propose_export_file_name_and_path()).replace_extension(new_extension).string(); } +std::vector<std::pair<double, DynamicPrintConfig>> Model::get_custom_tool_changes(double default_layer_height, size_t num_extruders) const +{ + std::vector<std::pair<double, DynamicPrintConfig>> custom_tool_changes; + if (!custom_gcode_per_height.empty()) { + for (const CustomGCode& custom_gcode : custom_gcode_per_height) + if (custom_gcode.gcode == ExtruderChangeCode) { + DynamicPrintConfig config; + // If extruder count in PrinterSettings was changed, use default (0) extruder for extruders, more than num_extruders + config.set_key_value("extruder", new ConfigOptionInt(custom_gcode.extruder > num_extruders ? 0 : custom_gcode.extruder)); + // For correct extruders(tools) changing, we should decrease custom_gcode.height value by one default layer height + custom_tool_changes.push_back({ custom_gcode.height - default_layer_height, config }); + } + } + return custom_tool_changes; +} + ModelObject::~ModelObject() { this->clear_volumes(); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index ccc48d52b..a9adbe0dc 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -749,6 +749,37 @@ public: ModelObjectPtrs objects; // Wipe tower object. ModelWipeTower wipe_tower; + + // Extensions for color print + struct CustomGCode + { + CustomGCode(double height, const std::string& code, int extruder, const std::string& color) : + height(height), gcode(code), extruder(extruder), color(color) {} + + bool operator<(const CustomGCode& other) const { return other.height > this->height; } + bool operator==(const CustomGCode& other) const + { + return (other.height == this->height) && + (other.gcode == this->gcode) && + (other.extruder == this->extruder )&& + (other.color == this->color ); + } + bool operator!=(const CustomGCode& other) const + { + return (other.height != this->height) || + (other.gcode != this->gcode) || + (other.extruder != this->extruder )|| + (other.color != this->color ); + } + + double height; + std::string gcode; + int extruder; // 0 - "gcode" will be applied for whole print + // else - "gcode" will be applied only for "extruder" print + std::string color; // if gcode is equal to PausePrintCode, + // this field is used for save a short message shown on Printer display + }; + std::vector<CustomGCode> custom_gcode_per_height; // Default constructor assigns a new ID to the model. Model() { assert(this->id().valid()); } @@ -814,6 +845,9 @@ public: // Propose an output path, replace extension. The new_extension shall contain the initial dot. std::string propose_export_file_name_and_path(const std::string &new_extension) const; + // from custom_gcode_per_height get just tool_change codes + std::vector<std::pair<double, DynamicPrintConfig>> get_custom_tool_changes(double default_layer_height, size_t num_extruders) const; + private: explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); }; void assign_new_unique_ids_recursive(); diff --git a/src/libslic3r/MotionPlanner.cpp b/src/libslic3r/MotionPlanner.cpp index 42bc6c2f1..45a80671c 100644 --- a/src/libslic3r/MotionPlanner.cpp +++ b/src/libslic3r/MotionPlanner.cpp @@ -319,7 +319,7 @@ Polyline MotionPlannerGraph::shortest_path(size_t node_start, size_t node_end) c std::vector<size_t> map_node_to_queue_id(m_adjacency_list.size(), size_t(-1)); distance[node_start] = 0.; - auto queue = make_mutable_priority_queue<node_t>( + auto queue = make_mutable_priority_queue<node_t, false>( [&map_node_to_queue_id](const node_t node, size_t idx) { map_node_to_queue_id[node] = idx; }, [&distance](const node_t node1, const node_t node2) { return distance[node1] < distance[node2]; }); queue.reserve(m_adjacency_list.size()); diff --git a/src/libslic3r/MutablePriorityQueue.hpp b/src/libslic3r/MutablePriorityQueue.hpp index da469b7ba..b20bf60ea 100644 --- a/src/libslic3r/MutablePriorityQueue.hpp +++ b/src/libslic3r/MutablePriorityQueue.hpp @@ -3,7 +3,7 @@ #include <assert.h> -template<typename T, typename IndexSetter, typename LessPredicate> +template<typename T, typename IndexSetter, typename LessPredicate, const bool ResetIndexWhenRemoved = false> class MutablePriorityQueue { public: @@ -42,26 +42,30 @@ private: LessPredicate m_less_predicate; }; -template<typename T, typename IndexSetter, typename LessPredicate> -MutablePriorityQueue<T, IndexSetter, LessPredicate> make_mutable_priority_queue(IndexSetter &&index_setter, LessPredicate &&less_predicate) +template<typename T, const bool ResetIndexWhenRemoved, typename IndexSetter, typename LessPredicate> +MutablePriorityQueue<T, IndexSetter, LessPredicate, ResetIndexWhenRemoved> make_mutable_priority_queue(IndexSetter &&index_setter, LessPredicate &&less_predicate) { - return MutablePriorityQueue<T, IndexSetter, LessPredicate>( + return MutablePriorityQueue<T, IndexSetter, LessPredicate, ResetIndexWhenRemoved>( std::forward<IndexSetter>(index_setter), std::forward<LessPredicate>(less_predicate)); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::clear() +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::clear() { -#ifndef NDEBUG - for (size_t idx = 0; idx < m_heap.size(); ++ idx) - // Mark as removed from the queue. - m_index_setter(m_heap[idx], std::numeric_limits<size_t>::max()); +#ifdef NDEBUG + // Only mark as removed from the queue in release mode, if configured so. + if (ResetIndexWhenRemoved) #endif /* NDEBUG */ + { + for (size_t idx = 0; idx < m_heap.size(); ++ idx) + // Mark as removed from the queue. + m_index_setter(m_heap[idx], std::numeric_limits<size_t>::max()); + } m_heap.clear(); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::push(const T &item) +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::push(const T &item) { size_t idx = m_heap.size(); m_heap.emplace_back(item); @@ -69,8 +73,8 @@ inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::push(const T &i update_heap_up(0, idx); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::push(T &&item) +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::push(T &&item) { size_t idx = m_heap.size(); m_heap.emplace_back(std::move(item)); @@ -78,14 +82,18 @@ inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::push(T &&item) update_heap_up(0, idx); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::pop() +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::pop() { assert(! m_heap.empty()); -#ifndef NDEBUG - // Mark as removed from the queue. - m_index_setter(m_heap.front(), std::numeric_limits<size_t>::max()); +#ifdef NDEBUG + // Only mark as removed from the queue in release mode, if configured so. + if (ResetIndexWhenRemoved) #endif /* NDEBUG */ + { + // Mark as removed from the queue. + m_index_setter(m_heap.front(), std::numeric_limits<size_t>::max()); + } if (m_heap.size() > 1) { m_heap.front() = m_heap.back(); m_heap.pop_back(); @@ -95,14 +103,18 @@ inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::pop() m_heap.clear(); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::remove(size_t idx) +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::remove(size_t idx) { assert(idx < m_heap.size()); -#ifndef NDEBUG - // Mark as removed from the queue. - m_index_setter(m_heap[idx], std::numeric_limits<size_t>::max()); +#ifdef NDEBUG + // Only mark as removed from the queue in release mode, if configured so. + if (ResetIndexWhenRemoved) #endif /* NDEBUG */ + { + // Mark as removed from the queue. + m_index_setter(m_heap[idx], std::numeric_limits<size_t>::max()); + } if (idx + 1 == m_heap.size()) { m_heap.pop_back(); return; @@ -114,8 +126,8 @@ inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::remove(size_t i update_heap_up(0, idx); } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::update_heap_up(size_t top, size_t bottom) +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::update_heap_up(size_t top, size_t bottom) { size_t childIdx = bottom; T *child = &m_heap[childIdx]; @@ -138,8 +150,8 @@ inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::update_heap_up( } } -template<class T, class LessPredicate, class IndexSetter> -inline void MutablePriorityQueue<T, LessPredicate, IndexSetter>::update_heap_down(size_t top, size_t bottom) +template<class T, class LessPredicate, class IndexSetter, const bool ResetIndexWhenRemoved> +inline void MutablePriorityQueue<T, LessPredicate, IndexSetter, ResetIndexWhenRemoved>::update_heap_down(size_t top, size_t bottom) { size_t parentIdx = top; T *parent = &m_heap[parentIdx]; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 78a09c511..ae32bd2d3 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -637,11 +637,59 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ else m_ranges.emplace_back(t_layer_height_range(m_ranges.back().first.second, DBL_MAX), nullptr); } + + // Convert input config ranges into continuous non-overlapping sorted vector of intervals and their configs, + // considering custom_tool_change values + void assign(const t_layer_config_ranges &in, const std::vector<std::pair<double, DynamicPrintConfig>> &custom_tool_changes) { + m_ranges.clear(); + m_ranges.reserve(in.size()); + // Input ranges are sorted lexicographically. First range trims the other ranges. + coordf_t last_z = 0; + for (const std::pair<const t_layer_height_range, DynamicPrintConfig> &range : in) + if (range.first.second > last_z) { + coordf_t min_z = std::max(range.first.first, 0.); + if (min_z > last_z + EPSILON) { + m_ranges.emplace_back(t_layer_height_range(last_z, min_z), nullptr); + last_z = min_z; + } + if (range.first.second > last_z + EPSILON) { + const DynamicPrintConfig* cfg = &range.second; + m_ranges.emplace_back(t_layer_height_range(last_z, range.first.second), cfg); + last_z = range.first.second; + } + } + + // add ranges for extruder changes from custom_tool_changes + for (size_t i = 0; i < custom_tool_changes.size(); i++) { + const DynamicPrintConfig* cfg = &custom_tool_changes[i].second; + coordf_t cur_Z = custom_tool_changes[i].first; + coordf_t next_Z = i == custom_tool_changes.size()-1 ? DBL_MAX : custom_tool_changes[i+1].first; + if (cur_Z > last_z + EPSILON) { + if (i==0) + m_ranges.emplace_back(t_layer_height_range(last_z, cur_Z), nullptr); + m_ranges.emplace_back(t_layer_height_range(cur_Z, next_Z), cfg); + } + else if (next_Z > last_z + EPSILON) + m_ranges.emplace_back(t_layer_height_range(last_z, next_Z), cfg); + } + + if (m_ranges.empty()) + m_ranges.emplace_back(t_layer_height_range(0, DBL_MAX), nullptr); + else if (m_ranges.back().second == nullptr) + m_ranges.back().first.second = DBL_MAX; + else if (m_ranges.back().first.second != DBL_MAX) + m_ranges.emplace_back(t_layer_height_range(m_ranges.back().first.second, DBL_MAX), nullptr); + } const DynamicPrintConfig* config(const t_layer_height_range &range) const { auto it = std::lower_bound(m_ranges.begin(), m_ranges.end(), std::make_pair< t_layer_height_range, const DynamicPrintConfig*>(t_layer_height_range(range.first - EPSILON, range.second - EPSILON), nullptr)); - assert(it != m_ranges.end()); - assert(it == m_ranges.end() || std::abs(it->first.first - range.first ) < EPSILON); - assert(it == m_ranges.end() || std::abs(it->first.second - range.second) < EPSILON); + // #ys_FIXME_COLOR + // assert(it != m_ranges.end()); + // assert(it == m_ranges.end() || std::abs(it->first.first - range.first ) < EPSILON); + // assert(it == m_ranges.end() || std::abs(it->first.second - range.second) < EPSILON); + if (it == m_ranges.end() || + std::abs(it->first.first - range.first) > EPSILON || + std::abs(it->first.second - range.second) > EPSILON ) + return nullptr; // desired range doesn't found return (it == m_ranges.end()) ? nullptr : it->second; } std::vector<std::pair<t_layer_height_range, const DynamicPrintConfig*>>::const_iterator begin() const { return m_ranges.cbegin(); } @@ -689,6 +737,13 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // The object list did not change. for (const ModelObject *model_object : m_model.objects) model_object_status.emplace(model_object->id(), ModelObjectStatus::Old); + + // But if custom gcode per layer height was changed + if (m_model.custom_gcode_per_height != model.custom_gcode_per_height) { + // we should stop background processing + update_apply_status(this->invalidate_step(psGCodeExport)); + m_model.custom_gcode_per_height = model.custom_gcode_per_height; + } } else if (model_object_list_extended(m_model, model)) { // Add new objects. Their volumes and configs will be synchronized later. update_apply_status(this->invalidate_step(psGCodeExport)); @@ -780,6 +835,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ for (PrintObject *print_object : m_objects) print_object_status.emplace(PrintObjectStatus(print_object)); + std::vector<std::pair<double, DynamicPrintConfig>> custom_tool_changes = + m_model.get_custom_tool_changes(m_default_object_config.layer_height, num_extruders); + // 3) Synchronize ModelObjects & PrintObjects. for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) { ModelObject &model_object = *m_model.objects[idx_model_object]; @@ -787,7 +845,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ assert(it_status != model_object_status.end()); assert(it_status->status != ModelObjectStatus::Deleted); const ModelObject& model_object_new = *model.objects[idx_model_object]; - const_cast<ModelObjectStatus&>(*it_status).layer_ranges.assign(model_object_new.layer_config_ranges); + // ys_FIXME_COLOR + // const_cast<ModelObjectStatus&>(*it_status).layer_ranges.assign(model_object_new.layer_config_ranges); + const_cast<ModelObjectStatus&>(*it_status).layer_ranges.assign(model_object_new.layer_config_ranges, custom_tool_changes); if (it_status->status == ModelObjectStatus::New) // PrintObject instances will be added in the next loop. continue; @@ -955,6 +1015,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintRegionConfig this_region_config; bool this_region_config_set = false; for (PrintObject *print_object : m_objects) { + if(m_force_update_print_regions && !custom_tool_changes.empty()) + goto print_object_end; const LayerRanges *layer_ranges; { auto it_status = model_object_status.find(ModelObjectStatus(print_object->model_object()->id())); @@ -1190,6 +1252,8 @@ std::string Print::validate() const return L("Ooze prevention is currently not supported with the wipe tower enabled."); if (m_config.use_volumetric_e) return L("The Wipe Tower currently does not support volumetric E (use_volumetric_e=0)."); + if (m_config.complete_objects && extruders().size() > 1) + return L("The Wipe Tower is currently not supported for multimaterial sequential prints."); if (m_objects.size() > 1) { bool has_custom_layering = false; @@ -1258,7 +1322,7 @@ std::string Print::validate() const } while (ref_z == next_ref_z); } if (std::abs(this_height - ref_height) > EPSILON) - return L("The Wipe tower is only supported if all objects have the same layer height profile"); + return L("The Wipe tower is only supported if all objects have the same variable layer height"); i += 2; } } @@ -1538,7 +1602,7 @@ void Print::process() // write error into the G-code, cannot execute post-processing scripts). // It is up to the caller to show an error message. #if ENABLE_THUMBNAIL_GENERATOR -std::string Print::export_gcode(const std::string& path_template, GCodePreviewData* preview_data, const std::vector<ThumbnailData>* thumbnail_data) +std::string Print::export_gcode(const std::string& path_template, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb) #else std::string Print::export_gcode(const std::string &path_template, GCodePreviewData *preview_data) #endif // ENABLE_THUMBNAIL_GENERATOR @@ -1559,7 +1623,7 @@ std::string Print::export_gcode(const std::string &path_template, GCodePreviewDa // The following line may die for multiple reasons. GCode gcode; #if ENABLE_THUMBNAIL_GENERATOR - gcode.do_export(this, path.c_str(), preview_data, thumbnail_data); + gcode.do_export(this, path.c_str(), preview_data, thumbnail_cb); #else gcode.do_export(this, path.c_str(), preview_data); #endif // ENABLE_THUMBNAIL_GENERATOR @@ -1618,11 +1682,18 @@ void Print::_make_skirt() if (has_wipe_tower() && ! m_wipe_tower_data.tool_changes.empty()) { double width = m_config.wipe_tower_width + 2*m_wipe_tower_data.brim_width; double depth = m_wipe_tower_data.depth + 2*m_wipe_tower_data.brim_width; - Vec2d pt = Vec2d(m_config.wipe_tower_x-m_wipe_tower_data.brim_width, m_config.wipe_tower_y-m_wipe_tower_data.brim_width); - points.push_back(Point(scale_(pt.x()), scale_(pt.y()))); - points.push_back(Point(scale_(pt.x()+width), scale_(pt.y()))); - points.push_back(Point(scale_(pt.x()+width), scale_(pt.y()+depth))); - points.push_back(Point(scale_(pt.x()), scale_(pt.y()+depth))); + Vec2d pt = Vec2d(-m_wipe_tower_data.brim_width, -m_wipe_tower_data.brim_width); + + std::vector<Vec2d> pts; + pts.push_back(Vec2d(pt.x(), pt.y())); + pts.push_back(Vec2d(pt.x()+width, pt.y())); + pts.push_back(Vec2d(pt.x()+width, pt.y()+depth)); + pts.push_back(Vec2d(pt.x(), pt.y()+depth)); + for (Vec2d& pt : pts) { + pt = Eigen::Rotation2Dd(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value)) * pt; + pt += Vec2d(m_config.wipe_tower_x.value, m_config.wipe_tower_y.value); + points.push_back(Point(scale_(pt.x()), scale_(pt.y()))); + } } if (points.size() < 3) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 4fcd67166..098049f1d 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -11,6 +11,9 @@ #include "Slicing.hpp" #include "GCode/ToolOrdering.hpp" #include "GCode/WipeTower.hpp" +#if ENABLE_THUMBNAIL_GENERATOR +#include "GCode/ThumbnailData.hpp" +#endif // ENABLE_THUMBNAIL_GENERATOR namespace Slic3r { @@ -19,9 +22,6 @@ class PrintObject; class ModelObject; class GCode; class GCodePreviewData; -#if ENABLE_THUMBNAIL_GENERATOR -struct ThumbnailData; -#endif // ENABLE_THUMBNAIL_GENERATOR // Print step IDs for keeping track of the print state. enum PrintStep { @@ -311,7 +311,7 @@ public: // Exports G-code into a file name based on the path_template, returns the file path of the generated G-code file. // If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r). #if ENABLE_THUMBNAIL_GENERATOR - std::string export_gcode(const std::string& path_template, GCodePreviewData* preview_data, const std::vector<ThumbnailData>* thumbnail_data = nullptr); + std::string export_gcode(const std::string& path_template, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); #else std::string export_gcode(const std::string &path_template, GCodePreviewData *preview_data); #endif // ENABLE_THUMBNAIL_GENERATOR @@ -370,6 +370,9 @@ public: // Accessed by SupportMaterial const PrintRegion* get_region(size_t idx) const { return m_regions[idx]; } + // force update of PrintRegions, when custom_tool_change is not empty and (Re)Slicing is started + void set_force_update_print_regions(bool force_update_print_regions) { m_force_update_print_regions = force_update_print_regions; } + protected: // methods for handling regions PrintRegion* get_region(size_t idx) { return m_regions[idx]; } @@ -412,6 +415,9 @@ private: // Estimated print time, filament consumed. PrintStatistics m_print_statistics; + // flag used + bool m_force_update_print_regions = false; + // To allow GCode to set the Print's GCodeExport step status. friend class GCode; // Allow PrintObject to access m_mutex and m_cancel_callback. diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ac822b525..20fb5d08a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -71,6 +71,12 @@ enum SLAPillarConnectionMode { slapcmDynamic }; +// ys_FIXME ! may be, it's not a best place +// Additional Codes which can be set by user using DoubleSlider +static const std::string ColorChangeCode = "M600"; +static const std::string PausePrintCode = "M601"; +static const std::string ExtruderChangeCode = "tool_change"; + template<> inline const t_config_enum_values& ConfigOptionEnum<PrinterTechnology>::get_enum_values() { static t_config_enum_values keys_map; if (keys_map.empty()) { diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c4ca46a8c..bf97baaf6 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1522,9 +1522,9 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_object, c layer_height_profile.clear(); if (layer_height_profile.empty()) { - //layer_height_profile = layer_height_profile_adaptive(slicing_parameters, model_object.layer_config_ranges, model_object.volumes); - layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, model_object.layer_config_ranges); - updated = true; + //layer_height_profile = layer_height_profile_adaptive(slicing_parameters, model_object.layer_config_ranges, model_object.volumes); + layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, model_object.layer_config_ranges); + updated = true; } return updated; } diff --git a/src/libslic3r/Semver.hpp b/src/libslic3r/Semver.hpp index a755becaa..172391077 100644 --- a/src/libslic3r/Semver.hpp +++ b/src/libslic3r/Semver.hpp @@ -114,6 +114,7 @@ public: bool operator&(const Semver &b) const { return ::semver_satisfies_patch(ver, b.ver) != 0; } bool operator^(const Semver &b) const { return ::semver_satisfies_caret(ver, b.ver) != 0; } bool in_range(const Semver &low, const Semver &high) const { return low <= *this && *this <= high; } + bool valid() const { return *this != zero() && *this != inf() && *this != invalid(); } // Conversion std::string to_string() const { diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index b38655e68..8b5cbf483 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -191,7 +191,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals } // Initialize a heap of end points sorted by the lowest distance to the next valid point of a path. - auto queue = make_mutable_priority_queue<EndPoint*>( + auto queue = make_mutable_priority_queue<EndPoint*, false>( [](EndPoint *ep, size_t idx){ ep->heap_idx = idx; }, [](EndPoint *l, EndPoint *r){ return l->distance_out < r->distance_out; }); queue.reserve(end_points.size() * 2 - 1); @@ -389,6 +389,585 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals return out; } +template<typename QueueType, typename KDTreeType, typename ChainsType, typename EndPointType> +void update_end_point_in_queue(QueueType &queue, const KDTreeType &kdtree, ChainsType &chains, std::vector<EndPointType> &end_points, EndPointType &end_point, size_t first_point_idx, const EndPointType *first_point) +{ + // Updating an end point or a 2nd from an end point. + size_t this_idx = end_point.index(end_points); + // If this segment is not the starting segment, then this end point or the opposite is unconnected. + assert(first_point_idx == this_idx || first_point_idx == (this_idx ^ 1) || end_point.chain_id == 0 || end_point.opposite(end_points).chain_id == 0); + end_point.edge_candidate = nullptr; + if (first_point_idx == this_idx || (end_point.chain_id > 0 && first_point_idx == (this_idx ^ 1))) + { + // One may never flip the 1st edge, don't try it again. + if (! end_point.heap_idx_invalid()) + queue.remove(end_point.heap_idx); + } + else + { + // Update edge_candidate and distance. + size_t chain1a = end_point.chain_id; + size_t chain1b = end_points[this_idx ^ 1].chain_id; + size_t this_chain = chains.equivalent(std::max(chain1a, chain1b)); + // Find the closest point to this end_point, which lies on a different extrusion path (filtered by the filter lambda). + size_t next_idx = find_closest_point(kdtree, end_point.pos, [&end_points, &chains, this_idx, first_point_idx, first_point, this_chain](size_t idx) { + assert(end_points[this_idx].edge_candidate == nullptr); + // Either this end of the edge or the other end of the edge is not yet connected. + assert((end_points[this_idx ].chain_id == 0 && end_points[this_idx ].edge_out == nullptr) || + (end_points[this_idx ^ 1].chain_id == 0 && end_points[this_idx ^ 1].edge_out == nullptr)); + if ((idx ^ this_idx) <= 1 || idx == first_point_idx) + // Points of the same segment shall not be connected. + // Don't connect to the first point, we must not flip the 1st edge. + return false; + size_t chain2a = end_points[idx].chain_id; + size_t chain2b = end_points[idx ^ 1].chain_id; + if (chain2a > 0 && chain2b > 0) + // Only unconnected end point or a point next to an unconnected end point may be connected to. + // Ideally those would be removed from the KD tree, but the update is difficult. + return false; + assert(chain2a == 0 || chain2b == 0); + size_t chain2 = chains.equivalent(std::max(chain2a, chain2b)); + if (this_chain == chain2) + // Don't connect back to the same chain, don't create a loop. + return this_chain == 0; + // Don't connect to a segment requiring flipping if the segment starts or ends with the first point. + if (chain2a > 0) { + // Chain requires flipping. + assert(chain2b == 0); + auto &chain = chains.chain(chain2); + if (chain.begin == first_point || chain.end == first_point) + return false; + } + // Everything is all right, try to connect. + return true; + }); + assert(next_idx < end_points.size()); + assert(chains.equivalent(end_points[next_idx].chain_id) != chains.equivalent(end_points[next_idx ^ 1].chain_id) || end_points[next_idx].chain_id == 0); + end_point.edge_candidate = &end_points[next_idx]; + end_point.distance_out = (end_points[next_idx].pos - end_point.pos).norm(); + if (end_point.chain_id > 0) + end_point.distance_out += chains.chain_flip_penalty(this_chain); + if (end_points[next_idx].chain_id > 0) + // The candidate chain is flipped. + end_point.distance_out += chains.chain_flip_penalty(end_points[next_idx].chain_id); + // Update position of this end point in the queue based on the distance calculated at the line above. + if (end_point.heap_idx_invalid()) + queue.push(&end_point); + else + queue.update(end_point.heap_idx); + } +} + +template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc> +std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals2_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near) +{ + std::vector<std::pair<size_t, bool>> out; + + if (num_segments == 0) { + // Nothing to do. + } + else if (num_segments == 1) + { + // Just sort the end points so that the first point visited is closest to start_near. + out.emplace_back(0, start_near != nullptr && + (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, false) - *start_near).template cast<double>().squaredNorm()); + } + else + { + // End points of segments for the KD tree closest point search. + // A single end point is inserted into the search structure for loops, two end points are entered for open paths. + struct EndPoint { + EndPoint(const Vec2d &pos) : pos(pos) {} + Vec2d pos; + + // Candidate for a new connection link. + EndPoint *edge_candidate = nullptr; + // Distance to the next end point following the link. + // Zero value -> start of the final path. + double distance_out = std::numeric_limits<double>::max(); + + size_t heap_idx = std::numeric_limits<size_t>::max(); + bool heap_idx_invalid() const { return this->heap_idx == std::numeric_limits<size_t>::max(); } + + // Identifier of the chain, to which this end point belongs. Zero means unassigned. + size_t chain_id = 0; + // Double linked chain of segment end points in current path. + EndPoint *edge_out = nullptr; + + size_t index(std::vector<EndPoint> &endpoints) const { return this - endpoints.data(); } + // Opposite end point of the same segment. + EndPoint& opposite(std::vector<EndPoint> &endpoints) { return endpoints[(this - endpoints.data()) ^ 1]; } + const EndPoint& opposite(const std::vector<EndPoint> &endpoints) const { return endpoints[(this - endpoints.data()) ^ 1]; } + }; + + std::vector<EndPoint> end_points; + end_points.reserve(num_segments * 2); + for (size_t i = 0; i < num_segments; ++ i) { + end_points.emplace_back(end_point_func(i, true ).template cast<double>()); + end_points.emplace_back(end_point_func(i, false).template cast<double>()); + } + + // Construct the closest point KD tree over end points of segments. + auto coordinate_fn = [&end_points](size_t idx, size_t dimension) -> double { return end_points[idx].pos[dimension]; }; + KDTreeIndirect<2, double, decltype(coordinate_fn)> kdtree(coordinate_fn, end_points.size()); + + // Chained segments with their sum of connection lengths. + // The chain supports flipping all the segments, connecting the segments at the opposite ends. + // (this is a very useful path optimization for infill lines). + struct Chain { + size_t num_segments = 0; + double cost = 0.; + double cost_flipped = 0.; + EndPoint *begin = nullptr; + EndPoint *end = nullptr; + size_t equivalent_with = 0; + + // Flipping the chain has a time complexity of O(n). + void flip(std::vector<EndPoint> &endpoints) + { + assert(this->num_segments > 1); + assert(this->begin->edge_out == nullptr); + assert(this->end ->edge_out == nullptr); + assert(this->begin->opposite(endpoints).edge_out != nullptr); + assert(this->end ->opposite(endpoints).edge_out != nullptr); + // Start of the current segment processed. + EndPoint *ept = this->begin; + // Previous end point to connect the other side of ept to. + EndPoint *ept_prev = nullptr; + do { + EndPoint *ept_end = &ept->opposite(endpoints); + EndPoint *ept_next = ept_end->edge_out; + assert(ept_next == nullptr || ept_next->edge_out == ept_end); + // Connect to the preceding segment. + ept_end->edge_out = ept_prev; + if (ept_prev != nullptr) + ept_prev->edge_out = ept_end; + ept_prev = ept; + ept = ept_next; + } while (ept != nullptr); + ept_prev->edge_out = nullptr; + // Swap the costs. + std::swap(this->cost, this->cost_flipped); + // Swap the ends. + EndPoint *new_begin = &this->begin->opposite(endpoints); + EndPoint *new_end = &this->end->opposite(endpoints); + std::swap(this->begin->chain_id, new_begin->chain_id); + std::swap(this->end ->chain_id, new_end ->chain_id); + this->begin = new_begin; + this->end = new_end; + assert(this->begin->edge_out == nullptr); + assert(this->end ->edge_out == nullptr); + assert(this->begin->opposite(endpoints).edge_out != nullptr); + assert(this->end ->opposite(endpoints).edge_out != nullptr); + } + + double flip_penalty() const { return this->cost_flipped - this->cost; } + }; + + // Helper to detect loops in already connected paths and to accomodate flipping of chains. + // + // Unique chain IDs are assigned to paths. If paths are connected, end points will not have their chain IDs updated, but the chain IDs + // will remember an "equivalent" chain ID, which is the lowest ID of all the IDs in the path, and the lowest ID is equivalent to itself. + // Chain IDs are indexed starting with 1. + // + // Chains remember their lengths and their lengths when each segment of the chain is flipped. + class Chains { + public: + // Zero'th chain ID is invalid. + Chains(size_t reserve) { + m_chains.reserve(reserve / 2); + // Indexing starts with 1. + m_chains.emplace_back(); + } + + // Generate next equivalence class. + size_t next_id() { + m_chains.emplace_back(); + m_chains.back().equivalent_with = ++ m_last_chain_id; + return m_last_chain_id; + } + + // Get equivalence class for chain ID, update the "equivalent_with" along the equivalence path. + size_t equivalent(size_t chain_id) { + if (chain_id != 0) { + for (size_t last = chain_id;;) { + size_t lower = m_chains[last].equivalent_with; + if (lower == last) { + m_chains[chain_id].equivalent_with = lower; + chain_id = lower; + break; + } + last = lower; + } + } + return chain_id; + } + + // Return a lowest chain ID of the two input chains. + // Produce a new chain ID of both chain IDs are zero. + size_t merge(size_t chain_id1, size_t chain_id2) { + if (chain_id1 == 0) + return (chain_id2 == 0) ? this->next_id() : chain_id2; + if (chain_id2 == 0) + return chain_id1; + assert(m_chains[chain_id1].equivalent_with == chain_id1); + assert(m_chains[chain_id2].equivalent_with == chain_id2); + size_t chain_id = std::min(chain_id1, chain_id2); + m_chains[chain_id1].equivalent_with = chain_id; + m_chains[chain_id2].equivalent_with = chain_id; + return chain_id; + } + + Chain& chain(size_t chain_id) { return m_chains[chain_id]; } + const Chain& chain(size_t chain_id) const { return m_chains[chain_id]; } + + double chain_flip_penalty(size_t chain_id) { + chain_id = this->equivalent(chain_id); + return m_chains[chain_id].flip_penalty(); + } + +#ifndef NDEBUG + bool validate() + { + // Validate that the segments merged chain IDs make up a directed acyclic graph + // with edges oriented towards the lower chain ID, therefore all ending up + // in the lowest chain ID of all of them. + assert(m_last_chain_id >= 0); + assert(m_last_chain_id + 1 == m_chains.size()); + for (size_t i = 0; i < m_chains.size(); ++ i) { + for (size_t last = i;;) { + size_t lower = m_chains[last].equivalent_with; + assert(lower <= last); + if (lower == last) + break; + last = lower; + } + } + return true; + } +#endif /* NDEBUG */ + + private: + std::vector<Chain> m_chains; + // Unique chain ID assigned to chains of end points of segments. + size_t m_last_chain_id = 0; + } chains(num_segments); + + // Find the first end point closest to start_near. + EndPoint *first_point = nullptr; + size_t first_point_idx = std::numeric_limits<size_t>::max(); + if (start_near != nullptr) { + size_t idx = find_closest_point(kdtree, start_near->template cast<double>()); + assert(idx < end_points.size()); + first_point = &end_points[idx]; + first_point->distance_out = 0.; + first_point->chain_id = chains.next_id(); + Chain &chain = chains.chain(first_point->chain_id); + chain.begin = first_point; + chain.end = &first_point->opposite(end_points); + first_point_idx = idx; + } + EndPoint *initial_point = first_point; + EndPoint *last_point = nullptr; + + // Assign the closest point and distance to the end points. + for (EndPoint &end_point : end_points) { + assert(end_point.edge_candidate == nullptr); + if (&end_point != first_point) { + size_t this_idx = end_point.index(end_points); + // Find the closest point to this end_point, which lies on a different extrusion path (filtered by the lambda). + // Ignore the starting point as the starting point is considered to be occupied, no end point coud connect to it. + size_t next_idx = find_closest_point(kdtree, end_point.pos, + [this_idx, first_point_idx](size_t idx){ return idx != first_point_idx && (idx ^ this_idx) > 1; }); + assert(next_idx < end_points.size()); + EndPoint &end_point2 = end_points[next_idx]; + end_point.edge_candidate = &end_point2; + end_point.distance_out = (end_point2.pos - end_point.pos).norm(); + } + } + + // Initialize a heap of end points sorted by the lowest distance to the next valid point of a path. + auto queue = make_mutable_priority_queue<EndPoint*, true>( + [](EndPoint *ep, size_t idx){ ep->heap_idx = idx; }, + [](EndPoint *l, EndPoint *r){ return l->distance_out < r->distance_out; }); + queue.reserve(end_points.size() * 2); + for (EndPoint &ep : end_points) + if (first_point != &ep) + queue.push(&ep); + +#ifndef NDEBUG + auto validate_graph_and_queue = [&chains, &end_points, &queue, first_point]() -> bool { + assert(chains.validate()); + for (EndPoint &ep : end_points) { + if (ep.heap_idx < queue.size()) { + // End point is on the heap. + assert(*(queue.cbegin() + ep.heap_idx) == &ep); + // One side or the other of the segment is not yet connected. + assert(ep.chain_id == 0 || ep.opposite(end_points).chain_id == 0); + } else { + // End point is NOT on the heap, therefore it must part of the output path. + assert(ep.heap_idx_invalid()); + assert(ep.chain_id != 0); + if (&ep == first_point) { + assert(ep.edge_out == nullptr); + } else { + assert(ep.edge_out != nullptr); + // Detect loops. + for (EndPoint *pt = &ep; pt != nullptr;) { + // Out of queue. It is a final point. + EndPoint *pt_other = &pt->opposite(end_points); + if (pt_other->heap_idx < queue.size()) { + // The other side of this segment is undecided yet. + // assert(pt_other->edge_out == nullptr); + break; + } + pt = pt_other->edge_out; + } + } + } + } + for (EndPoint *ep : queue) + // Points in the queue or the opposites of the same segment are not connected yet. + assert(ep->chain_id == 0 || ep->opposite(end_points).chain_id == 0); + return true; + }; +#endif /* NDEBUG */ + + // Chain the end points: find (num_segments - 1) shortest links not forming bifurcations or loops. + assert(num_segments >= 2); +#ifndef NDEBUG + double distance_taken_last = 0.; +#endif /* NDEBUG */ + // Some links stored onto the priority queue are being invalidated during the calculation and they are not + // updated immediately. If such a situation is detected for an end point pulled from the priority queue, + // the end point is being updated and re-inserted into the priority queue. Therefore the number of iterations + // required is higher than expected (it would be the number of links, num_segments - 1). + // The limit here may not be necessary, but it guards us against an endless loop if something goes wrong. + size_t num_iter = num_segments * 16; + for (size_t num_connections_to_end = num_segments - 1; num_iter > 0; -- num_iter) { + assert(validate_graph_and_queue()); + // Take the first end point, for which the link points to the currently closest valid neighbor. + EndPoint *end_point1 = queue.top(); + assert(end_point1 != first_point); + EndPoint *end_point1_other = &end_point1->opposite(end_points); + // true if end_point1 is not the end of its chain, but the 2nd point. When connecting to the 2nd point, this chain needs + // to be flipped first. + bool chain1_flip = end_point1->chain_id > 0; + // Either this point at the queue is not connected, or it is the 2nd point of a chain. + // If connecting to a 2nd point of a chain, the 1st point shall not yet be connected and this chain will need + // to be flipped. + assert( chain1_flip || (end_point1->chain_id == 0 && end_point1->edge_out == nullptr)); + assert(! chain1_flip || (end_point1_other->chain_id == 0 && end_point1_other->edge_out == nullptr)); + assert(end_point1->edge_candidate != nullptr); +#ifndef NDEBUG + // Each edge added shall be longer than the previous one taken. + //assert(end_point1->distance_out > distance_taken_last - SCALED_EPSILON); + if (end_point1->distance_out < distance_taken_last - SCALED_EPSILON) { +// printf("Warning: taking shorter length than previously is suspicious\n"); + } + distance_taken_last = end_point1->distance_out; +#endif /* NDEBUG */ + // Take the closest end point to the first end point, + EndPoint *end_point2 = end_point1->edge_candidate; + EndPoint *end_point2_other = &end_point2->opposite(end_points); + bool chain2_flip = end_point2->chain_id > 0; + // Is the link from end_point1 to end_point2 still valid? If yes, the link may be taken. Otherwise the link needs to be refreshed. + bool valid = true; + size_t end_point1_chain_id = 0; + size_t end_point2_chain_id = 0; + if (end_point2->chain_id > 0 && end_point2_other->chain_id > 0) { + // The other side is part of the output path. Don't connect to end_point2, update end_point1 and try another one. + valid = false; + } else { + // End points of the opposite ends of the segments. + end_point1_chain_id = chains.equivalent((chain1_flip ? end_point1 : end_point1_other)->chain_id); + end_point2_chain_id = chains.equivalent((chain2_flip ? end_point2 : end_point2_other)->chain_id); + if (end_point1_chain_id == end_point2_chain_id && end_point1_chain_id != 0) + // This edge forms a loop. Update end_point1 and try another one. + valid = false; + else { + // Verify whether end_point1.distance_out still matches the current state of the two end points to be connected and their chains. + // Namely, the other chain may have been flipped in the meantime. + double dist = (end_point2->pos - end_point1->pos).norm(); + if (chain1_flip) + dist += chains.chain_flip_penalty(end_point1_chain_id); + if (chain2_flip) + dist += chains.chain_flip_penalty(end_point2_chain_id); + if (std::abs(dist - end_point1->distance_out) > SCALED_EPSILON) + // The distance changed due to flipping of one of the chains. Refresh this end point in the queue. + valid = false; + } + if (valid && first_point != nullptr) { + // Verify that a chain starting or ending with the first_point does not get flipped. + if (chain1_flip) { + Chain &chain = chains.chain(end_point1_chain_id); + if (chain.begin == first_point || chain.end == first_point) + valid = false; + } + if (valid && chain2_flip) { + Chain &chain = chains.chain(end_point2_chain_id); + if (chain.begin == first_point || chain.end == first_point) + valid = false; + } + } + } + if (valid) { + // Remove the first and second point from the queue. + queue.pop(); + queue.remove(end_point2->heap_idx); + assert(end_point1->edge_candidate == end_point2); + end_point1->edge_candidate = nullptr; + Chain *chain1 = (end_point1_chain_id == 0) ? nullptr : &chains.chain(end_point1_chain_id); + Chain *chain2 = (end_point2_chain_id == 0) ? nullptr : &chains.chain(end_point2_chain_id); + assert(chain1 == nullptr || (chain1_flip ? (chain1->begin == end_point1_other || chain1->end == end_point1_other) : (chain1->begin == end_point1 || chain1->end == end_point1))); + assert(chain2 == nullptr || (chain2_flip ? (chain2->begin == end_point2_other || chain2->end == end_point2_other) : (chain2->begin == end_point2 || chain2->end == end_point2))); + if (chain1_flip) + chain1->flip(end_points); + if (chain2_flip) + chain2->flip(end_points); + assert(chain1 == nullptr || chain1->begin == end_point1 || chain1->end == end_point1); + assert(chain2 == nullptr || chain2->begin == end_point2 || chain2->end == end_point2); + size_t chain_id = chains.merge(end_point1_chain_id, end_point2_chain_id); + Chain &chain = chains.chain(chain_id); + { + Chain chain_dst; + chain_dst.begin = (chain1 == nullptr) ? end_point1_other : (chain1->begin == end_point1) ? chain1->end : chain1->begin; + chain_dst.end = (chain2 == nullptr) ? end_point2_other : (chain2->begin == end_point2) ? chain2->end : chain2->begin; + chain_dst.cost = (chain1 == 0 ? 0. : chain1->cost) + (chain2 == 0 ? 0. : chain2->cost) + (end_point2->pos - end_point1->pos).norm(); + chain_dst.cost_flipped = (chain1 == 0 ? 0. : chain1->cost_flipped) + (chain2 == 0 ? 0. : chain2->cost_flipped) + (end_point2_other->pos - end_point1_other->pos).norm(); + chain_dst.num_segments = (chain1 == 0 ? 1 : chain1->num_segments) + (chain2 == 0 ? 1 : chain2->num_segments); + chain_dst.equivalent_with = chain_id; + chain = chain_dst; + } + if (chain.begin != end_point1_other && ! end_point1_other->heap_idx_invalid()) + queue.remove(end_point1_other->heap_idx); + if (chain.end != end_point2_other && ! end_point2_other->heap_idx_invalid()) + queue.remove(end_point2_other->heap_idx); + end_point1->edge_out = end_point2; + end_point2->edge_out = end_point1; + end_point1->chain_id = chain_id; + end_point2->chain_id = chain_id; + end_point1_other->chain_id = chain_id; + end_point2_other->chain_id = chain_id; + if (chain.begin != first_point) + chain.begin->chain_id = 0; + if (chain.end != first_point) + chain.end->chain_id = 0; + if (-- num_connections_to_end == 0) { + assert(validate_graph_and_queue()); + // Last iteration. There shall be exactly one or two end points waiting to be connected. + assert(queue.size() <= ((first_point == nullptr) ? 4 : 2)); + if (first_point == nullptr) { + // Find the first remaining end point. + do { + first_point = queue.top(); + queue.pop(); + } while (first_point->edge_out != nullptr); + assert(first_point->edge_out == nullptr); + } + // Find the first remaining end point. + do { + last_point = queue.top(); + queue.pop(); + } while (last_point->edge_out != nullptr); + assert(last_point->edge_out == nullptr); +#ifndef NDEBUG + while (! queue.empty()) { + assert(queue.top()->edge_out != nullptr && queue.top()->chain_id > 0); + queue.pop(); + } +#endif /* NDEBUG */ + break; + } else { + //FIXME update the 2nd end points on the queue. + // Update end points of the flipped segments. + update_end_point_in_queue(queue, kdtree, chains, end_points, chain.begin->opposite(end_points), first_point_idx, first_point); + update_end_point_in_queue(queue, kdtree, chains, end_points, chain.end->opposite(end_points), first_point_idx, first_point); + if (chain1_flip) + update_end_point_in_queue(queue, kdtree, chains, end_points, *chain.begin, first_point_idx, first_point); + if (chain2_flip) + update_end_point_in_queue(queue, kdtree, chains, end_points, *chain.end, first_point_idx, first_point); + // End points of chains shall certainly stay in the queue. + assert(chain.begin == first_point || chain.begin->heap_idx < queue.size()); + assert(chain.end == first_point || chain.end ->heap_idx < queue.size()); + assert(&chain.begin->opposite(end_points) != first_point && + (chain.begin == first_point ? chain.begin->opposite(end_points).heap_idx_invalid() : chain.begin->opposite(end_points).heap_idx < queue.size())); + assert(&chain.end ->opposite(end_points) != first_point && + (chain.end == first_point ? chain.end ->opposite(end_points).heap_idx_invalid() : chain.end ->opposite(end_points).heap_idx < queue.size())); + + } + } else { + // This edge forms a loop. Update end_point1 and try another one. + update_end_point_in_queue(queue, kdtree, chains, end_points, *end_point1, first_point_idx, first_point); +#ifndef NDEBUG + // Each edge shall be longer than the last one removed from the queue. + //assert(end_point1->distance_out > distance_taken_last - SCALED_EPSILON); + if (end_point1->distance_out < distance_taken_last - SCALED_EPSILON) { +// printf("Warning: taking shorter length than previously is suspicious\n"); + } +#endif /* NDEBUG */ + //FIXME Remove the other end point from the KD tree. + // As the KD tree update is expensive, do it only after some larger number of points is removed from the queue. + } + assert(validate_graph_and_queue()); + } + assert(queue.empty()); + + // Now interconnect pairs of segments into a chain. + assert(first_point != nullptr); + out.reserve(num_segments); + bool failed = false; + do { + assert(out.size() < num_segments); + size_t first_point_id = first_point - &end_points.front(); + size_t segment_id = first_point_id >> 1; + bool reverse = (first_point_id & 1) != 0; + EndPoint *second_point = &end_points[first_point_id ^ 1]; + if (REVERSE_COULD_FAIL) { + if (reverse && ! could_reverse_func(segment_id)) { + failed = true; + break; + } + } else { + assert(! reverse || could_reverse_func(segment_id)); + } + out.emplace_back(segment_id, reverse); + first_point = second_point->edge_out; + } while (first_point != nullptr); + if (REVERSE_COULD_FAIL) { + if (failed) { + if (start_near == nullptr) { + // We may try the reverse order. + out.clear(); + first_point = last_point; + failed = false; + do { + assert(out.size() < num_segments); + size_t first_point_id = first_point - &end_points.front(); + size_t segment_id = first_point_id >> 1; + bool reverse = (first_point_id & 1) != 0; + EndPoint *second_point = &end_points[first_point_id ^ 1]; + if (reverse && ! could_reverse_func(segment_id)) { + failed = true; + break; + } + out.emplace_back(segment_id, reverse); + first_point = second_point->edge_out; + } while (first_point != nullptr); + } + } + if (failed) + // As a last resort, try a dumb algorithm, which is not sensitive to edge reversal constraints. + out = chain_segments_closest_point<EndPoint, decltype(kdtree), CouldReverseFunc>(end_points, kdtree, could_reverse_func, (initial_point != nullptr) ? *initial_point : end_points.front()); + } else { + assert(! failed); + } + } + + assert(out.size() == num_segments); + return out; +} + template<typename PointType, typename SegmentEndPointFunc, typename CouldReverseFunc> std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near) { @@ -402,6 +981,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy(SegmentEndPointFunc e return chain_segments_greedy_constrained_reversals_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near); } +template<typename PointType, typename SegmentEndPointFunc, typename CouldReverseFunc> +std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals2(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near) +{ + return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, true, CouldReverseFunc>(end_point_func, could_reverse_func, num_segments, start_near); +} + +template<typename PointType, typename SegmentEndPointFunc> +std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc end_point_func, size_t num_segments, const PointType *start_near) +{ + auto could_reverse_func = [](size_t /* idx */) -> bool { return true; }; + return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near); +} + std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near) { auto segment_end_point = [&entities](size_t idx, bool first_point) -> const Point& { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); }; @@ -472,8 +1064,22 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near) return out; } +#ifndef NDEBUG + // #define DEBUG_SVG_OUTPUT +#endif /* NDEBUG */ + +#ifdef DEBUG_SVG_OUTPUT +void svg_draw_polyline_chain(const char *name, size_t idx, const Polylines &polylines) +{ + BoundingBox bbox = get_extents(polylines); + SVG svg(debug_out_path("%s-%d.svg", name, idx).c_str(), bbox); + svg.draw(polylines); + for (size_t i = 1; i < polylines.size(); ++i) + svg.draw(Line(polylines[i - 1].last_point(), polylines[i].first_point()), "red"); +} +#endif /* DEBUG_SVG_OUTPUT */ + // Flip the sequences of polylines to lower the total length of connecting lines. -// #define DEBUG_SVG_OUTPUT static inline void improve_ordering_by_segment_flipping(Polylines &polylines, bool fixed_start) { #ifndef NDEBUG @@ -487,14 +1093,8 @@ static inline void improve_ordering_by_segment_flipping(Polylines &polylines, bo static int iRun = 0; ++ iRun; - BoundingBox bbox = get_extents(polylines); #ifdef DEBUG_SVG_OUTPUT - { - SVG svg(debug_out_path("improve_ordering_by_segment_flipping-initial-%d.svg", iRun).c_str(), bbox); - svg.draw(polylines); - for (size_t i = 1; i < polylines.size(); ++ i) - svg.draw(Line(polylines[i - 1].last_point(), polylines[i].first_point()), "red"); - } + svg_draw_polyline_chain("improve_ordering_by_segment_flipping-initial", iRun, polylines); #endif /* DEBUG_SVG_OUTPUT */ #endif /* NDEBUG */ @@ -550,7 +1150,7 @@ static inline void improve_ordering_by_segment_flipping(Polylines &polylines, bo #endif /* NDEBUG */ // Initialize a MutablePriorityHeap of connections between polylines. - auto queue = make_mutable_priority_queue<Connection*>( + auto queue = make_mutable_priority_queue<Connection*, false>( [](Connection *connection, size_t idx){ connection->heap_idx = idx; }, // Sort by decreasing connection distance. [&polylines, &connections](Connection *l, Connection *r){ return l->squaredNorm(polylines, connections) > r->squaredNorm(polylines, connections); }); @@ -643,34 +1243,689 @@ static inline void improve_ordering_by_segment_flipping(Polylines &polylines, bo #ifndef NDEBUG double cost_final = cost(); #ifdef DEBUG_SVG_OUTPUT + svg_draw_polyline_chain("improve_ordering_by_segment_flipping-final", iRun, polylines); +#endif /* DEBUG_SVG_OUTPUT */ + assert(cost_final <= cost_prev); + assert(cost_final <= cost_initial); +#endif /* NDEBUG */ +} + +struct FlipEdge { + FlipEdge(const Vec2d &p1, const Vec2d &p2, size_t source_index) : p1(p1), p2(p2), source_index(source_index) {} + void flip() { std::swap(this->p1, this->p2); } + Vec2d p1; + Vec2d p2; + size_t source_index; +}; + +struct ConnectionCost { + ConnectionCost(double cost, double cost_flipped) : cost(cost), cost_flipped(cost_flipped) {} + ConnectionCost() : cost(0.), cost_flipped(0.) {} + void flip() { std::swap(this->cost, this->cost_flipped); } + double cost = 0; + double cost_flipped = 0; +}; +static inline ConnectionCost operator-(const ConnectionCost &lhs, const ConnectionCost& rhs) { return ConnectionCost(lhs.cost - rhs.cost, lhs.cost_flipped - rhs.cost_flipped); } + +static inline std::pair<double, size_t> minimum_crossover_cost( + const std::vector<FlipEdge> &edges, + const std::pair<size_t, size_t> &span1, const ConnectionCost &cost1, + const std::pair<size_t, size_t> &span2, const ConnectionCost &cost2, + const std::pair<size_t, size_t> &span3, const ConnectionCost &cost3, + const double cost_current) +{ + auto connection_cost = [&edges]( + const std::pair<size_t, size_t> &span1, const ConnectionCost &cost1, bool reversed1, bool flipped1, + const std::pair<size_t, size_t> &span2, const ConnectionCost &cost2, bool reversed2, bool flipped2, + const std::pair<size_t, size_t> &span3, const ConnectionCost &cost3, bool reversed3, bool flipped3) { + auto first_point = [&edges](const std::pair<size_t, size_t> &span, bool flipped) { return flipped ? edges[span.first].p2 : edges[span.first].p1; }; + auto last_point = [&edges](const std::pair<size_t, size_t> &span, bool flipped) { return flipped ? edges[span.second - 1].p1 : edges[span.second - 1].p2; }; + auto point = [first_point, last_point](const std::pair<size_t, size_t> &span, bool start, bool flipped) { return start ? first_point(span, flipped) : last_point(span, flipped); }; + auto cost = [](const ConnectionCost &acost, bool flipped) { + assert(acost.cost >= 0. && acost.cost_flipped >= 0.); + return flipped ? acost.cost_flipped : acost.cost; + }; + // Ignore reversed single segment spans. + auto simple_span_ignore = [](const std::pair<size_t, size_t>& span, bool reversed) { + return span.first + 1 == span.second && reversed; + }; + assert(span1.first < span1.second); + assert(span2.first < span2.second); + assert(span3.first < span3.second); + return + simple_span_ignore(span1, reversed1) || simple_span_ignore(span2, reversed2) || simple_span_ignore(span3, reversed3) ? + // Don't perform unnecessary calculations simulating reversion of single segment spans. + std::numeric_limits<double>::max() : + // Calculate the cost of reverting chains and / or flipping segment orientations. + cost(cost1, flipped1) + cost(cost2, flipped2) + cost(cost3, flipped3) + + (point(span2, ! reversed2, flipped2) - point(span1, reversed1, flipped1)).norm() + + (point(span3, ! reversed3, flipped3) - point(span2, reversed2, flipped2)).norm(); + }; + +#ifndef NDEBUG { - SVG svg(debug_out_path("improve_ordering_by_segment_flipping-final-%d.svg", iRun).c_str(), bbox); - svg.draw(polylines); - for (size_t i = 1; i < polylines.size(); ++ i) - svg.draw(Line(polylines[i - 1].last_point(), polylines[i].first_point()), "red"); + double c = connection_cost(span1, cost1, false, false, span2, cost2, false, false, span3, cost3, false, false); + assert(std::abs(c - cost_current) < SCALED_EPSILON); + } +#endif /* NDEBUG */ + + double cost_min = cost_current; + size_t flip_min = 0; // no flip, no improvement + for (size_t i = 0; i < (1 << 6); ++ i) { + // From the three combinations of 1,2,3 ordering, the other three are reversals of the first three. + double c1 = (i == 0) ? cost_current : + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, cost2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, cost3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + double c2 = connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, cost3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, cost2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + double c3 = connection_cost(span2, cost2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, cost1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, cost3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + if (c1 < cost_min) { + cost_min = c1; + flip_min = i; + } + if (c2 < cost_min) { + cost_min = c2; + flip_min = i + (1 << 6); + } + if (c3 < cost_min) { + cost_min = c3; + flip_min = i + (2 << 6); + } + } + return std::make_pair(cost_min, flip_min); +} + +static inline std::pair<double, size_t> minimum_crossover_cost( + const std::vector<FlipEdge> &edges, + const std::pair<size_t, size_t> &span1, const ConnectionCost &cost1, + const std::pair<size_t, size_t> &span2, const ConnectionCost &cost2, + const std::pair<size_t, size_t> &span3, const ConnectionCost &cost3, + const std::pair<size_t, size_t> &span4, const ConnectionCost &cost4, + const double cost_current) +{ + auto connection_cost = [&edges]( + const std::pair<size_t, size_t> &span1, const ConnectionCost &cost1, bool reversed1, bool flipped1, + const std::pair<size_t, size_t> &span2, const ConnectionCost &cost2, bool reversed2, bool flipped2, + const std::pair<size_t, size_t> &span3, const ConnectionCost &cost3, bool reversed3, bool flipped3, + const std::pair<size_t, size_t> &span4, const ConnectionCost &cost4, bool reversed4, bool flipped4) { + auto first_point = [&edges](const std::pair<size_t, size_t> &span, bool flipped) { return flipped ? edges[span.first].p2 : edges[span.first].p1; }; + auto last_point = [&edges](const std::pair<size_t, size_t> &span, bool flipped) { return flipped ? edges[span.second - 1].p1 : edges[span.second - 1].p2; }; + auto point = [first_point, last_point](const std::pair<size_t, size_t> &span, bool start, bool flipped) { return start ? first_point(span, flipped) : last_point(span, flipped); }; + auto cost = [](const ConnectionCost &acost, bool flipped) { + assert(acost.cost >= 0. && acost.cost_flipped >= 0.); + return flipped ? acost.cost_flipped : acost.cost; + }; + // Ignore reversed single segment spans. + auto simple_span_ignore = [](const std::pair<size_t, size_t>& span, bool reversed) { + return span.first + 1 == span.second && reversed; + }; + assert(span1.first < span1.second); + assert(span2.first < span2.second); + assert(span3.first < span3.second); + assert(span4.first < span4.second); + return + simple_span_ignore(span1, reversed1) || simple_span_ignore(span2, reversed2) || simple_span_ignore(span3, reversed3) || simple_span_ignore(span4, reversed4) ? + // Don't perform unnecessary calculations simulating reversion of single segment spans. + std::numeric_limits<double>::max() : + // Calculate the cost of reverting chains and / or flipping segment orientations. + cost(cost1, flipped1) + cost(cost2, flipped2) + cost(cost3, flipped3) + cost(cost4, flipped4) + + (point(span2, ! reversed2, flipped2) - point(span1, reversed1, flipped1)).norm() + + (point(span3, ! reversed3, flipped3) - point(span2, reversed2, flipped2)).norm() + + (point(span4, ! reversed4, flipped4) - point(span3, reversed3, flipped3)).norm(); + }; + +#ifndef NDEBUG + { + double c = connection_cost(span1, cost1, false, false, span2, cost2, false, false, span3, cost3, false, false, span4, cost4, false, false); + assert(std::abs(c - cost_current) < SCALED_EPSILON); + } +#endif /* NDEBUG */ + + double cost_min = cost_current; + size_t flip_min = 0; // no flip, no improvement + for (size_t i = 0; i < (1 << 8); ++ i) { + // From the three combinations of 1,2,3 ordering, the other three are reversals of the first three. + size_t permutation = 0; + for (double c : { + (i == 0) ? cost_current : + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, cost2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, cost3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, cost2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, cost4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, cost3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, cost3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, cost2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, cost3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, cost4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span2, cost2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span4, cost4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, cost2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, cost3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span1, cost1, (i & 1) != 0, (i & (1 << 1)) != 0, span4, cost4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, cost3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span2, cost2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span2, cost2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, cost1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, cost3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span2, cost2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, cost1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, cost4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, cost3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span2, cost2, (i & 1) != 0, (i & (1 << 1)) != 0, span3, cost3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, cost1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span2, cost2, (i & 1) != 0, (i & (1 << 1)) != 0, span4, cost4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, cost1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, cost3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span3, cost3, (i & 1) != 0, (i & (1 << 1)) != 0, span1, cost1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, cost2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(span3, cost3, (i & 1) != 0, (i & (1 << 1)) != 0, span2, cost2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, cost1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, cost4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0) + }) { + if (c < cost_min) { + cost_min = c; + flip_min = i + (permutation << 8); + } + ++ permutation; + } } + return std::make_pair(cost_min, flip_min); +} + +static inline void do_crossover(const std::vector<FlipEdge> &edges_in, std::vector<FlipEdge> &edges_out, + const std::pair<size_t, size_t> &span1, const std::pair<size_t, size_t> &span2, const std::pair<size_t, size_t> &span3, + size_t i) +{ + assert(edges_in.size() == edges_out.size()); + auto do_it = [&edges_in, &edges_out]( + const std::pair<size_t, size_t> &span1, bool reversed1, bool flipped1, + const std::pair<size_t, size_t> &span2, bool reversed2, bool flipped2, + const std::pair<size_t, size_t> &span3, bool reversed3, bool flipped3) { + auto it_edges_out = edges_out.begin(); + auto copy_span = [&edges_in, &edges_out, &it_edges_out](std::pair<size_t, size_t> span, bool reversed, bool flipped) { + assert(span.first < span.second); + auto it = it_edges_out; + if (reversed) + std::reverse_copy(edges_in.begin() + span.first, edges_in.begin() + span.second, it_edges_out); + else + std::copy (edges_in.begin() + span.first, edges_in.begin() + span.second, it_edges_out); + it_edges_out += span.second - span.first; + if (reversed != flipped) { + for (; it != it_edges_out; ++ it) + it->flip(); + } + }; + copy_span(span1, reversed1, flipped1); + copy_span(span2, reversed2, flipped2); + copy_span(span3, reversed3, flipped3); + }; + switch (i >> 6) { + case 0: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + break; + case 1: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + break; + default: + assert((i >> 6) == 2); + do_it(span2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0); + } + assert(edges_in.size() == edges_out.size()); +} + + +static inline void do_crossover(const std::vector<FlipEdge> &edges_in, std::vector<FlipEdge> &edges_out, + const std::pair<size_t, size_t> &span1, const std::pair<size_t, size_t> &span2, const std::pair<size_t, size_t> &span3, const std::pair<size_t, size_t> &span4, + size_t i) +{ + assert(edges_in.size() == edges_out.size()); + auto do_it = [&edges_in, &edges_out]( + const std::pair<size_t, size_t> &span1, bool reversed1, bool flipped1, + const std::pair<size_t, size_t> &span2, bool reversed2, bool flipped2, + const std::pair<size_t, size_t> &span3, bool reversed3, bool flipped3, + const std::pair<size_t, size_t> &span4, bool reversed4, bool flipped4) { + auto it_edges_out = edges_out.begin(); + auto copy_span = [&edges_in, &edges_out, &it_edges_out](std::pair<size_t, size_t> span, bool reversed, bool flipped) { + assert(span.first < span.second); + auto it = it_edges_out; + if (reversed) + std::reverse_copy(edges_in.begin() + span.first, edges_in.begin() + span.second, it_edges_out); + else + std::copy (edges_in.begin() + span.first, edges_in.begin() + span.second, it_edges_out); + it_edges_out += span.second - span.first; + if (reversed != flipped) { + for (; it != it_edges_out; ++ it) + it->flip(); + } + }; + copy_span(span1, reversed1, flipped1); + copy_span(span2, reversed2, flipped2); + copy_span(span3, reversed3, flipped3); + copy_span(span4, reversed4, flipped4); + }; + switch (i >> 8) { + case 0: + assert(i != 0); // otherwise it would be a no-op + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 1: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 2: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 3: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 4: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 5: + do_it(span1, (i & 1) != 0, (i & (1 << 1)) != 0, span4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 6: + do_it(span2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 7: + do_it(span2, (i & 1) != 0, (i & (1 << 1)) != 0, span1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span4, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 8: + do_it(span2, (i & 1) != 0, (i & (1 << 1)) != 0, span3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 9: + do_it(span2, (i & 1) != 0, (i & (1 << 1)) != 0, span4, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + case 10: + do_it(span3, (i & 1) != 0, (i & (1 << 1)) != 0, span1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + default: + assert((i >> 8) == 11); + do_it(span3, (i & 1) != 0, (i & (1 << 1)) != 0, span2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, span1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, span4, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0); + break; + } + assert(edges_in.size() == edges_out.size()); +} + +static inline void reorder_by_two_exchanges_with_segment_flipping(std::vector<FlipEdge> &edges) +{ + if (edges.size() < 2) + return; + + std::vector<ConnectionCost> connections(edges.size()); + std::vector<FlipEdge> edges_tmp(edges); + std::vector<std::pair<double, size_t>> connection_lengths(edges.size() - 1, std::pair<double, size_t>(0., 0)); + std::vector<char> connection_tried(edges.size(), false); + for (size_t iter = 0; iter < edges.size(); ++ iter) { + // Initialize connection costs and connection lengths. + for (size_t i = 1; i < edges.size(); ++ i) { + const FlipEdge &e1 = edges[i - 1]; + const FlipEdge &e2 = edges[i]; + ConnectionCost &c = connections[i]; + c = connections[i - 1]; + double l = (e2.p1 - e1.p2).norm(); + c.cost += l; + c.cost_flipped += (e2.p2 - e1.p1).norm(); + connection_lengths[i - 1] = std::make_pair(l, i); + } + std::sort(connection_lengths.begin(), connection_lengths.end(), [](const std::pair<double, size_t> &l, const std::pair<double, size_t> &r) { return l.first > r.first; }); + std::fill(connection_tried.begin(), connection_tried.end(), false); + size_t crossover1_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover2_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover_flip_final = 0; + for (const std::pair<double, size_t> &first_crossover_candidate : connection_lengths) { + double longest_connection_length = first_crossover_candidate.first; + size_t longest_connection_idx = first_crossover_candidate.second; + connection_tried[longest_connection_idx] = true; + // Find the second crossover connection with the lowest total chain cost. + size_t crossover_pos_min = std::numeric_limits<size_t>::max(); + double crossover_cost_min = connections.back().cost; + size_t crossover_flip_min = 0; + for (size_t j = 1; j < connections.size(); ++ j) + if (! connection_tried[j]) { + size_t a = j; + size_t b = longest_connection_idx; + if (a > b) + std::swap(a, b); + std::pair<double, size_t> cost_and_flip = minimum_crossover_cost(edges, + std::make_pair(size_t(0), a), connections[a - 1], std::make_pair(a, b), connections[b - 1] - connections[a], std::make_pair(b, edges.size()), connections.back() - connections[b], + connections.back().cost); + if (cost_and_flip.second > 0 && cost_and_flip.first < crossover_cost_min) { + crossover_pos_min = j; + crossover_cost_min = cost_and_flip.first; + crossover_flip_min = cost_and_flip.second; + assert(crossover_cost_min < connections.back().cost + EPSILON); + } + } + if (crossover_cost_min < connections.back().cost) { + // The cost of the chain with the proposed two crossovers has a lower total cost than the current chain. Apply the crossover. + crossover1_pos_final = longest_connection_idx; + crossover2_pos_final = crossover_pos_min; + crossover_flip_final = crossover_flip_min; + break; + } else { + // Continue with another long candidate edge. + } + } + if (crossover_flip_final > 0) { + // Pair of cross over positions and flip / reverse constellation has been found, which improves the total cost of the connection. + // Perform a crossover. + if (crossover1_pos_final > crossover2_pos_final) + std::swap(crossover1_pos_final, crossover2_pos_final); + do_crossover(edges, edges_tmp, std::make_pair(size_t(0), crossover1_pos_final), std::make_pair(crossover1_pos_final, crossover2_pos_final), std::make_pair(crossover2_pos_final, edges.size()), crossover_flip_final); + edges.swap(edges_tmp); + } else { + // No valid pair of cross over positions was found improving the total cost. Giving up. + break; + } + } +} + +static inline void reorder_by_three_exchanges_with_segment_flipping(std::vector<FlipEdge> &edges) +{ + if (edges.size() < 3) { + reorder_by_two_exchanges_with_segment_flipping(edges); + return; + } + + std::vector<ConnectionCost> connections(edges.size()); + std::vector<FlipEdge> edges_tmp(edges); + std::vector<std::pair<double, size_t>> connection_lengths(edges.size() - 1, std::pair<double, size_t>(0., 0)); + std::vector<char> connection_tried(edges.size(), false); + for (size_t iter = 0; iter < edges.size(); ++ iter) { + // Initialize connection costs and connection lengths. + for (size_t i = 1; i < edges.size(); ++ i) { + const FlipEdge &e1 = edges[i - 1]; + const FlipEdge &e2 = edges[i]; + ConnectionCost &c = connections[i]; + c = connections[i - 1]; + double l = (e2.p1 - e1.p2).norm(); + c.cost += l; + c.cost_flipped += (e2.p2 - e1.p1).norm(); + connection_lengths[i - 1] = std::make_pair(l, i); + } + std::sort(connection_lengths.begin(), connection_lengths.end(), [](const std::pair<double, size_t> &l, const std::pair<double, size_t> &r) { return l.first > r.first; }); + std::fill(connection_tried.begin(), connection_tried.end(), false); + size_t crossover1_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover2_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover3_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover_flip_final = 0; + for (const std::pair<double, size_t> &first_crossover_candidate : connection_lengths) { + double longest_connection_length = first_crossover_candidate.first; + size_t longest_connection_idx = first_crossover_candidate.second; + connection_tried[longest_connection_idx] = true; + // Find the second crossover connection with the lowest total chain cost. + size_t crossover_pos_min = std::numeric_limits<size_t>::max(); + double crossover_cost_min = connections.back().cost; + for (size_t j = 1; j < connections.size(); ++ j) + if (! connection_tried[j]) { + for (size_t k = j + 1; k < connections.size(); ++ k) + if (! connection_tried[k]) { + size_t a = longest_connection_idx; + size_t b = j; + size_t c = k; + if (a > c) + std::swap(a, c); + if (a > b) + std::swap(a, b); + if (b > c) + std::swap(b, c); + std::pair<double, size_t> cost_and_flip = minimum_crossover_cost(edges, + std::make_pair(size_t(0), a), connections[a - 1], std::make_pair(a, b), connections[b - 1] - connections[a], + std::make_pair(b, c), connections[c - 1] - connections[b], std::make_pair(c, edges.size()), connections.back() - connections[c], + connections.back().cost); + if (cost_and_flip.second > 0 && cost_and_flip.first < crossover_cost_min) { + crossover_cost_min = cost_and_flip.first; + crossover1_pos_final = a; + crossover2_pos_final = b; + crossover3_pos_final = c; + crossover_flip_final = cost_and_flip.second; + assert(crossover_cost_min < connections.back().cost + EPSILON); + } + } + } + if (crossover_flip_final > 0) { + // The cost of the chain with the proposed two crossovers has a lower total cost than the current chain. Apply the crossover. + break; + } else { + // Continue with another long candidate edge. + } + } + if (crossover_flip_final > 0) { + // Pair of cross over positions and flip / reverse constellation has been found, which improves the total cost of the connection. + // Perform a crossover. + do_crossover(edges, edges_tmp, std::make_pair(size_t(0), crossover1_pos_final), std::make_pair(crossover1_pos_final, crossover2_pos_final), + std::make_pair(crossover2_pos_final, crossover3_pos_final), std::make_pair(crossover3_pos_final, edges.size()), crossover_flip_final); + edges.swap(edges_tmp); + } else { + // No valid pair of cross over positions was found improving the total cost. Giving up. + break; + } + } +} + +typedef Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::DontAlign> Matrixd; + +class FourOptCosts { +public: + FourOptCosts(const ConnectionCost &c1, const ConnectionCost &c2, const ConnectionCost &c3, const ConnectionCost &c4) : costs { &c1, &c2, &c3, &c4 } {} + + double operator()(size_t piece_idx, bool flipped) const { return flipped ? costs[piece_idx]->cost_flipped : costs[piece_idx]->cost; } + +private: + const ConnectionCost* costs[4]; +}; + +static inline std::pair<double, size_t> minimum_crossover_cost( + const FourOptCosts &segment_costs, + const Matrixd &segment_end_point_distance_matrix, + const double cost_current) +{ + // Distance from the end of span1 to the start of span2. + auto end_point_distance = [&segment_end_point_distance_matrix](size_t span1, bool reversed1, bool flipped1, size_t span2, bool reversed2, bool flipped2) { + return segment_end_point_distance_matrix(span1 * 4 + (! reversed1) * 2 + flipped1, span2 * 4 + reversed2 * 2 + flipped2); + }; + auto connection_cost = [&segment_costs, end_point_distance]( + const size_t span1, bool reversed1, bool flipped1, + const size_t span2, bool reversed2, bool flipped2, + const size_t span3, bool reversed3, bool flipped3, + const size_t span4, bool reversed4, bool flipped4) { + // Calculate the cost of reverting chains and / or flipping segment orientations. + return segment_costs(span1, flipped1) + segment_costs(span2, flipped2) + segment_costs(span3, flipped3) + segment_costs(span4, flipped4) + + end_point_distance(span1, reversed1, flipped1, span2, reversed2, flipped2) + + end_point_distance(span2, reversed2, flipped2, span3, reversed3, flipped3) + + end_point_distance(span3, reversed3, flipped3, span4, reversed4, flipped4); + }; + +#ifndef NDEBUG + { + double c = connection_cost(0, false, false, 1, false, false, 2, false, false, 3, false, false); + assert(std::abs(c - cost_current) < SCALED_EPSILON); + } +#endif /* NDEBUG */ + + double cost_min = cost_current; + size_t flip_min = 0; // no flip, no improvement + for (size_t i = 0; i < (1 << 8); ++ i) { + // From the three combinations of 1,2,3 ordering, the other three are reversals of the first three. + size_t permutation = 0; + for (double c : { + (i == 0) ? cost_current : + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 1, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(0, (i & 1) != 0, (i & (1 << 1)) != 0, 3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 1, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(1, (i & 1) != 0, (i & (1 << 1)) != 0, 0, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 2, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(1, (i & 1) != 0, (i & (1 << 1)) != 0, 0, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 3, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(1, (i & 1) != 0, (i & (1 << 1)) != 0, 2, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 0, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(1, (i & 1) != 0, (i & (1 << 1)) != 0, 3, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 0, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 2, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(2, (i & 1) != 0, (i & (1 << 1)) != 0, 0, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 1, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0), + connection_cost(2, (i & 1) != 0, (i & (1 << 1)) != 0, 1, (i & (1 << 2)) != 0, (i & (1 << 3)) != 0, 0, (i & (1 << 4)) != 0, (i & (1 << 5)) != 0, 3, (i & (1 << 6)) != 0, (i & (1 << 7)) != 0) + }) { + if (c < cost_min) { + cost_min = c; + flip_min = i + (permutation << 8); + } + ++ permutation; + } + } + return std::make_pair(cost_min, flip_min); +} + +static inline void reorder_by_three_exchanges_with_segment_flipping2(std::vector<FlipEdge> &edges) +{ + if (edges.size() < 3) { + reorder_by_two_exchanges_with_segment_flipping(edges); + return; + } + + std::vector<ConnectionCost> connections(edges.size()); + std::vector<FlipEdge> edges_tmp(edges); + std::vector<std::pair<double, size_t>> connection_lengths(edges.size() - 1, std::pair<double, size_t>(0., 0)); + std::vector<char> connection_tried(edges.size(), false); + for (size_t iter = 0; iter < edges.size(); ++ iter) { + // Initialize connection costs and connection lengths. + for (size_t i = 1; i < edges.size(); ++ i) { + const FlipEdge &e1 = edges[i - 1]; + const FlipEdge &e2 = edges[i]; + ConnectionCost &c = connections[i]; + c = connections[i - 1]; + double l = (e2.p1 - e1.p2).norm(); + c.cost += l; + c.cost_flipped += (e2.p2 - e1.p1).norm(); + connection_lengths[i - 1] = std::make_pair(l, i); + } + std::sort(connection_lengths.begin(), connection_lengths.end(), [](const std::pair<double, size_t> &l, const std::pair<double, size_t> &r) { return l.first > r.first; }); + std::fill(connection_tried.begin(), connection_tried.end(), false); + size_t crossover1_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover2_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover3_pos_final = std::numeric_limits<size_t>::max(); + size_t crossover_flip_final = 0; + // Distances between the end points of the four pieces of the current segment sequence. +#ifdef NDEBUG + Matrixd segment_end_point_distance_matrix(4 * 4, 4 * 4); +#else /* NDEBUG */ + Matrixd segment_end_point_distance_matrix = Matrixd::Constant(4 * 4, 4 * 4, std::numeric_limits<double>::max()); +#endif /* NDEBUG */ + for (const std::pair<double, size_t> &first_crossover_candidate : connection_lengths) { + double longest_connection_length = first_crossover_candidate.first; + size_t longest_connection_idx = first_crossover_candidate.second; + connection_tried[longest_connection_idx] = true; + // Find the second crossover connection with the lowest total chain cost. + size_t crossover_pos_min = std::numeric_limits<size_t>::max(); + double crossover_cost_min = connections.back().cost; + for (size_t j = 1; j < connections.size(); ++ j) + if (! connection_tried[j]) { + for (size_t k = j + 1; k < connections.size(); ++ k) + if (! connection_tried[k]) { + size_t a = longest_connection_idx; + size_t b = j; + size_t c = k; + if (a > c) + std::swap(a, c); + if (a > b) + std::swap(a, b); + if (b > c) + std::swap(b, c); + const Vec2d* endpts[16] = { + &edges[0].p1, &edges[0].p2, &edges[a - 1].p2, &edges[a - 1].p1, + &edges[a].p1, &edges[a].p2, &edges[b - 1].p2, &edges[b - 1].p1, + &edges[b].p1, &edges[b].p2, &edges[c - 1].p2, &edges[c - 1].p1, + &edges[c].p1, &edges[c].p2, &edges.back().p2, &edges.back().p1 }; + for (size_t v = 0; v < 16; ++ v) { + const Vec2d &p1 = *endpts[v]; + for (size_t u = (v & (~3)) + 4; u < 16; ++ u) + segment_end_point_distance_matrix(u, v) = segment_end_point_distance_matrix(v, u) = (*endpts[u] - p1).norm(); + } + FourOptCosts segment_costs(connections[a - 1], connections[b - 1] - connections[a], connections[c - 1] - connections[b], connections.back() - connections[c]); + std::pair<double, size_t> cost_and_flip = minimum_crossover_cost(segment_costs, segment_end_point_distance_matrix, connections.back().cost); + if (cost_and_flip.second > 0 && cost_and_flip.first < crossover_cost_min) { + crossover_cost_min = cost_and_flip.first; + crossover1_pos_final = a; + crossover2_pos_final = b; + crossover3_pos_final = c; + crossover_flip_final = cost_and_flip.second; + assert(crossover_cost_min < connections.back().cost + EPSILON); + } + } + } + if (crossover_flip_final > 0) { + // The cost of the chain with the proposed two crossovers has a lower total cost than the current chain. Apply the crossover. + break; + } else { + // Continue with another long candidate edge. + } + } + if (crossover_flip_final > 0) { + // Pair of cross over positions and flip / reverse constellation has been found, which improves the total cost of the connection. + // Perform a crossover. + do_crossover(edges, edges_tmp, std::make_pair(size_t(0), crossover1_pos_final), std::make_pair(crossover1_pos_final, crossover2_pos_final), + std::make_pair(crossover2_pos_final, crossover3_pos_final), std::make_pair(crossover3_pos_final, edges.size()), crossover_flip_final); + edges.swap(edges_tmp); + } else { + // No valid pair of cross over positions was found improving the total cost. Giving up. + break; + } + } +} + +// Flip the sequences of polylines to lower the total length of connecting lines. +static inline void improve_ordering_by_two_exchanges_with_segment_flipping(Polylines &polylines, bool fixed_start) +{ +#ifndef NDEBUG + auto cost = [&polylines]() { + double sum = 0.; + for (size_t i = 1; i < polylines.size(); ++i) + sum += (polylines[i].first_point() - polylines[i - 1].last_point()).cast<double>().norm(); + return sum; + }; + double cost_initial = cost(); + + static int iRun = 0; + ++ iRun; +#ifdef DEBUG_SVG_OUTPUT + svg_draw_polyline_chain("improve_ordering_by_two_exchanges_with_segment_flipping-initial", iRun, polylines); #endif /* DEBUG_SVG_OUTPUT */ #endif /* NDEBUG */ - assert(cost_final <= cost_prev); + std::vector<FlipEdge> edges; + edges.reserve(polylines.size()); + std::transform(polylines.begin(), polylines.end(), std::back_inserter(edges), + [&polylines](const Polyline &pl){ return FlipEdge(pl.first_point().cast<double>(), pl.last_point().cast<double>(), &pl - polylines.data()); }); +#if 1 + reorder_by_two_exchanges_with_segment_flipping(edges); +#else + // reorder_by_three_exchanges_with_segment_flipping(edges); + reorder_by_three_exchanges_with_segment_flipping2(edges); +#endif + Polylines out; + out.reserve(polylines.size()); + for (const FlipEdge &edge : edges) { + Polyline &pl = polylines[edge.source_index]; + out.emplace_back(std::move(pl)); + if (edge.p2 == pl.first_point().cast<double>()) { + // Polyline is flipped. + out.back().reverse(); + } else { + // Polyline is not flipped. + assert(edge.p1 == pl.first_point().cast<double>()); + } + } + +#ifndef NDEBUG + double cost_final = cost(); +#ifdef DEBUG_SVG_OUTPUT + svg_draw_polyline_chain("improve_ordering_by_two_exchanges_with_segment_flipping-final", iRun, out); +#endif /* DEBUG_SVG_OUTPUT */ assert(cost_final <= cost_initial); +#endif /* NDEBUG */ } Polylines chain_polylines(Polylines &&polylines, const Point *start_near) { +#ifdef DEBUG_SVG_OUTPUT + static int iRun = 0; + ++ iRun; + svg_draw_polyline_chain("chain_polylines-initial", iRun, polylines); +#endif /* DEBUG_SVG_OUTPUT */ + Polylines out; if (! polylines.empty()) { auto segment_end_point = [&polylines](size_t idx, bool first_point) -> const Point& { return first_point ? polylines[idx].first_point() : polylines[idx].last_point(); }; - std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, polylines.size(), start_near); + std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy2<Point, decltype(segment_end_point)>(segment_end_point, polylines.size(), start_near); out.reserve(polylines.size()); for (auto &segment_and_reversal : ordered) { out.emplace_back(std::move(polylines[segment_and_reversal.first])); if (segment_and_reversal.second) out.back().reverse(); } - if (out.size() > 1) - improve_ordering_by_segment_flipping(out, start_near != nullptr); + if (out.size() > 1 && start_near == nullptr) { + improve_ordering_by_two_exchanges_with_segment_flipping(out, start_near != nullptr); + //improve_ordering_by_segment_flipping(out, start_near != nullptr); + } } + +#ifdef DEBUG_SVG_OUTPUT + svg_draw_polyline_chain("chain_polylines-final", iRun, out); +#endif /* DEBUG_SVG_OUTPUT */ return out; } diff --git a/src/libslic3r/Slicing.cpp b/src/libslic3r/Slicing.cpp index c62736ffe..9af6048ab 100644 --- a/src/libslic3r/Slicing.cpp +++ b/src/libslic3r/Slicing.cpp @@ -224,40 +224,59 @@ std::vector<coordf_t> layer_height_profile_from_ranges( // Based on the work of @platsch // Fill layer_height_profile by heights ensuring a prescribed maximum cusp height. +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +std::vector<double> layer_height_profile_adaptive(const SlicingParameters& slicing_params, + const ModelObject& object, float cusp_value) +#else std::vector<coordf_t> layer_height_profile_adaptive( const SlicingParameters &slicing_params, const t_layer_config_ranges & /* layer_config_ranges */, const ModelVolumePtrs &volumes) +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE { +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // 1) Initialize the SlicingAdaptive class with the object meshes. SlicingAdaptive as; as.set_slicing_parameters(slicing_params); - for (const ModelVolume *volume : volumes) + as.set_object(object); +#else + // 1) Initialize the SlicingAdaptive class with the object meshes. + SlicingAdaptive as; + as.set_slicing_parameters(slicing_params); + for (const ModelVolume* volume : volumes) if (volume->is_model_part()) as.add_mesh(&volume->mesh()); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + as.prepare(); // 2) Generate layers using the algorithm of @platsch // loop until we have at least one layer and the max slice_z reaches the object height - //FIXME make it configurable - // Cusp value: A maximum allowed distance from a corner of a rectangular extrusion to a chrodal line, in mm. - const coordf_t cusp_value = 0.2; // $self->config->get_value('cusp_value'); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + double cusp_value = 0.2; +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE - std::vector<coordf_t> layer_height_profile; - layer_height_profile.push_back(0.); + std::vector<double> layer_height_profile; + layer_height_profile.push_back(0.0); layer_height_profile.push_back(slicing_params.first_object_layer_height); if (slicing_params.first_object_layer_height_fixed()) { layer_height_profile.push_back(slicing_params.first_object_layer_height); layer_height_profile.push_back(slicing_params.first_object_layer_height); } - coordf_t slice_z = slicing_params.first_object_layer_height; - coordf_t height = slicing_params.first_object_layer_height; + double slice_z = slicing_params.first_object_layer_height; int current_facet = 0; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + while (slice_z <= slicing_params.object_print_z_height()) { + double height = slicing_params.max_layer_height; +#else + double height = slicing_params.first_object_layer_height; while ((slice_z - height) <= slicing_params.object_print_z_height()) { - height = 999; + height = 999.0; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // Slic3r::debugf "\n Slice layer: %d\n", $id; // determine next layer height - coordf_t cusp_height = as.cusp_height(slice_z, cusp_value, current_facet); + double cusp_height = as.cusp_height((float)slice_z, cusp_value, current_facet); + // check for horizontal features and object size /* if($self->config->get_value('match_horizontal_surfaces')) { @@ -303,19 +322,113 @@ std::vector<coordf_t> layer_height_profile_adaptive( layer_height_profile.push_back(slice_z); layer_height_profile.push_back(height); slice_z += height; +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE layer_height_profile.push_back(slice_z); layer_height_profile.push_back(height); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE } - coordf_t last = std::max(slicing_params.first_object_layer_height, layer_height_profile[layer_height_profile.size() - 2]); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + double z_gap = slicing_params.object_print_z_height() - layer_height_profile[layer_height_profile.size() - 2]; + if (z_gap > 0.0) + { + layer_height_profile.push_back(slicing_params.object_print_z_height()); + layer_height_profile.push_back(clamp(slicing_params.min_layer_height, slicing_params.max_layer_height, z_gap)); + } +#else + double last = std::max(slicing_params.first_object_layer_height, layer_height_profile[layer_height_profile.size() - 2]); layer_height_profile.push_back(last); layer_height_profile.push_back(slicing_params.first_object_layer_height); layer_height_profile.push_back(slicing_params.object_print_z_height()); layer_height_profile.push_back(slicing_params.first_object_layer_height); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE return layer_height_profile; } +std::vector<double> smooth_height_profile(const std::vector<double>& profile, const SlicingParameters& slicing_params, const HeightProfileSmoothingParams& smoothing_params) +{ + auto gauss_blur = [&slicing_params](const std::vector<double>& profile, const HeightProfileSmoothingParams& smoothing_params) -> std::vector<double> { + auto gauss_kernel = [] (unsigned int radius) -> std::vector<double> { + unsigned int size = 2 * radius + 1; + std::vector<double> ret; + ret.reserve(size); + + // Reworked from static inline int getGaussianKernelSize(float sigma) taken from opencv-4.1.2\modules\features2d\src\kaze\AKAZEFeatures.cpp + double sigma = 0.3 * (double)(radius - 1) + 0.8; + double two_sq_sigma = 2.0 * sigma * sigma; + double inv_root_two_pi_sq_sigma = 1.0 / ::sqrt(M_PI * two_sq_sigma); + + for (unsigned int i = 0; i < size; ++i) + { + double x = (double)i - (double)radius; + ret.push_back(inv_root_two_pi_sq_sigma * ::exp(-x * x / two_sq_sigma)); + } + + return ret; + }; + + // skip first layer ? + size_t skip_count = slicing_params.first_object_layer_height_fixed() ? 4 : 0; + + // not enough data to smmoth + if ((int)profile.size() - (int)skip_count < 6) + return profile; + + unsigned int radius = std::max(smoothing_params.radius, (unsigned int)1); + std::vector<double> kernel = gauss_kernel(radius); + int two_radius = 2 * (int)radius; + + std::vector<double> ret; + size_t size = profile.size(); + ret.reserve(size); + + // leave first layer untouched + for (size_t i = 0; i < skip_count; ++i) + { + ret.push_back(profile[i]); + } + + // smooth the rest of the profile by biasing a gaussian blur + // the bias moves the smoothed profile closer to the min_layer_height + double delta_h = slicing_params.max_layer_height - slicing_params.min_layer_height; + double inv_delta_h = (delta_h != 0.0) ? 1.0 / delta_h : 1.0; + + double max_dz_band = (double)radius * slicing_params.layer_height; + for (size_t i = skip_count; i < size; i += 2) + { + double zi = profile[i]; + double hi = profile[i + 1]; + ret.push_back(zi); + ret.push_back(0.0); + double& height = ret.back(); + int begin = std::max((int)i - two_radius, (int)skip_count); + int end = std::min((int)i + two_radius, (int)size - 2); + double weight_total = 0.0; + for (int j = begin; j <= end; j += 2) + { + int kernel_id = radius + (j - (int)i) / 2; + double dz = std::abs(zi - profile[j]); + if (dz * slicing_params.layer_height <= max_dz_band) + { + double dh = std::abs(slicing_params.max_layer_height - profile[j + 1]); + double weight = kernel[kernel_id] * sqrt(dh * inv_delta_h); + height += weight * profile[j + 1]; + weight_total += weight; + } + } + + height = clamp(slicing_params.min_layer_height, slicing_params.max_layer_height, (weight_total != 0.0) ? height /= weight_total : hi); + if (smoothing_params.keep_min) + height = std::min(height, hi); + } + + return ret; + }; + + return gauss_blur(profile, smoothing_params); +} + void adjust_layer_height_profile( const SlicingParameters &slicing_params, std::vector<coordf_t> &layer_height_profile, @@ -609,7 +722,11 @@ int generate_layer_height_texture( const Vec3crd &color1 = palette_raw[idx1]; const Vec3crd &color2 = palette_raw[idx2]; coordf_t z = cell_to_z * coordf_t(cell); - assert(z >= lo && z <= hi); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + assert((lo - EPSILON <= z) && (z <= hi + EPSILON)); +#else + assert(z >= lo && z <= hi); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // Intensity profile to visualize the layers. coordf_t intensity = cos(M_PI * 0.7 * (mid - z) / h); // Color mapping from layer height to RGB. diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index 064363ec2..03ef7e67d 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -18,8 +18,12 @@ namespace Slic3r class PrintConfig; class PrintObjectConfig; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +class ModelObject; +#else class ModelVolume; typedef std::vector<ModelVolume*> ModelVolumePtrs; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // Parameters to guide object slicing and support generation. // The slicing parameters account for a raft and whether the 1st object layer is printed with a normal or a bridging flow @@ -138,11 +142,29 @@ extern std::vector<coordf_t> layer_height_profile_from_ranges( const SlicingParameters &slicing_params, const t_layer_config_ranges &layer_config_ranges); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +extern std::vector<double> layer_height_profile_adaptive( + const SlicingParameters& slicing_params, + const ModelObject& object, float cusp_value); + +struct HeightProfileSmoothingParams +{ + unsigned int radius; + bool keep_min; + + HeightProfileSmoothingParams() : radius(5), keep_min(false) {} + HeightProfileSmoothingParams(unsigned int radius, bool keep_min) : radius(radius), keep_min(keep_min) {} +}; + +extern std::vector<double> smooth_height_profile( + const std::vector<double>& profile, const SlicingParameters& slicing_params, + const HeightProfileSmoothingParams& smoothing_params); +#else extern std::vector<coordf_t> layer_height_profile_adaptive( const SlicingParameters &slicing_params, const t_layer_config_ranges &layer_config_ranges, const ModelVolumePtrs &volumes); - +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE enum LayerHeightEditActionType : unsigned int { LAYER_HEIGHT_EDIT_ACTION_INCREASE = 0, diff --git a/src/libslic3r/SlicingAdaptive.cpp b/src/libslic3r/SlicingAdaptive.cpp index ad03b550b..b6ebf1ac0 100644 --- a/src/libslic3r/SlicingAdaptive.cpp +++ b/src/libslic3r/SlicingAdaptive.cpp @@ -1,16 +1,22 @@ #include "libslic3r.h" +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +#include "Model.hpp" +#else #include "TriangleMesh.hpp" +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE #include "SlicingAdaptive.hpp" namespace Slic3r { +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE void SlicingAdaptive::clear() { - m_meshes.clear(); + m_meshes.clear(); m_faces.clear(); m_face_normal_z.clear(); } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE std::pair<float, float> face_z_span(const stl_facet *f) { @@ -21,38 +27,54 @@ std::pair<float, float> face_z_span(const stl_facet *f) void SlicingAdaptive::prepare() { - // 1) Collect faces of all meshes. - int nfaces_total = 0; - for (std::vector<const TriangleMesh*>::const_iterator it_mesh = m_meshes.begin(); it_mesh != m_meshes.end(); ++ it_mesh) +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + if (m_object == nullptr) + return; + + m_faces.clear(); + m_face_normal_z.clear(); + + m_mesh = m_object->raw_mesh(); + const ModelInstance* first_instance = m_object->instances.front(); + m_mesh.transform(first_instance->get_matrix(), first_instance->is_left_handed()); + + // 1) Collect faces from mesh. + m_faces.reserve(m_mesh.stl.stats.number_of_facets); + for (stl_facet& face : m_mesh.stl.facet_start) + { + face.normal.normalize(); + m_faces.emplace_back(&face); + } +#else + // 1) Collect faces of all meshes. + int nfaces_total = 0; + for (std::vector<const TriangleMesh*>::const_iterator it_mesh = m_meshes.begin(); it_mesh != m_meshes.end(); ++ it_mesh) nfaces_total += (*it_mesh)->stl.stats.number_of_facets; - m_faces.reserve(nfaces_total); - for (std::vector<const TriangleMesh*>::const_iterator it_mesh = m_meshes.begin(); it_mesh != m_meshes.end(); ++ it_mesh) - for (const stl_facet &face : (*it_mesh)->stl.facet_start) - m_faces.emplace_back(&face); + m_faces.reserve(nfaces_total); + for (std::vector<const TriangleMesh*>::const_iterator it_mesh = m_meshes.begin(); it_mesh != m_meshes.end(); ++ it_mesh) + for (const stl_facet& face : (*it_mesh)->stl.facet_start) + m_faces.emplace_back(&face); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // 2) Sort faces lexicographically by their Z span. - std::sort(m_faces.begin(), m_faces.end(), [](const stl_facet *f1, const stl_facet *f2) { - std::pair<float, float> span1 = face_z_span(f1); - std::pair<float, float> span2 = face_z_span(f2); - return span1 < span2; - }); + std::sort(m_faces.begin(), m_faces.end(), [](const stl_facet *f1, const stl_facet *f2) { return face_z_span(f1) < face_z_span(f2); }); // 3) Generate Z components of the facet normals. - m_face_normal_z.assign(m_faces.size(), 0.f); + m_face_normal_z.assign(m_faces.size(), 0.0f); for (size_t iface = 0; iface < m_faces.size(); ++ iface) m_face_normal_z[iface] = m_faces[iface]->normal(2); } float SlicingAdaptive::cusp_height(float z, float cusp_value, int ¤t_facet) { - float height = m_slicing_params.max_layer_height; + float height = (float)m_slicing_params.max_layer_height; bool first_hit = false; // find all facets intersecting the slice-layer int ordered_id = current_facet; for (; ordered_id < int(m_faces.size()); ++ ordered_id) { - std::pair<float, float> zspan = face_z_span(m_faces[ordered_id]); - // facet's minimum is higher than slice_z -> end loop + std::pair<float, float> zspan = face_z_span(m_faces[ordered_id]); + // facet's minimum is higher than slice_z -> end loop if (zspan.first >= z) break; // facet's maximum is higher than slice_z -> store the first event for next cusp_height call to begin at this point @@ -61,14 +83,14 @@ float SlicingAdaptive::cusp_height(float z, float cusp_value, int ¤t_facet if (! first_hit) { first_hit = true; current_facet = ordered_id; - } + } // skip touching facets which could otherwise cause small cusp values if (zspan.second <= z + EPSILON) continue; // compute cusp-height for this facet and store minimum of all heights float normal_z = m_face_normal_z[ordered_id]; - height = std::min(height, (normal_z == 0.f) ? 9999.f : std::abs(cusp_value / normal_z)); - } + height = std::min(height, (normal_z == 0.0f) ? (float)m_slicing_params.max_layer_height : std::abs(cusp_value / normal_z)); + } } // lower height limit due to printer capabilities @@ -77,8 +99,8 @@ float SlicingAdaptive::cusp_height(float z, float cusp_value, int ¤t_facet // check for sloped facets inside the determined layer and correct height if necessary if (height > m_slicing_params.min_layer_height) { for (; ordered_id < int(m_faces.size()); ++ ordered_id) { - std::pair<float, float> zspan = face_z_span(m_faces[ordered_id]); - // facet's minimum is higher than slice_z + height -> end loop + std::pair<float, float> zspan = face_z_span(m_faces[ordered_id]); + // facet's minimum is higher than slice_z + height -> end loop if (zspan.first >= z + height) break; @@ -88,13 +110,13 @@ float SlicingAdaptive::cusp_height(float z, float cusp_value, int ¤t_facet // Compute cusp-height for this facet and check against height. float normal_z = m_face_normal_z[ordered_id]; - float cusp = (normal_z == 0) ? 9999 : abs(cusp_value / normal_z); - + float cusp = (normal_z == 0.0f) ? (float)m_slicing_params.max_layer_height : std::abs(cusp_value / normal_z); + float z_diff = zspan.first - z; // handle horizontal facets - if (m_face_normal_z[ordered_id] > 0.999) { - // Slic3r::debugf "cusp computation, height is reduced from %f", $height; + if (normal_z > 0.999f) { + // Slic3r::debugf "cusp computation, height is reduced from %f", $height; height = z_diff; // Slic3r::debugf "to %f due to near horizontal facet\n", $height; } else if (cusp > z_diff) { @@ -112,29 +134,30 @@ float SlicingAdaptive::cusp_height(float z, float cusp_value, int ¤t_facet // lower height limit due to printer capabilities again height = std::max(height, float(m_slicing_params.min_layer_height)); } - + // Slic3r::debugf "cusp computation, layer-bottom at z:%f, cusp_value:%f, resulting layer height:%f\n", unscale $z, $cusp_value, $height; return height; } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // Returns the distance to the next horizontal facet in Z-dir // to consider horizontal object features in slice thickness float SlicingAdaptive::horizontal_facet_distance(float z) { for (size_t i = 0; i < m_faces.size(); ++ i) { - std::pair<float, float> zspan = face_z_span(m_faces[i]); - // facet's minimum is higher than max forward distance -> end loop + std::pair<float, float> zspan = face_z_span(m_faces[i]); + // facet's minimum is higher than max forward distance -> end loop if (zspan.first > z + m_slicing_params.max_layer_height) break; // min_z == max_z -> horizontal facet - if (zspan.first > z && zspan.first == zspan.second) + if ((zspan.first > z) && (zspan.first == zspan.second)) return zspan.first - z; } // objects maximum? - return (z + m_slicing_params.max_layer_height > m_slicing_params.object_print_z_height()) ? - std::max<float>(m_slicing_params.object_print_z_height() - z, 0.f) : - m_slicing_params.max_layer_height; + return (z + (float)m_slicing_params.max_layer_height > (float)m_slicing_params.object_print_z_height()) ? + std::max((float)m_slicing_params.object_print_z_height() - z, 0.f) : (float)m_slicing_params.max_layer_height; } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE }; // namespace Slic3r diff --git a/src/libslic3r/SlicingAdaptive.hpp b/src/libslic3r/SlicingAdaptive.hpp index bfd081d81..1d2996986 100644 --- a/src/libslic3r/SlicingAdaptive.hpp +++ b/src/libslic3r/SlicingAdaptive.hpp @@ -5,29 +5,49 @@ #include "Slicing.hpp" #include "admesh/stl.h" +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +#include "TriangleMesh.hpp" +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE namespace Slic3r { +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +class ModelVolume; +#else class TriangleMesh; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE class SlicingAdaptive { public: - void clear(); - void set_slicing_parameters(SlicingParameters params) { m_slicing_params = params; } - void add_mesh(const TriangleMesh *mesh) { m_meshes.push_back(mesh); } - void prepare(); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void clear(); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void set_slicing_parameters(SlicingParameters params) { m_slicing_params = params; } +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void set_object(const ModelObject& object) { m_object = &object; } +#else + void add_mesh(const TriangleMesh* mesh) { m_meshes.push_back(mesh); } +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void prepare(); float cusp_height(float z, float cusp_value, int ¤t_facet); - float horizontal_facet_distance(float z); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + float horizontal_facet_distance(float z); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE protected: SlicingParameters m_slicing_params; - std::vector<const TriangleMesh*> m_meshes; - // Collected faces of all meshes, sorted by raising Z of the bottom most face. +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + const ModelObject* m_object; + TriangleMesh m_mesh; +#else + std::vector<const TriangleMesh*> m_meshes; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + // Collected faces of all meshes, sorted by raising Z of the bottom most face. std::vector<const stl_facet*> m_faces; - // Z component of face normals, normalized. + // Z component of face normals, normalized. std::vector<float> m_face_normal_z; }; diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 5d0a7592c..0ec8b36ee 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -26,8 +26,6 @@ // Disable synchronization of unselected instances #define DISABLE_INSTANCES_SYNCH (0 && ENABLE_1_42_0_ALPHA1) -// Disable imgui dialog for move, rotate and scale gizmos -#define DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI (1 && ENABLE_1_42_0_ALPHA1) // Use wxDataViewRender instead of wxDataViewCustomRenderer #define ENABLE_NONCUSTOM_DATA_VIEW_RENDERING (0 && ENABLE_1_42_0_ALPHA1) @@ -38,8 +36,21 @@ #define ENABLE_2_2_0_ALPHA1 1 // Enable thumbnail generator +// When removing this technology, remove it also from stable branch, +// where it has been partially copied for patch 2.1.1 #define ENABLE_THUMBNAIL_GENERATOR (1 && ENABLE_2_2_0_ALPHA1) #define ENABLE_THUMBNAIL_GENERATOR_DEBUG (0 && ENABLE_THUMBNAIL_GENERATOR) -#define ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE (1 && ENABLE_THUMBNAIL_GENERATOR) + +// Enable adaptive layer height profile +#define ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE (1 && ENABLE_2_2_0_ALPHA1) + +// Enable grayed variant for gizmos icons in non activable state +#define ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE (1 && ENABLE_2_2_0_ALPHA1) + +// Enable fix for view toolbar background not showing up on Mac with dark mode +#define ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX (1 && ENABLE_2_2_0_ALPHA1) + +// Enable selection for missing files in reload from disk command +#define ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION (1 && ENABLE_2_2_0_ALPHA1) #endif // _technologies_h_ diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 9c9f82040..ef935455e 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -198,6 +198,29 @@ private: void make_expolygons(std::vector<IntersectionLine> &lines, const float closing_radius, ExPolygons* slices) const; }; +inline void slice_mesh( + const TriangleMesh & mesh, + const std::vector<float> & z, + std::vector<Polygons> & layers, + TriangleMeshSlicer::throw_on_cancel_callback_type thr = nullptr) +{ + if (mesh.empty()) return; + TriangleMeshSlicer slicer(&mesh); + slicer.slice(z, &layers, thr); +} + +inline void slice_mesh( + const TriangleMesh & mesh, + const std::vector<float> & z, + std::vector<ExPolygons> & layers, + float closing_radius, + TriangleMeshSlicer::throw_on_cancel_callback_type thr = nullptr) +{ + if (mesh.empty()) return; + TriangleMeshSlicer slicer(&mesh); + slicer.slice(z, closing_radius, &layers, thr); +} + TriangleMesh make_cube(double x, double y, double z); // Generate a TriangleMesh of a cylinder diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 6368388c4..859c17e89 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -1,5 +1,5 @@ +cmake_minimum_required(VERSION 3.8) project(libslic3r_gui) -cmake_minimum_required(VERSION 2.6) include(PrecompiledHeader) @@ -107,6 +107,8 @@ set(SLIC3R_GUI_SOURCES GUI/Camera.hpp GUI/wxExtensions.cpp GUI/wxExtensions.hpp + GUI/ExtruderSequenceDialog.cpp + GUI/ExtruderSequenceDialog.hpp GUI/WipeTowerDialog.cpp GUI/WipeTowerDialog.hpp GUI/RammingChart.cpp @@ -138,6 +140,8 @@ set(SLIC3R_GUI_SOURCES GUI/ProgressStatusBar.cpp GUI/PrintHostDialogs.cpp GUI/PrintHostDialogs.hpp + GUI/Mouse3DController.cpp + GUI/Mouse3DController.hpp Utils/Http.cpp Utils/Http.hpp Utils/FixModelByWin10.cpp @@ -172,7 +176,7 @@ add_library(libslic3r_gui STATIC ${SLIC3R_GUI_SOURCES}) encoding_check(libslic3r_gui) -target_link_libraries(libslic3r_gui libslic3r avrdude cereal imgui ${GLEW_LIBRARIES}) +target_link_libraries(libslic3r_gui libslic3r avrdude cereal imgui GLEW::GLEW OpenGL::GL OpenGL::GLU hidapi) if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE) endif () diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp index da522dd5e..fcebf88d1 100644 --- a/src/slic3r/Config/Version.cpp +++ b/src/slic3r/Config/Version.cpp @@ -286,16 +286,21 @@ Index::const_iterator Index::find(const Semver &ver) const return (it == m_configs.end() || it->config_version == ver) ? it : m_configs.end(); } -Index::const_iterator Index::recommended() const +Index::const_iterator Index::recommended(const Semver &slic3r_version) const { const_iterator highest = this->end(); for (const_iterator it = this->begin(); it != this->end(); ++ it) - if (it->is_current_slic3r_supported() && + if (it->is_slic3r_supported(slic3r_version) && (highest == this->end() || highest->config_version < it->config_version)) highest = it; return highest; } +Index::const_iterator Index::recommended() const +{ + return this->recommended(Slic3r::SEMVER); +} + std::vector<Index> Index::load_db() { boost::filesystem::path cache_dir = boost::filesystem::path(Slic3r::data_dir()) / "cache"; diff --git a/src/slic3r/Config/Version.hpp b/src/slic3r/Config/Version.hpp index 19c565ffb..e5f1a2e21 100644 --- a/src/slic3r/Config/Version.hpp +++ b/src/slic3r/Config/Version.hpp @@ -71,6 +71,8 @@ public: // Returns configs().end() if such version does not exist in the index. This shall never happen // if the index is valid. const_iterator recommended() const; + // Recommended config for a provided slic3r version. Used when checking for slic3r update (slic3r_version is the old one read out from PrusaSlicer.ini) + const_iterator recommended(const Semver &slic3r_version) const; // Returns the filesystem path from which this index has originally been loaded const boost::filesystem::path& path() const { return m_path; } diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index d0bab50c6..dce7c4c99 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -171,6 +171,7 @@ void Bed3D::Axes::render() const glsafe(::glPopMatrix()); glsafe(::glDisable(GL_LIGHTING)); + glsafe(::glDisable(GL_DEPTH_TEST)); } void Bed3D::Axes::render_axis(double length) const @@ -264,11 +265,14 @@ Point Bed3D::point_projection(const Point& point) const return m_polygon.point_projection(point); } -void Bed3D::render(GLCanvas3D& canvas, float theta, float scale_factor) const +void Bed3D::render(GLCanvas3D& canvas, float theta, float scale_factor, bool show_axes) const { m_scale_factor = scale_factor; - render_axes(); + if (show_axes) + render_axes(); + + glsafe(::glEnable(GL_DEPTH_TEST)); switch (m_type) { @@ -280,6 +284,8 @@ void Bed3D::render(GLCanvas3D& canvas, float theta, float scale_factor) const default: case Custom: { render_custom(canvas, theta > 90.0f); break; } } + + glsafe(::glDisable(GL_DEPTH_TEST)); } void Bed3D::calc_bounding_boxes() const diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 132836711..19ecddc0a 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -112,7 +112,7 @@ public: bool contains(const Point& point) const; Point point_projection(const Point& point) const; - void render(GLCanvas3D& canvas, float theta, float scale_factor) const; + void render(GLCanvas3D& canvas, float theta, float scale_factor, bool show_axes) const; private: void calc_bounding_boxes() const; diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 8c5040eee..5d559c246 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -303,6 +303,8 @@ public: int instance_id; bool operator==(const CompositeID &rhs) const { return object_id == rhs.object_id && volume_id == rhs.volume_id && instance_id == rhs.instance_id; } bool operator!=(const CompositeID &rhs) const { return ! (*this == rhs); } + bool operator< (const CompositeID &rhs) const + { return object_id < rhs.object_id || (object_id == rhs.object_id && (volume_id < rhs.volume_id || (volume_id == rhs.volume_id && instance_id < rhs.instance_id))); } }; CompositeID composite_id; // Fingerprint of the source geometry. For ModelVolumes, it is the ModelVolume::ID and ModelInstanceID, diff --git a/src/slic3r/GUI/AppConfig.cpp b/src/slic3r/GUI/AppConfig.cpp index 6b3f54f3a..d33d945ef 100644 --- a/src/slic3r/GUI/AppConfig.cpp +++ b/src/slic3r/GUI/AppConfig.cpp @@ -103,7 +103,7 @@ void AppConfig::load() // Error while parsing config file. We'll customize the error message and rethrow to be displayed. throw std::runtime_error( _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manualy delete the file to recover from the error. Your user profiles will not be affected.")) + + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); } @@ -271,6 +271,80 @@ void AppConfig::set_recent_projects(const std::vector<std::string>& recent_proje } } +void AppConfig::set_mouse_device(const std::string& name, double translation_speed, double translation_deadzone, float rotation_speed, float rotation_deadzone) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + it = m_storage.insert(std::map<std::string, std::map<std::string, std::string>>::value_type(key, std::map<std::string, std::string>())).first; + + it->second.clear(); + it->second["translation_speed"] = std::to_string(translation_speed); + it->second["translation_deadzone"] = std::to_string(translation_deadzone); + it->second["rotation_speed"] = std::to_string(rotation_speed); + it->second["rotation_deadzone"] = std::to_string(rotation_deadzone); +} + +bool AppConfig::get_mouse_device_translation_speed(const std::string& name, double& speed) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + return false; + + auto it_val = it->second.find("translation_speed"); + if (it_val == it->second.end()) + return false; + + speed = ::atof(it_val->second.c_str()); + return true; +} + +bool AppConfig::get_mouse_device_translation_deadzone(const std::string& name, double& deadzone) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + return false; + + auto it_val = it->second.find("translation_deadzone"); + if (it_val == it->second.end()) + return false; + + deadzone = ::atof(it_val->second.c_str()); + return true; +} + +bool AppConfig::get_mouse_device_rotation_speed(const std::string& name, float& speed) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + return false; + + auto it_val = it->second.find("rotation_speed"); + if (it_val == it->second.end()) + return false; + + speed = (float)::atof(it_val->second.c_str()); + return true; +} + +bool AppConfig::get_mouse_device_rotation_deadzone(const std::string& name, float& deadzone) +{ + std::string key = std::string("mouse_device:") + name; + auto it = m_storage.find(key); + if (it == m_storage.end()) + return false; + + auto it_val = it->second.find("rotation_deadzone"); + if (it_val == it->second.end()) + return false; + + deadzone = (float)::atof(it_val->second.c_str()); + return true; +} + void AppConfig::update_config_dir(const std::string &dir) { this->set("recent", "config_directory", dir); diff --git a/src/slic3r/GUI/AppConfig.hpp b/src/slic3r/GUI/AppConfig.hpp index 97c369ab6..355370450 100644 --- a/src/slic3r/GUI/AppConfig.hpp +++ b/src/slic3r/GUI/AppConfig.hpp @@ -131,8 +131,15 @@ public: std::vector<std::string> get_recent_projects() const; void set_recent_projects(const std::vector<std::string>& recent_projects); + void set_mouse_device(const std::string& name, double translation_speed, double translation_deadzone, float rotation_speed, float rotation_deadzone); + bool get_mouse_device_translation_speed(const std::string& name, double& speed); + bool get_mouse_device_translation_deadzone(const std::string& name, double& deadzone); + bool get_mouse_device_rotation_speed(const std::string& name, float& speed); + bool get_mouse_device_rotation_deadzone(const std::string& name, float& deadzone); + static const std::string SECTION_FILAMENTS; static const std::string SECTION_MATERIALS; + private: // Map of section, name -> value std::map<std::string, std::map<std::string, std::string>> m_storage; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 5ab65f340..e587509ac 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -20,9 +20,6 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/GCode/PostProcessor.hpp" #include "libslic3r/GCode/PreviewData.hpp" -#if ENABLE_THUMBNAIL_GENERATOR -#include "libslic3r/GCode/ThumbnailData.hpp" -#endif // ENABLE_THUMBNAIL_GENERATOR #include "libslic3r/libslic3r.h" #include <cassert> @@ -36,6 +33,7 @@ #include <boost/log/trivial.hpp> #include <boost/nowide/cstdio.hpp> #include "I18N.hpp" +#include "GUI.hpp" namespace Slic3r { @@ -91,11 +89,19 @@ void BackgroundSlicingProcess::process_fff() m_print->process(); wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_slicing_completed_id)); #if ENABLE_THUMBNAIL_GENERATOR - m_fff_print->export_gcode(m_temp_output_path, m_gcode_preview_data, m_thumbnail_data); + m_fff_print->export_gcode(m_temp_output_path, m_gcode_preview_data, m_thumbnail_cb); #else m_fff_print->export_gcode(m_temp_output_path, m_gcode_preview_data); #endif // ENABLE_THUMBNAIL_GENERATOR - if (this->set_step_started(bspsGCodeFinalize)) { + + if (m_fff_print->model().custom_gcode_per_height != GUI::wxGetApp().model().custom_gcode_per_height) { + GUI::wxGetApp().model().custom_gcode_per_height = m_fff_print->model().custom_gcode_per_height; + // #ys_FIXME : controll text + GUI::show_info(nullptr, _(L("To except of redundant tool manipulation, \n" + "Color change(s) for unused extruder(s) was(were) deleted")), _(L("Info"))); + } + + if (this->set_step_started(bspsGCodeFinalize)) { if (! m_export_path.empty()) { //FIXME localize the messages // Perform the final post-processing of the export path by applying the print statistics over the file name. @@ -111,7 +117,7 @@ void BackgroundSlicingProcess::process_fff() m_print->set_status(100, _utf8(L("Slicing complete"))); } this->set_step_done(bspsGCodeFinalize); - } + } } #if ENABLE_THUMBNAIL_GENERATOR @@ -139,9 +145,12 @@ void BackgroundSlicingProcess::process_sla() m_sla_print->export_raster(zipper); #if ENABLE_THUMBNAIL_GENERATOR - if (m_thumbnail_data != nullptr) + if (m_thumbnail_cb != nullptr) { - for (const ThumbnailData& data : *m_thumbnail_data) + ThumbnailsList thumbnails; + m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true); +// m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, false, true, true); // renders also supports and pad + for (const ThumbnailData& data : thumbnails) { if (data.is_valid()) write_thumbnail(zipper, data); @@ -461,9 +470,12 @@ void BackgroundSlicingProcess::prepare_upload() Zipper zipper{source_path.string()}; m_sla_print->export_raster(zipper, m_upload_job.upload_data.upload_path.string()); #if ENABLE_THUMBNAIL_GENERATOR - if (m_thumbnail_data != nullptr) + if (m_thumbnail_cb != nullptr) { - for (const ThumbnailData& data : *m_thumbnail_data) + ThumbnailsList thumbnails; + m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, true, true, true); +// m_thumbnail_cb(thumbnails, current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values, true, false, true, true); // renders also supports and pad + for (const ThumbnailData& data : thumbnails) { if (data.is_valid()) write_thumbnail(zipper, data); diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index a603d52ac..a66dcf39c 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -17,9 +17,6 @@ namespace Slic3r { class DynamicPrintConfig; class GCodePreviewData; -#if ENABLE_THUMBNAIL_GENERATOR -struct ThumbnailData; -#endif // ENABLE_THUMBNAIL_GENERATOR class Model; class SLAPrint; @@ -53,7 +50,7 @@ public: void set_sla_print(SLAPrint *print) { m_sla_print = print; } void set_gcode_preview_data(GCodePreviewData *gpd) { m_gcode_preview_data = gpd; } #if ENABLE_THUMBNAIL_GENERATOR - void set_thumbnail_data(const std::vector<ThumbnailData>* data) { m_thumbnail_data = data; } + void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; } #endif // ENABLE_THUMBNAIL_GENERATOR // The following wxCommandEvent will be sent to the UI thread / Platter window, when the slicing is finished @@ -135,6 +132,11 @@ public: // This "finished" flag does not account for the final export of the output file (.gcode or zipped PNGs), // and it does not account for the OctoPrint scheduling. bool finished() const { return m_print->finished(); } + + void set_force_update_print_regions(bool force_update_print_regions) { + if (m_fff_print) + m_fff_print->set_force_update_print_regions(force_update_print_regions); + } private: void thread_proc(); @@ -159,8 +161,8 @@ private: // Data structure, to which the G-code export writes its annotations. GCodePreviewData *m_gcode_preview_data = nullptr; #if ENABLE_THUMBNAIL_GENERATOR - // Data structures, used to write thumbnails into gcode. - const std::vector<ThumbnailData>* m_thumbnail_data = nullptr; + // Callback function, used to write thumbnails into gcode. + ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; #endif // ENABLE_THUMBNAIL_GENERATOR // Temporary G-code, there is one defined for the BackgroundSlicingProcess, differentiated from the other processes by a process ID. std::string m_temp_output_path; diff --git a/src/slic3r/GUI/Camera.cpp b/src/slic3r/GUI/Camera.cpp index 9fbabe930..bb514e888 100644 --- a/src/slic3r/GUI/Camera.cpp +++ b/src/slic3r/GUI/Camera.cpp @@ -91,10 +91,16 @@ void Camera::select_next_type() void Camera::set_target(const Vec3d& target) { - m_target = target; - m_target(0) = clamp(m_scene_box.min(0), m_scene_box.max(0), m_target(0)); - m_target(1) = clamp(m_scene_box.min(1), m_scene_box.max(1), m_target(1)); - m_target(2) = clamp(m_scene_box.min(2), m_scene_box.max(2), m_target(2)); + BoundingBoxf3 test_box = m_scene_box; + test_box.translate(-m_scene_box.center()); + // We may let this factor be customizable + static const double ScaleFactor = 1.5; + test_box.scale(ScaleFactor); + test_box.translate(m_scene_box.center()); + + m_target(0) = clamp(test_box.min(0), test_box.max(0), target(0)); + m_target(1) = clamp(test_box.min(1), test_box.max(1), target(1)); + m_target(2) = clamp(test_box.min(2), test_box.max(2), target(2)); } void Camera::set_theta(float theta, bool apply_limit) @@ -109,20 +115,20 @@ void Camera::set_theta(float theta, bool apply_limit) } } -void Camera::set_zoom(double zoom, const BoundingBoxf3& max_box, int canvas_w, int canvas_h) +void Camera::update_zoom(double delta_zoom) { - zoom = std::max(std::min(zoom, 4.0), -4.0) / 10.0; - zoom = m_zoom / (1.0 - zoom); + set_zoom(m_zoom / (1.0 - std::max(std::min(delta_zoom, 4.0), -4.0) * 0.1)); +} +void Camera::set_zoom(double zoom) +{ // Don't allow to zoom too far outside the scene. - double zoom_min = calc_zoom_to_bounding_box_factor(max_box, canvas_w, canvas_h); + double zoom_min = calc_zoom_to_bounding_box_factor(m_scene_box, (int)m_viewport[2], (int)m_viewport[3]); if (zoom_min > 0.0) zoom = std::max(zoom, zoom_min * 0.7); // Don't allow to zoom too close to the scene. - zoom = std::min(zoom, 100.0); - - m_zoom = zoom; + m_zoom = std::min(zoom, 100.0); } bool Camera::select_view(const std::string& direction) @@ -190,7 +196,7 @@ void Camera::apply_view_matrix() const glsafe(::glGetDoublev(GL_MODELVIEW_MATRIX, m_view_matrix.data())); } -void Camera::apply_projection(const BoundingBoxf3& box) const +void Camera::apply_projection(const BoundingBoxf3& box, double near_z, double far_z) const { set_distance(DefaultDistance); @@ -201,6 +207,12 @@ void Camera::apply_projection(const BoundingBoxf3& box) const { m_frustrum_zs = calc_tight_frustrum_zs_around(box); + if (near_z > 0.0) + m_frustrum_zs.first = std::min(m_frustrum_zs.first, near_z); + + if (far_z > 0.0) + m_frustrum_zs.second = std::max(m_frustrum_zs.second, far_z); + w = 0.5 * (double)m_viewport[2]; h = 0.5 * (double)m_viewport[3]; @@ -310,7 +322,6 @@ void Camera::zoom_to_volumes(const GLVolumePtrs& volumes, int canvas_w, int canv void Camera::debug_render() const { ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.set_next_window_bg_alpha(0.5f); imgui.begin(std::string("Camera statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); std::string type = get_type_as_string(); @@ -355,27 +366,10 @@ std::pair<double, double> Camera::calc_tight_frustrum_zs_around(const BoundingBo while (true) { - ret = std::make_pair(DBL_MAX, -DBL_MAX); - - // box vertices in world space - std::vector<Vec3d> vertices; - vertices.reserve(8); - vertices.push_back(box.min); - vertices.emplace_back(box.max(0), box.min(1), box.min(2)); - vertices.emplace_back(box.max(0), box.max(1), box.min(2)); - vertices.emplace_back(box.min(0), box.max(1), box.min(2)); - vertices.emplace_back(box.min(0), box.min(1), box.max(2)); - vertices.emplace_back(box.max(0), box.min(1), box.max(2)); - vertices.push_back(box.max); - vertices.emplace_back(box.min(0), box.max(1), box.max(2)); - - // set the Z range in eye coordinates (negative Zs are in front of the camera) - for (const Vec3d& v : vertices) - { - double z = -(m_view_matrix * v)(2); - ret.first = std::min(ret.first, z); - ret.second = std::max(ret.second, z); - } + // box in eye space + BoundingBoxf3 eye_box = box.transformed(m_view_matrix); + ret.first = -eye_box.max(2); + ret.second = -eye_box.min(2); // apply margin ret.first -= FrustrumZMargin; @@ -434,8 +428,10 @@ double Camera::calc_zoom_to_bounding_box_factor(const BoundingBoxf3& box, int ca vertices.push_back(box.max); vertices.emplace_back(box.min(0), box.max(1), box.max(2)); - double max_x = 0.0; - double max_y = 0.0; + double min_x = DBL_MAX; + double min_y = DBL_MAX; + double max_x = -DBL_MAX; + double max_y = -DBL_MAX; #if !ENABLE_THUMBNAIL_GENERATOR // margin factor to give some empty space around the box @@ -452,17 +448,24 @@ double Camera::calc_zoom_to_bounding_box_factor(const BoundingBoxf3& box, int ca double x_on_plane = proj_on_plane.dot(right); double y_on_plane = proj_on_plane.dot(up); - max_x = std::max(max_x, std::abs(x_on_plane)); - max_y = std::max(max_y, std::abs(y_on_plane)); + min_x = std::min(min_x, x_on_plane); + min_y = std::min(min_y, y_on_plane); + max_x = std::max(max_x, x_on_plane); + max_y = std::max(max_y, y_on_plane); } - if ((max_x == 0.0) || (max_y == 0.0)) + double dx = max_x - min_x; + double dy = max_y - min_y; + if ((dx <= 0.0) || (dy <= 0.0)) return -1.0f; - max_x *= margin_factor; - max_y *= margin_factor; + double med_x = 0.5 * (max_x + min_x); + double med_y = 0.5 * (max_y + min_y); + + dx *= margin_factor; + dy *= margin_factor; - return std::min((double)canvas_w / (2.0 * max_x), (double)canvas_h / (2.0 * max_y)); + return std::min((double)canvas_w / dx, (double)canvas_h / dy); } #if ENABLE_THUMBNAIL_GENERATOR @@ -524,7 +527,7 @@ double Camera::calc_zoom_to_volumes_factor(const GLVolumePtrs& volumes, int canv double dx = margin_factor * (max_x - min_x); double dy = margin_factor * (max_y - min_y); - if ((dx == 0.0) || (dy == 0.0)) + if ((dx <= 0.0) || (dy <= 0.0)) return -1.0f; return std::min((double)canvas_w / dx, (double)canvas_h / dy); diff --git a/src/slic3r/GUI/Camera.hpp b/src/slic3r/GUI/Camera.hpp index cb634138f..6cd1b75a5 100644 --- a/src/slic3r/GUI/Camera.hpp +++ b/src/slic3r/GUI/Camera.hpp @@ -70,8 +70,8 @@ public: void set_theta(float theta, bool apply_limit); double get_zoom() const { return m_zoom; } - void set_zoom(double zoom, const BoundingBoxf3& max_box, int canvas_w, int canvas_h); - void set_zoom(double zoom) { m_zoom = zoom; } + void update_zoom(double delta_zoom); + void set_zoom(double zoom); const BoundingBoxf3& get_scene_box() const { return m_scene_box; } void set_scene_box(const BoundingBoxf3& box) { m_scene_box = box; } @@ -95,7 +95,9 @@ public: void apply_viewport(int x, int y, unsigned int w, unsigned int h) const; void apply_view_matrix() const; - void apply_projection(const BoundingBoxf3& box) const; + // Calculates and applies the projection matrix tighting the frustrum z range around the given box. + // If larger z span is needed, pass the desired values of near and far z (negative values are ignored) + void apply_projection(const BoundingBoxf3& box, double near_z = -1.0, double far_z = -1.0) const; #if ENABLE_THUMBNAIL_GENERATOR void zoom_to_box(const BoundingBoxf3& box, int canvas_w, int canvas_h, double margin_factor = DefaultZoomToBoxMarginFactor); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f76f752f0..966f34761 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -77,7 +77,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con "- no top solid layers\n" "- 0% fill density\n" "- no support material\n" - "- no ensure_vertical_shell_thickness")); + "- inactive Ensure vertical shell thickness")); if (is_global_config) msg_text += "\n\n" + _(L("Shall I adjust those settings in order to enable Spiral Vase?")); wxMessageDialog dialog(nullptr, msg_text, _(L("Spiral Vase")), diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 393d973b7..971bd1f1d 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -374,7 +374,7 @@ ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxStrin sizer->AddSpacer(10); content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1); + sizer->Add(content, 1, wxEXPAND); SetSizer(sizer); @@ -504,7 +504,8 @@ void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) { if (technology == T_FFF && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0) { + && printer_pickers.size() > 0 + && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { printer_pickers[0]->select_one(0, true); } } @@ -528,15 +529,17 @@ PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxStrin list_l2->SetMinSize(wxSize(13*em, list_h)); list_l3->SetMinSize(wxSize(25*em, list_h)); - auto *grid = new wxFlexGridSizer(3, 0, em); + auto *grid = new wxFlexGridSizer(3, em/2, em); + grid->AddGrowableCol(2, 1); + grid->AddGrowableRow(1, 1); grid->Add(new wxStaticText(this, wxID_ANY, list1name)); grid->Add(new wxStaticText(this, wxID_ANY, _(L("Vendor:")))); grid->Add(new wxStaticText(this, wxID_ANY, _(L("Profile:")))); - grid->Add(list_l1); - grid->Add(list_l2); - grid->Add(list_l3); + grid->Add(list_l1, 0, wxEXPAND); + grid->Add(list_l2, 0, wxEXPAND); + grid->Add(list_l3, 1, wxEXPAND); auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); auto *sel_all = new wxButton(this, wxID_ANY, _(L("All"))); @@ -548,7 +551,7 @@ PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxStrin grid->Add(new wxBoxSizer(wxHORIZONTAL)); grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - append(grid); + append(grid, 1, wxEXPAND); list_l1->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { update_lists(list_l1->GetSelection(), list_l2->GetSelection()); @@ -626,10 +629,28 @@ void PageMaterials::update_lists(int sel1, int sel2) const std::string &vendor = list_l2->get_data(sel2); materials->filter_presets(type, vendor, [this](const Preset *p) { - const int i = list_l3->append(p->name, p); - const bool checked = wizard_p()->appconfig_new.has(materials->appconfig_section(), p->name); - list_l3->Check(i, checked); - }); + bool was_checked = false; + + int cur_i = list_l3->find(p->alias); + if (cur_i == wxNOT_FOUND) + cur_i = list_l3->append(p->alias, &p->alias); + else + was_checked = list_l3->IsChecked(cur_i); + + const std::string& section = materials->appconfig_section(); + + const bool checked = wizard_p()->appconfig_new.has(section, p->name); + list_l3->Check(cur_i, checked | was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + } ); } sel2_prev = sel2; @@ -639,13 +660,9 @@ void PageMaterials::update_lists(int sel1, int sel2) void PageMaterials::select_material(int i) { const bool checked = list_l3->IsChecked(i); - const Preset &preset = list_l3->get_data(i); - if (checked) { - wizard_p()->appconfig_new.set(materials->appconfig_section(), preset.name, "1"); - } else { - wizard_p()->appconfig_new.erase(materials->appconfig_section(), preset.name); - } + const std::string& alias_key = list_l3->get_data(i); + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); } void PageMaterials::select_all(bool select) @@ -753,8 +770,8 @@ PageMode::PageMode(ConfigWizard *parent) { append_text(_(L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " - "The other two offer progressivly more sophisticated fine-tuning, " - "they are suitable for advanced and expert usiser, respectively."))); + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively."))); radio_simple = new wxRadioButton(this, wxID_ANY, _(L("Simple mode"))); radio_advanced = new wxRadioButton(this, wxID_ANY, _(L("Advanced mode"))); @@ -777,11 +794,17 @@ void PageMode::on_activate() void PageMode::serialize_mode(AppConfig *app_config) const { - const char *mode = "simple"; + std::string mode = ""; + if (radio_simple->GetValue()) { mode = "simple"; } if (radio_advanced->GetValue()) { mode = "advanced"; } if (radio_expert->GetValue()) { mode = "expert"; } + // If "Mode" page wasn't selected (no one radiobutton is checked), + // we shouldn't to update a view_mode value in app_config + if (mode.empty()) + return; + app_config->set("view_mode", mode); } @@ -790,7 +813,7 @@ PageVendors::PageVendors(ConfigWizard *parent) { const AppConfig &appconfig = this->wizard_p()->appconfig_new; - append_text(wxString::Format(_(L("Pick another vendor supported by %s: (FIXME: this text)")), SLIC3R_APP_NAME)); + append_text(wxString::Format(_(L("Pick another vendor supported by %s")), SLIC3R_APP_NAME) + ":"); auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); boldfont.SetWeight(wxFONTWEIGHT_BOLD); @@ -809,9 +832,11 @@ PageVendors::PageVendors(ConfigWizard *parent) if (enabled) { cbox->SetValue(true); - auto pair = wizard_p()->pages_3rdparty.find(vendor->id); - wxCHECK_RET(pair != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); - pair->second->install = true; + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; } append(cbox); @@ -1221,7 +1246,7 @@ const std::string Materials::UNKNOWN = "(Unknown)"; void Materials::push(const Preset *preset) { - presets.insert(preset); + presets.push_back(preset); types.insert(technology & T_FFF ? Materials::get_filament_type(preset) : Materials::get_material_type(preset)); @@ -1309,9 +1334,10 @@ void ConfigWizard::priv::load_pages() index->add_page(page_fff); index->add_page(page_msla); index->add_page(page_vendors); - for (const auto &pair : pages_3rdparty) { - PagePrinters *page = pair.second; - if (page->install) { index->add_page(page); } + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && page->install) + index->add_page(page); } index->add_page(page_custom); @@ -1326,6 +1352,9 @@ void ConfigWizard::priv::load_pages() if (any_fff_selected) { index->add_page(page_filaments); } if (any_sla_selected) { index->add_page(page_sla_materials); } + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected); + index->add_page(page_update); index->add_page(page_mode); @@ -1384,8 +1413,6 @@ void ConfigWizard::priv::load_vendors() pair.second.preset_bundle->load_installed_printers(appconfig_new); } - update_materials(T_ANY); - if (app_config->has_section(AppConfig::SECTION_FILAMENTS)) { appconfig_new.set_section(AppConfig::SECTION_FILAMENTS, app_config->get_section(AppConfig::SECTION_FILAMENTS)); } @@ -1396,7 +1423,8 @@ void ConfigWizard::priv::load_vendors() void ConfigWizard::priv::add_page(ConfigWizardPage *page) { - hscroll_sizer->Add(page, 0, wxEXPAND); + const int proportion = (page->shortname == _(L("Filaments"))) || (page->shortname == _(L("SLA Materials"))) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); all_pages.push_back(page); } @@ -1409,10 +1437,22 @@ void ConfigWizard::priv::enable_next(bool enable) void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) { switch (start_page) { - case ConfigWizard::SP_PRINTERS: index->go_to(page_fff); break; - case ConfigWizard::SP_FILAMENTS: index->go_to(page_filaments); break; - case ConfigWizard::SP_MATERIALS: index->go_to(page_sla_materials); break; - default: index->go_to(page_welcome); break; + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; } } @@ -1422,10 +1462,31 @@ void ConfigWizard::priv::create_3rdparty_pages() const VendorProfile *vendor = pair.second.vendor_profile; if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - auto *page = new PagePrinters(q, vendor->name, vendor->name, *vendor, 1, T_ANY); - add_page(page); + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_(L("FFF Technology Printers")), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _(L("SLA Technology Printers")), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } - pages_3rdparty.insert({vendor->id, page}); + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); } } @@ -1441,52 +1502,54 @@ void ConfigWizard::priv::update_materials(Technology technology) { if (any_fff_selected && (technology & T_FFF)) { filaments.clear(); + aliases_fff.clear(); // Iterate filaments in all bundles for (const auto &pair : bundles) { for (const auto &filament : pair.second.preset_bundle->filaments) { // Check if filament is already added - if (filaments.containts(&filament)) { continue; } - + if (filaments.containts(&filament)) + continue; // Iterate printers in all bundles - for (const auto &pair : bundles) { - for (const auto &printer : pair.second.preset_bundle->printers) { + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. +// for (const auto &pair : bundles) + for (const auto &printer : pair.second.preset_bundle->printers) // Filter out inapplicable printers - if (!printer.is_visible || printer.printer_technology() != ptFFF) { - continue; - } - - if (filament.is_compatible_with_printer(printer)) { + if (printer.is_visible && printer.printer_technology() == ptFFF && + is_compatible_with_printer(PresetWithVendorProfile(filament, nullptr), PresetWithVendorProfile(printer, nullptr)) && + // Check if filament is already added + ! filaments.containts(&filament)) { filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); } - } - } } } } if (any_sla_selected && (technology & T_SLA)) { sla_materials.clear(); + aliases_sla.clear(); // Iterate SLA materials in all bundles for (const auto &pair : bundles) { for (const auto &material : pair.second.preset_bundle->sla_materials) { // Check if material is already added - if (sla_materials.containts(&material)) { continue; } - + if (sla_materials.containts(&material)) + continue; // Iterate printers in all bundles - for (const auto &pair : bundles) { - for (const auto &printer : pair.second.preset_bundle->printers) { + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. +// for (const auto &pair : bundles) + for (const auto &printer : pair.second.preset_bundle->printers) // Filter out inapplicable printers - if (!printer.is_visible || printer.printer_technology() != ptSLA) { - continue; - } - - if (material.is_compatible_with_printer(printer)) { + if (printer.is_visible && printer.printer_technology() == ptSLA && + is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr)) && + // Check if material is already added + ! sla_materials.containts(&material)) { sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); } - } - } } } } @@ -1499,10 +1562,10 @@ void ConfigWizard::priv::on_custom_setup() void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) { - if (page_msla->any_selected() != any_sla_selected || - page_fff->any_selected() != any_fff_selected) { - any_fff_selected = page_fff->any_selected(); - any_sla_selected = page_msla->any_selected(); + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); load_pages(); } @@ -1519,9 +1582,9 @@ void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPicker } } - if (page == page_fff) { + if (page->technology & T_FFF) { page_filaments->clear(); - } else if (page == page_msla) { + } else if (page->technology & T_SLA) { page_sla_materials->clear(); } } @@ -1530,17 +1593,48 @@ void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool i { auto it = pages_3rdparty.find(vendor->id); wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); - PagePrinters *page = it->second; - if (page->install && !install) { - page->select_all(false); - } - page->install = install; - page->Layout(); + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + page->Layout(); + } load_pages(); } +bool ConfigWizard::priv::check_material_config(Technology technology) +{ + const auto exist_preset = [this](const std::string& section, const Materials& materials) + { + if (appconfig_new.has_section(section) && + !appconfig_new.get_section(section).empty()) + { + const std::map<std::string, std::string>& appconfig_presets = appconfig_new.get_section(section); + for (const auto& preset : appconfig_presets) + if (materials.exist_preset(preset.first)) + return true; + } + return false; + }; + + if (any_fff_selected && technology & T_FFF && !exist_preset(AppConfig::SECTION_FILAMENTS, filaments)) + { + show_info(q, _(L("You have to select at least one filament for selected printers")), ""); + return false; + } + + if (any_sla_selected && technology & T_SLA && !exist_preset(AppConfig::SECTION_MATERIALS, sla_materials)) + { + show_info(q, _(L("You have to select at least one material for selected printers")), ""); + return false; + } + + return true; +} + void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater) { const auto enabled_vendors = appconfig_new.vendors(); @@ -1666,11 +1760,48 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese preset_bundle->export_selections(*app_config); } +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + // Public ConfigWizard::ConfigWizard(wxWindow *parent) - : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + name(), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name().ToStdString()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , p(new priv(this)) { this->SetFont(wxGetApp().normal_font()); @@ -1722,10 +1853,15 @@ ConfigWizard::ConfigWizard(wxWindow *parent) p->page_msla = new PagePrinters(this, _(L("Prusa MSLA Technology Printers")), "Prusa MSLA", *vendor_prusa, 0, T_SLA); p->add_page(p->page_msla); + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = p->check_fff_selected(); + + p->update_materials(T_ANY); + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, _(L("Filament Profiles Selection")), _(L("Filaments")), _(L("Type:")) )); p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, - _(L("SLA Material Profiles Selection")), _(L("SLA Materials")), _(L("Layer height:")) )); + _(L("SLA Material Profiles Selection")) + " ", _(L("SLA Materials")), _(L("Layer height:")) )); p->add_page(p->page_custom = new PageCustom(this)); p->add_page(p->page_update = new PageUpdate(this)); @@ -1739,9 +1875,6 @@ ConfigWizard::ConfigWizard(wxWindow *parent) p->create_3rdparty_pages(); // Needs to ne done _before_ creating PageVendors p->add_page(p->page_vendors = new PageVendors(this)); - p->any_sla_selected = p->page_msla->any_selected(); - p->any_fff_selected = p->page_fff->any_selected(); - p->load_pages(); p->index->go_to(size_t{0}); @@ -1760,9 +1893,23 @@ ConfigWizard::ConfigWizard(wxWindow *parent) }); p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); - p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_next(); }); - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->EndModal(wxID_OK); }); - p->btn_finish->Hide(); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if ( (active_page == p->page_filaments || active_page == p->page_sla_materials) + && !p->check_material_config(dynamic_cast<PageMaterials*>(active_page)->materials->technology)) + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (!p->check_material_config(T_ANY)) + return; + this->EndModal(wxID_OK); + }); p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { p->any_sla_selected = true; @@ -1775,7 +1922,8 @@ ConfigWizard::ConfigWizard(wxWindow *parent) p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { const bool is_last = p->index->active_is_last(); p->btn_next->Show(! is_last); - p->btn_finish->Show(is_last); + if (is_last) + p->btn_finish->SetFocus(); Layout(); }); diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 995957816..fc91909e7 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -58,15 +58,16 @@ enum Technology { struct Materials { Technology technology; - std::set<const Preset*> presets; + // use vector for the presets to purpose of save of presets sorting in the bundle + std::vector<const Preset*> presets; std::set<std::string> types; Materials(Technology technology) : technology(technology) {} void push(const Preset *preset); void clear(); - bool containts(const Preset *preset) { - return presets.find(preset) != presets.end(); + bool containts(const Preset *preset) const { + return std::find(presets.begin(), presets.end(), preset) != presets.end(); } const std::string& appconfig_section() const; @@ -81,6 +82,14 @@ struct Materials } } + bool exist_preset(const std::string& preset_name) const + { + for (const Preset* preset : presets) + if (preset->name == preset_name) + return true; + return false; + } + static const std::string UNKNOWN; static const std::string& get_filament_type(const Preset *preset); static const std::string& get_filament_vendor(const Preset *preset); @@ -240,10 +249,12 @@ template<class T, class D> struct DataList : public T return wxNOT_FOUND; } + + int size() { return this->GetCount(); } }; typedef DataList<wxListBox, std::string> StringList; -typedef DataList<wxCheckListBox, Preset> PresetList; +typedef DataList<wxCheckListBox, std::string> PresetList; struct PageMaterials: ConfigWizardPage { @@ -343,7 +354,10 @@ struct PageTemperatures: ConfigWizardPage virtual void apply_custom_config(DynamicPrintConfig &config); }; -typedef std::map<std::string /* = vendor ID */, PagePrinters*> Pages3rdparty; +// hypothetically, each vendor can has printers both of technologies (FFF and SLA) +typedef std::map<std::string /* = vendor ID */, + std::pair<PagePrinters* /* = FFF page */, + PagePrinters* /* = SLA page */>> Pages3rdparty; class ConfigWizardIndex: public wxPanel @@ -404,6 +418,8 @@ wxDEFINE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); // ConfigWizard private data +typedef std::map<std::string, std::set<std::string>> PresetAliases; + struct ConfigWizard::priv { ConfigWizard *q; @@ -415,6 +431,8 @@ struct ConfigWizard::priv // PrinterPickers state. Materials filaments; // Holds available filament presets and their types & vendors Materials sla_materials; // Ditto for SLA materials + PresetAliases aliases_fff; // Map of aliase to preset names + PresetAliases aliases_sla; // Map of aliase to preset names std::unique_ptr<DynamicPrintConfig> custom_config; // Backing for custom printer definition bool any_fff_selected; // Used to decide whether to display Filaments page bool any_sla_selected; // Used to decide whether to display SLA Materials page @@ -454,7 +472,6 @@ struct ConfigWizard::priv : q(q) , filaments(T_FFF) , sla_materials(T_SLA) - , any_sla_selected(false) {} void load_pages(); @@ -472,13 +489,17 @@ struct ConfigWizard::priv void on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt); void on_3rdparty_install(const VendorProfile *vendor, bool install); + bool check_material_config(Technology technology); void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater); + // #ys_FIXME_alise + void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add); + + bool check_fff_selected(); // Used to decide whether to display Filaments page + bool check_sla_selected(); // Used to decide whether to display SLA Materials page int em() const { return index->em(); } }; - - } } diff --git a/src/slic3r/GUI/ExtruderSequenceDialog.cpp b/src/slic3r/GUI/ExtruderSequenceDialog.cpp new file mode 100644 index 000000000..a850ac192 --- /dev/null +++ b/src/slic3r/GUI/ExtruderSequenceDialog.cpp @@ -0,0 +1,235 @@ +#include "ExtruderSequenceDialog.hpp" + +#include <wx/wx.h> +#include <wx/stattext.h> +#include <wx/dialog.h> +#include <wx/sizer.h> +#include <wx/bmpcbox.h> + +#include <vector> +#include <set> +#include <functional> + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" +#include "OptionsGroup.hpp" + + +namespace Slic3r { +namespace GUI { + +ExtruderSequenceDialog::ExtruderSequenceDialog(const DoubleSlider::ExtrudersSequence& sequence) + : DPIDialog(NULL, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(L("Set extruder sequence")), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_sequence(sequence) +{ + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + SetDoubleBuffered(true); + SetFont(wxGetApp().normal_font()); + + auto main_sizer = new wxBoxSizer(wxVERTICAL); + const int em = wxGetApp().em_unit(); + + m_bmp_del = ScalableBitmap(this, "remove_copies"); + m_bmp_add = ScalableBitmap(this, "add_copies"); + + auto option_sizer = new wxBoxSizer(wxVERTICAL); + + auto intervals_box = new wxStaticBox(this, wxID_ANY, _(L("Set extruder change for every"))+ " : "); + auto intervals_box_sizer = new wxStaticBoxSizer(intervals_box, wxVERTICAL); + + m_intervals_grid_sizer = new wxFlexGridSizer(3, 5, em); + + auto editor_sz = wxSize(4*em, wxDefaultCoord); + + auto ID_RADIO_BUTTON = wxWindow::NewControlId(1); + + wxRadioButton* rb_by_layers = new wxRadioButton(this, ID_RADIO_BUTTON, "", wxDefaultPosition, wxDefaultSize, wxRB_GROUP); + rb_by_layers->Bind(wxEVT_RADIOBUTTON, [this](wxCommandEvent& event) { m_sequence.is_mm_intervals = false; }); + rb_by_layers->SetValue(!m_sequence.is_mm_intervals); + + wxStaticText* st_by_layers = new wxStaticText(this, wxID_ANY, _(L("layers"))); + m_interval_by_layers = new wxTextCtrl(this, wxID_ANY, + wxString::Format("%d", m_sequence.interval_by_layers), + wxDefaultPosition, editor_sz); + m_interval_by_layers->Bind(wxEVT_TEXT, [this, rb_by_layers](wxEvent&) + { + wxString str = m_interval_by_layers->GetValue(); + if (str.IsEmpty()) { + m_interval_by_layers->SetValue(wxString::Format("%d", m_sequence.interval_by_layers)); + return; + } + + int val = wxAtoi(str); + if (val < 1) { + m_interval_by_layers->SetValue("1"); + val = 1; + } + + if (m_sequence.interval_by_layers == val) + return; + + m_sequence.interval_by_layers = val; + + m_sequence.is_mm_intervals = false; + rb_by_layers->SetValue(true); + }); + + m_intervals_grid_sizer->Add(rb_by_layers, 0, wxALIGN_CENTER_VERTICAL); + m_intervals_grid_sizer->Add(m_interval_by_layers,0, wxALIGN_CENTER_VERTICAL); + m_intervals_grid_sizer->Add(st_by_layers,0, wxALIGN_CENTER_VERTICAL); + + wxRadioButton* rb_by_mm = new wxRadioButton(this, ID_RADIO_BUTTON, ""); + rb_by_mm->Bind(wxEVT_RADIOBUTTON, [this](wxEvent&) { m_sequence.is_mm_intervals = true; }); + rb_by_mm->SetValue(m_sequence.is_mm_intervals); + + wxStaticText* st_by_mm = new wxStaticText(this, wxID_ANY, _(L("mm"))); + m_interval_by_mm = new wxTextCtrl(this, wxID_ANY, + double_to_string(sequence.interval_by_mm), + wxDefaultPosition, editor_sz, wxTE_PROCESS_ENTER); + + auto change_value = [this]() + { + wxString str = m_interval_by_mm->GetValue(); + if (str.IsEmpty()) { + m_interval_by_mm->SetValue(wxString::Format("%d", m_sequence.interval_by_mm)); + return; + } + + str.Replace(",", ".", false); + double val; + if (str == "." || !str.ToCDouble(&val) || val <= 0.0) + val = 3.0; // default value + + if (fabs(m_sequence.interval_by_layers - val) < 0.001) + return; + + m_sequence.interval_by_mm = val; + }; + + m_interval_by_mm->Bind(wxEVT_TEXT, [this, rb_by_mm](wxEvent&) + { + m_sequence.is_mm_intervals = true; + rb_by_mm->SetValue(true); + }); + + m_interval_by_mm->Bind(wxEVT_KILL_FOCUS, [this, change_value](wxFocusEvent& event) + { + change_value(); + event.Skip(); + }); + + m_interval_by_mm->Bind(wxEVT_TEXT_ENTER, [this, change_value](wxEvent&) + { + change_value(); + }); + + m_intervals_grid_sizer->Add(rb_by_mm, 0, wxALIGN_CENTER_VERTICAL); + m_intervals_grid_sizer->Add(m_interval_by_mm,0, wxALIGN_CENTER_VERTICAL); + m_intervals_grid_sizer->Add(st_by_mm,0, wxALIGN_CENTER_VERTICAL); + + intervals_box_sizer->Add(m_intervals_grid_sizer, 0, wxLEFT, em); + option_sizer->Add(intervals_box_sizer, 0, wxEXPAND); + + + auto extruders_box = new wxStaticBox(this, wxID_ANY, _(L("Set extruder(tool) sequence"))+ " : "); + auto extruders_box_sizer = new wxStaticBoxSizer(extruders_box, wxVERTICAL); + + m_extruders_grid_sizer = new wxFlexGridSizer(3, 5, em); + + apply_extruder_sequence(); + + extruders_box_sizer->Add(m_extruders_grid_sizer, 0, wxALL, em); + option_sizer->Add(extruders_box_sizer, 0, wxEXPAND | wxTOP, em); + + main_sizer->Add(option_sizer, 0, wxEXPAND | wxALL, em); + + wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL); + main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, em); + + SetSizer(main_sizer); + main_sizer->SetSizeHints(this); + + /* For this moment min sizes for dialog and its sizer are calculated. + * If we left them, it can cause a problem with layouts during deleting of extruders + */ + if (m_sequence.extruders.size()>1) + { + wxSize sz = wxSize(-1, 10 * em); + SetMinSize(sz); + GetSizer()->SetMinSize(sz); + } +} + +void ExtruderSequenceDialog::apply_extruder_sequence() +{ + m_extruders_grid_sizer->Clear(true); + + for (size_t extruder=0; extruder < m_sequence.extruders.size(); ++extruder) + { + wxBitmapComboBox* extruder_selector = nullptr; + apply_extruder_selector(&extruder_selector, this, "", wxDefaultPosition, wxSize(15*wxGetApp().em_unit(), -1)); + extruder_selector->SetSelection(m_sequence.extruders[extruder]); + + extruder_selector->Bind(wxEVT_COMBOBOX, [this, extruder_selector, extruder](wxCommandEvent& evt) + { + m_sequence.extruders[extruder] = extruder_selector->GetSelection(); + evt.StopPropagation(); + }); + + auto del_btn = new ScalableButton(this, wxID_ANY, m_bmp_del); + del_btn->SetToolTip(_(L("Remove extruder from sequence"))); + if (m_sequence.extruders.size()==1) + del_btn->Disable(); + + del_btn->Bind(wxEVT_BUTTON, [this, extruder](wxEvent&) { + m_sequence.delete_extruder(extruder); + apply_extruder_sequence(); + }); + + auto add_btn = new ScalableButton(this, wxID_ANY, m_bmp_add); + add_btn->SetToolTip(_(L("Add extruder to sequence"))); + + add_btn->Bind(wxEVT_BUTTON, [this, extruder](wxEvent&) { + m_sequence.add_extruder(extruder); + apply_extruder_sequence(); + }); + + m_extruders_grid_sizer->Add(extruder_selector, 0, wxALIGN_CENTER_VERTICAL); + m_extruders_grid_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL); + m_extruders_grid_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL); + } + m_extruders_grid_sizer->ShowItems(true); // show items hidden in apply_extruder_selector() + + Fit(); + Refresh(); +} + +void ExtruderSequenceDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + SetFont(wxGetApp().normal_font()); + + m_bmp_add.msw_rescale(); + m_bmp_del.msw_rescale(); + + const int em = em_unit(); + + m_intervals_grid_sizer->SetHGap(em); + m_intervals_grid_sizer->SetVGap(em); + m_extruders_grid_sizer->SetHGap(em); + m_extruders_grid_sizer->SetVGap(em); + + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); + + // wxSize size = get_size(); + // SetMinSize(size); + + Fit(); + Refresh(); +} + +} +} + + diff --git a/src/slic3r/GUI/ExtruderSequenceDialog.hpp b/src/slic3r/GUI/ExtruderSequenceDialog.hpp new file mode 100644 index 000000000..3efd9e3a2 --- /dev/null +++ b/src/slic3r/GUI/ExtruderSequenceDialog.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_GUI_ExtruderSequenceDialog_hpp_ +#define slic3r_GUI_ExtruderSequenceDialog_hpp_ + +#include "GUI_Utils.hpp" +#include "wxExtensions.hpp" + +class wxTextCtrl; +class wxFlexGridSizer; + +namespace Slic3r { +namespace GUI { + +// ---------------------------------------------------------------------------- +// ExtruderSequenceDialog: a node inside ObjectDataViewModel +// ---------------------------------------------------------------------------- + +class ExtruderSequenceDialog: public DPIDialog +{ + ScalableBitmap m_bmp_del; + ScalableBitmap m_bmp_add; + DoubleSlider::ExtrudersSequence m_sequence; + + wxTextCtrl* m_interval_by_layers {nullptr}; + wxTextCtrl* m_interval_by_mm {nullptr}; + + wxFlexGridSizer* m_intervals_grid_sizer {nullptr}; + wxFlexGridSizer* m_extruders_grid_sizer {nullptr}; +public: + ExtruderSequenceDialog(const DoubleSlider::ExtrudersSequence& sequence); + + ~ExtruderSequenceDialog() {} + + DoubleSlider::ExtrudersSequence GetValue() { return m_sequence; } + +protected: + void apply_extruder_sequence(); + void on_dpi_changed(const wxRect& suggested_rect) override; + +}; + +} +} + + +#endif // slic3r_GUI_ExtruderSequenceDialog_hpp_ diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c5ade144e..c8f660e14 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -22,9 +22,11 @@ #include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/GUI/Tab.hpp" #include "slic3r/GUI/GUI_Preview.hpp" + #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" #include "GUI_ObjectManipulation.hpp" +#include "Mouse3DController.hpp" #include "I18N.hpp" #if ENABLE_RETINA_GL @@ -130,6 +132,9 @@ GLCanvas3D::LayersEditing::LayersEditing() , m_object_max_z(0.f) , m_slicing_parameters(nullptr) , m_layer_height_profile_modified(false) +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + , m_adaptive_cusp(0.0f) +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE , state(Unknown) , band_width(2.0f) , strength(0.005f) @@ -150,7 +155,9 @@ GLCanvas3D::LayersEditing::~LayersEditing() } const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f; +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE const float GLCanvas3D::LayersEditing::THICKNESS_RESET_BUTTON_HEIGHT = 22.0f; +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename) { @@ -217,13 +224,110 @@ void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) const if (!m_enabled) return; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f); + + const Size& cnv_size = canvas.get_canvas_size(); + float canvas_w = (float)cnv_size.get_width(); + float canvas_h = (float)cnv_size.get_height(); + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.set_next_window_pos(canvas_w - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, canvas_h, ImGuiCond_Always, 1.0f, 1.0f); + + imgui.begin(_(L("Variable layer height")), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + + ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); + imgui.text(_(L("Left mouse button:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(_(L("Add detail"))); + + ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); + imgui.text(_(L("Right mouse button:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(_(L("Remove detail"))); + + ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); + imgui.text(_(L("Shift + Left mouse button:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(_(L("Reset to base"))); + + ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); + imgui.text(_(L("Shift + Right mouse button:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(_(L("Smoothing"))); + + ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); + imgui.text(_(L("Mouse wheel:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(_(L("Increase/decrease edit area"))); + + ImGui::Separator(); + if (imgui.button(_(L("Adaptive")))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event<float>(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_cusp)); + + ImGui::SameLine(); + float text_align = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + imgui.text(_(L("Cusp (mm)"))); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(_(L("I am a tooltip"))); + ImGui::EndTooltip(); + } + + ImGui::SameLine(); + float widget_align = ImGui::GetCursorPosX(); + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + m_adaptive_cusp = clamp(0.0f, 0.5f * (float)m_slicing_parameters->layer_height, m_adaptive_cusp); + ImGui::SliderFloat("", &m_adaptive_cusp, 0.0f, 0.5f * (float)m_slicing_parameters->layer_height, "%.3f"); + + ImGui::Separator(); + if (imgui.button(_(L("Smooth")))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params)); + + ImGui::SameLine(); + ImGui::SetCursorPosX(text_align); + ImGui::AlignTextToFramePadding(); + imgui.text(_(L("Radius"))); + ImGui::SameLine(); + ImGui::SetCursorPosX(widget_align); + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + int radius = (int)m_smooth_params.radius; + if (ImGui::SliderInt("##1", &radius, 1, 10)) + m_smooth_params.radius = (unsigned int)radius; + + ImGui::SetCursorPosX(text_align); + ImGui::AlignTextToFramePadding(); + imgui.text(_(L("Keep min"))); + ImGui::SameLine(); + if (ImGui::GetCursorPosX() < widget_align) // because of line lenght after localization + ImGui::SetCursorPosX(widget_align); + + ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); + imgui.checkbox("##2", m_smooth_params.keep_min); + + ImGui::Separator(); + if (imgui.button(_(L("Reset")))) + wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE)); + + imgui.end(); + + const Rect& bar_rect = get_bar_rect_viewport(canvas); +#else const Rect& bar_rect = get_bar_rect_viewport(canvas); const Rect& reset_rect = get_reset_rect_viewport(canvas); _render_tooltip_texture(canvas, bar_rect, reset_rect); _render_reset_texture(reset_rect); - _render_active_object_annotations(canvas, bar_rect); - _render_profile(bar_rect); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + render_active_object_annotations(canvas, bar_rect); + render_profile(bar_rect); } float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas) @@ -248,11 +352,13 @@ bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, floa return (rect.get_left() <= x) && (x <= rect.get_right()) && (rect.get_top() <= y) && (y <= rect.get_bottom()); } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE bool GLCanvas3D::LayersEditing::reset_rect_contains(const GLCanvas3D& canvas, float x, float y) { const Rect& rect = get_reset_rect_screen(canvas); return (rect.get_left() <= x) && (x <= rect.get_right()) && (rect.get_top() <= y) && (y <= rect.get_bottom()); } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas) { @@ -260,9 +366,14 @@ Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas) float w = (float)cnv_size.get_width(); float h = (float)cnv_size.get_height(); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + return Rect(w - thickness_bar_width(canvas), 0.0f, w, h); +#else return Rect(w - thickness_bar_width(canvas), 0.0f, w, h - reset_button_height(canvas)); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE Rect GLCanvas3D::LayersEditing::get_reset_rect_screen(const GLCanvas3D& canvas) { const Size& cnv_size = canvas.get_canvas_size(); @@ -271,6 +382,7 @@ Rect GLCanvas3D::LayersEditing::get_reset_rect_screen(const GLCanvas3D& canvas) return Rect(w - thickness_bar_width(canvas), h - reset_button_height(canvas), w, h); } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas) { @@ -281,9 +393,14 @@ Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas) float zoom = (float)canvas.get_camera().get_zoom(); float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + return Rect((half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom); +#else return Rect((half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, (-half_h + reset_button_height(canvas)) * inv_zoom); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE Rect GLCanvas3D::LayersEditing::get_reset_rect_viewport(const GLCanvas3D& canvas) { const Size& cnv_size = canvas.get_canvas_size(); @@ -295,13 +412,43 @@ Rect GLCanvas3D::LayersEditing::get_reset_rect_viewport(const GLCanvas3D& canvas return Rect((half_w - thickness_bar_width(canvas)) * inv_zoom, (-half_h + reset_button_height(canvas)) * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom); } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE - -bool GLCanvas3D::LayersEditing::_is_initialized() const +bool GLCanvas3D::LayersEditing::is_initialized() const { return m_shader.is_initialized(); } +std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const +{ + std::string ret; + if (m_enabled && (m_layer_height_profile.size() >= 4)) + { + float z = get_cursor_z_relative(canvas); + if (z != -1000.0f) + { + z *= m_object_max_z; + + float h = 0.0f; + for (size_t i = m_layer_height_profile.size() - 2; i >= 2; i -= 2) + { + float zi = m_layer_height_profile[i]; + float zi_1 = m_layer_height_profile[i - 2]; + if ((zi_1 <= z) && (z <= zi)) + { + float dz = zi - zi_1; + h = (dz != 0.0f) ? lerp(m_layer_height_profile[i - 1], m_layer_height_profile[i + 1], (z - zi_1) / dz) : m_layer_height_profile[i + 1]; + break; + } + } + if (h > 0.0f) + ret = std::to_string(h); + } + } + return ret; +} + +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE void GLCanvas3D::LayersEditing::_render_tooltip_texture(const GLCanvas3D& canvas, const Rect& bar_rect, const Rect& reset_rect) const { // TODO: do this with ImGui @@ -347,8 +494,9 @@ void GLCanvas3D::LayersEditing::_render_reset_texture(const Rect& reset_rect) co GLTexture::render_texture(m_reset_texture.get_id(), reset_rect.get_left(), reset_rect.get_right(), reset_rect.get_bottom(), reset_rect.get_top()); } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE -void GLCanvas3D::LayersEditing::_render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const +void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const { m_shader.start_using(); @@ -379,7 +527,7 @@ void GLCanvas3D::LayersEditing::_render_active_object_annotations(const GLCanvas m_shader.stop_using(); } -void GLCanvas3D::LayersEditing::_render_profile(const Rect& bar_rect) const +void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect) const { //FIXME show some kind of legend. @@ -496,6 +644,26 @@ void GLCanvas3D::LayersEditing::reset_layer_height_profile(GLCanvas3D& canvas) canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +void GLCanvas3D::LayersEditing::adaptive_layer_height_profile(GLCanvas3D& canvas, float cusp) +{ + this->update_slicing_parameters(); + m_layer_height_profile = layer_height_profile_adaptive(*m_slicing_parameters, *m_model_object, cusp); + const_cast<ModelObject*>(m_model_object)->layer_height_profile = m_layer_height_profile; + m_layers_texture.valid = false; + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLCanvas3D::LayersEditing::smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_params) +{ + this->update_slicing_parameters(); + m_layer_height_profile = smooth_height_profile(m_layer_height_profile, *m_slicing_parameters, smoothing_params); + const_cast<ModelObject*>(m_model_object)->layer_height_profile = m_layer_height_profile; + m_layers_texture.valid = false; + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void GLCanvas3D::LayersEditing::generate_layer_height_texture() { this->update_slicing_parameters(); @@ -530,7 +698,7 @@ void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas) { if (last_object_id >= 0) { if (m_layer_height_profile_modified) { - wxGetApp().plater()->take_snapshot(_(L("Layers heights"))); + wxGetApp().plater()->take_snapshot(_(L("Variable layer height - Manual edit"))); const_cast<ModelObject*>(m_model_object)->layer_height_profile = m_layer_height_profile; canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } @@ -544,6 +712,11 @@ void GLCanvas3D::LayersEditing::update_slicing_parameters() m_slicing_parameters = new SlicingParameters(); *m_slicing_parameters = PrintObject::slicing_parameters(*m_config, *m_model_object, m_object_max_z); } + +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + if (m_adaptive_cusp == 0.0f) + m_adaptive_cusp = 0.25f * m_slicing_parameters->layer_height; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE } float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas) @@ -557,6 +730,7 @@ float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas) * THICKNESS_BAR_WIDTH; } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE float GLCanvas3D::LayersEditing::reset_button_height(const GLCanvas3D &canvas) { return @@ -567,6 +741,7 @@ float GLCanvas3D::LayersEditing::reset_button_height(const GLCanvas3D &canvas) #endif * THICKNESS_RESET_BUTTON_HEIGHT; } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX); @@ -836,47 +1011,125 @@ GLCanvas3D::LegendTexture::LegendTexture() { } -void GLCanvas3D::LegendTexture::fill_color_print_legend_values(const GCodePreviewData& preview_data, const GLCanvas3D& canvas, - std::vector<std::pair<double, double>>& cp_legend_values) +void GLCanvas3D::LegendTexture::fill_color_print_legend_items( const GLCanvas3D& canvas, + const std::vector<float>& colors_in, + std::vector<float>& colors, + std::vector<std::string>& cp_legend_items) { - if (preview_data.extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint && - wxGetApp().extruders_edited_cnt() == 1) // show color change legend only for single-material presets + std::vector<Model::CustomGCode> custom_gcode_per_height = wxGetApp().plater()->model().custom_gcode_per_height; + + const int extruders_cnt = wxGetApp().extruders_edited_cnt(); + if (extruders_cnt == 1) { - auto& config = wxGetApp().preset_bundle->project_config; - const std::vector<double>& color_print_values = config.option<ConfigOptionFloats>("colorprint_heights")->values; + if (custom_gcode_per_height.empty()) { + cp_legend_items.push_back(I18N::translate_utf8(L("Default print color"))); + colors = colors_in; + return; + } + std::vector<std::pair<double, double>> cp_values; - if (!color_print_values.empty()) { - std::vector<double> print_zs = canvas.get_current_print_zs(true); - for (auto cp_value : color_print_values) - { - auto lower_b = std::lower_bound(print_zs.begin(), print_zs.end(), cp_value - DoubleSlider::epsilon()); + std::vector<double> print_zs = canvas.get_current_print_zs(true); + for (auto custom_code : custom_gcode_per_height) + { + if (custom_code.gcode != ColorChangeCode) + continue; + auto lower_b = std::lower_bound(print_zs.begin(), print_zs.end(), custom_code.height - DoubleSlider::epsilon()); - if (lower_b == print_zs.end()) - continue; + if (lower_b == print_zs.end()) + continue; + + double current_z = *lower_b; + double previous_z = lower_b == print_zs.begin() ? 0.0 : *(--lower_b); + + // to avoid duplicate values, check adding values + if (cp_values.empty() || + !(cp_values.back().first == previous_z && cp_values.back().second == current_z)) + cp_values.push_back(std::pair<double, double>(previous_z, current_z)); + } + + const auto items_cnt = (int)cp_values.size(); + if (items_cnt == 0) // There is no one color change, but there is/are some pause print or custom Gcode + { + cp_legend_items.push_back(I18N::translate_utf8(L("Default print color"))); + cp_legend_items.push_back(I18N::translate_utf8(L("Pause print or custom G-code"))); + colors = colors_in; + return; + } - double current_z = *lower_b; - double previous_z = lower_b == print_zs.begin() ? 0.0 : *(--lower_b); + const int color_cnt = (int)colors_in.size() / 4; + colors.resize(colors_in.size(), 0.0); + + ::memcpy((void*)(colors.data()), (const void*)(colors_in.data() + (color_cnt - 1) * 4), 4 * sizeof(float)); + cp_legend_items.push_back(I18N::translate_utf8(L("Pause print or custom G-code"))); + size_t color_pos = 4; + + for (int i = items_cnt; i >= 0; --i, color_pos+=4) + { + // update colors for color print item + ::memcpy((void*)(colors.data() + color_pos), (const void*)(colors_in.data() + i * 4), 4 * sizeof(float)); - // to avoid duplicate values, check adding values - if (cp_legend_values.empty() || - !(cp_legend_values.back().first == previous_z && cp_legend_values.back().second == current_z) ) - cp_legend_values.push_back(std::pair<double, double>(previous_z, current_z)); + // create label for color print item + std::string id_str = std::to_string(i + 1) + ": "; + + if (i == 0) { + cp_legend_items.push_back(id_str + (boost::format(I18N::translate_utf8(L("up to %.2f mm"))) % cp_values[0].first).str()); + break; } + if (i == items_cnt) { + cp_legend_items.push_back(id_str + (boost::format(I18N::translate_utf8(L("above %.2f mm"))) % cp_values[i - 1].second).str()); + continue; + } + + cp_legend_items.push_back(id_str + (boost::format(I18N::translate_utf8(L("%.2f - %.2f mm"))) % cp_values[i - 1].second % cp_values[i].first).str()); } } + else + { + // colors = colors_in; + const int color_cnt = (int)colors_in.size() / 4; + colors.resize(colors_in.size(), 0.0); + + ::memcpy((void*)(colors.data()), (const void*)(colors_in.data()), 4 * extruders_cnt * sizeof(float)); + size_t color_pos = 4 * extruders_cnt; + size_t color_in_pos = 4 * (color_cnt - 1); + + for (unsigned int i = 0; i < (unsigned int)extruders_cnt; ++i) + cp_legend_items.push_back((boost::format(I18N::translate_utf8(L("Extruder %d"))) % (i + 1)).str()); + + ::memcpy((void*)(colors.data() + color_pos), (const void*)(colors_in.data() + color_in_pos), 4 * sizeof(float)); + color_pos += 4; + color_in_pos -= 4; + cp_legend_items.push_back(I18N::translate_utf8(L("Pause print or custom G-code"))); + + int cnt = custom_gcode_per_height.size(); + for (int i = cnt-1; i >= 0; --i) + if (custom_gcode_per_height[i].gcode == ColorChangeCode) { + ::memcpy((void*)(colors.data() + color_pos), (const void*)(colors_in.data() + color_in_pos), 4 * sizeof(float)); + color_pos += 4; + color_in_pos -= 4; + cp_legend_items.push_back((boost::format(I18N::translate_utf8(L("Color change for Extruder %d at %.2f mm"))) % custom_gcode_per_height[i].extruder % custom_gcode_per_height[i].height).str()); + } + } } -bool GLCanvas3D::LegendTexture::generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors, const GLCanvas3D& canvas, bool compress) +bool GLCanvas3D::LegendTexture::generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors_in, const GLCanvas3D& canvas, bool compress) { reset(); // collects items to render auto title = _(preview_data.get_legend_title()); - std::vector<std::pair<double, double>> cp_legend_values; - fill_color_print_legend_values(preview_data, canvas, cp_legend_values); + std::vector<std::string> cp_legend_items; + std::vector<float> cp_colors; - const GCodePreviewData::LegendItemsList& items = preview_data.get_legend_items(tool_colors, cp_legend_values); + if (preview_data.extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint) + { + cp_legend_items.reserve(cp_colors.size()); + fill_color_print_legend_items(canvas, tool_colors_in, cp_colors, cp_legend_items); + } + + const std::vector<float>& tool_colors = preview_data.extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint ? cp_colors : tool_colors_in; + const GCodePreviewData::LegendItemsList& items = preview_data.get_legend_items(tool_colors, cp_legend_items); unsigned int items_count = (unsigned int)items.size(); if (items_count == 0) @@ -1094,7 +1347,9 @@ void GLCanvas3D::LegendTexture::render(const GLCanvas3D& canvas) const } } +#if !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX wxDEFINE_EVENT(EVT_GLCANVAS_INIT, SimpleEvent); +#endif // !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); @@ -1118,6 +1373,11 @@ wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, wxKeyEvent); wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +wxDEFINE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event<float>); +wxDEFINE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE #if ENABLE_THUMBNAIL_GENERATOR const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; @@ -1130,7 +1390,6 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D& bed, Camera& camera, GLToolbar , m_retina_helper(nullptr) #endif , m_in_render(false) - , m_render_enabled(true) , m_bed(bed) , m_camera(camera) , m_view_toolbar(view_toolbar) @@ -1260,7 +1519,9 @@ bool GLCanvas3D::init() if (m_selection.is_enabled() && !m_selection.init()) return false; +#if !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX post_event(SimpleEvent(EVT_GLCANVAS_INIT)); +#endif // !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX m_initialized = true; @@ -1376,7 +1637,7 @@ void GLCanvas3D::set_model(Model* model) void GLCanvas3D::bed_shape_changed() { - m_camera.set_scene_box(scene_bounding_box()); + refresh_camera_scene_box(); m_camera.requires_zoom_to_bed = true; m_dirty = true; if (m_bed.is_prusa()) @@ -1402,7 +1663,7 @@ BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const BoundingBoxf3 GLCanvas3D::scene_bounding_box() const { BoundingBoxf3 bb = volumes_bounding_box(); - bb.merge(m_bed.get_bounding_box(false)); + bb.merge(m_bed.get_bounding_box(true)); if (m_config != nullptr) { @@ -1424,6 +1685,32 @@ bool GLCanvas3D::is_layers_editing_allowed() const return m_layers_editing.is_allowed(); } +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +void GLCanvas3D::reset_layer_height_profile() +{ + wxGetApp().plater()->take_snapshot(_(L("Variable layer height - Reset"))); + m_layers_editing.reset_layer_height_profile(*this); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} + +void GLCanvas3D::adaptive_layer_height_profile(float cusp) +{ + wxGetApp().plater()->take_snapshot(_(L("Variable layer height - Adaptive"))); + m_layers_editing.adaptive_layer_height_profile(*this, cusp); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} + +void GLCanvas3D::smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params) +{ + wxGetApp().plater()->take_snapshot(_(L("Variable layer height - Smooth all"))); + m_layers_editing.smooth_layer_height_profile(*this, smoothing_params); + m_layers_editing.state = LayersEditing::Completed; + m_dirty = true; +} +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + bool GLCanvas3D::is_reload_delayed() const { return m_reload_delayed; @@ -1521,7 +1808,7 @@ void GLCanvas3D::update_volumes_colors_by_extruder() void GLCanvas3D::render() { - if (!m_render_enabled || m_in_render) + if (m_in_render) { // if called recursively, return m_dirty = true; @@ -1550,10 +1837,11 @@ void GLCanvas3D::render() return; } + const Size& cnv_size = get_canvas_size(); + if (m_camera.requires_zoom_to_bed) { zoom_to_bed(); - const Size& cnv_size = get_canvas_size(); _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); m_camera.requires_zoom_to_bed = false; } @@ -1594,7 +1882,7 @@ void GLCanvas3D::render() _render_objects(); _render_sla_slices(); _render_selection(); - _render_bed(theta); + _render_bed(theta, true); #if ENABLE_RENDER_SELECTION_CENTER _render_selection_center(); @@ -1623,7 +1911,6 @@ void GLCanvas3D::render() #if ENABLE_RENDER_STATISTICS ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.set_next_window_bg_alpha(0.5f); imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); imgui.text("Last frame: "); ImGui::SameLine(); @@ -1644,6 +1931,8 @@ void GLCanvas3D::render() m_camera.debug_render(); #endif // ENABLE_CAMERA_STATISTICS + wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height()); + wxGetApp().imgui()->render(); m_canvas->SwapBuffers(); @@ -1655,13 +1944,13 @@ void GLCanvas3D::render() } #if ENABLE_THUMBNAIL_GENERATOR -void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background) +void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const { switch (GLCanvas3DManager::get_framebuffers_type()) { - case GLCanvas3DManager::FB_Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, printable_only, parts_only, transparent_background); break; } - case GLCanvas3DManager::FB_Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, printable_only, parts_only, transparent_background); break; } - default: { _render_thumbnail_legacy(thumbnail_data, w, h, printable_only, parts_only, transparent_background); break; } + case GLCanvas3DManager::FB_Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, printable_only, parts_only, show_bed, transparent_background); break; } + case GLCanvas3DManager::FB_Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, printable_only, parts_only, show_bed, transparent_background); break; } + default: { _render_thumbnail_legacy(thumbnail_data, w, h, printable_only, parts_only, show_bed, transparent_background); break; } } } #endif // ENABLE_THUMBNAIL_GENERATOR @@ -1790,9 +2079,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re struct GLVolumeState { GLVolumeState() : - volume_idx(-1) {} + volume_idx(size_t(-1)) {} GLVolumeState(const GLVolume* volume, unsigned int volume_idx) : composite_id(volume->composite_id), volume_idx(volume_idx) {} + GLVolumeState(const GLVolume::CompositeID &composite_id) : + composite_id(composite_id), volume_idx(size_t(-1)) {} GLVolume::CompositeID composite_id; // Volume index in the old GLVolume vector. @@ -1918,22 +2209,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } } sort_remove_duplicates(instance_ids_selected); + auto deleted_volumes_lower = [](const GLVolumeState &v1, const GLVolumeState &v2) { return v1.composite_id < v2.composite_id; }; + std::sort(deleted_volumes.begin(), deleted_volumes.end(), deleted_volumes_lower); if (m_reload_delayed) return; bool update_object_list = false; - - auto find_old_volume_id = [&deleted_volumes](const GLVolume::CompositeID& id) -> unsigned int { - for (unsigned int i = 0; i < (unsigned int)deleted_volumes.size(); ++i) - { - const GLVolumeState& v = deleted_volumes[i]; - if (v.composite_id == id) - return v.volume_idx; - } - return (unsigned int)-1; - }; - if (m_volumes.volumes != glvolumes_new) update_object_list = true; m_volumes.volumes = std::move(glvolumes_new); @@ -1948,9 +2230,10 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); if (it->new_geometry()) { // New volume. - unsigned int old_id = find_old_volume_id(it->composite_id); - if (old_id != (unsigned int)-1) - map_glvolume_old_to_new[old_id] = m_volumes.volumes.size(); + auto it_old_volume = std::lower_bound(deleted_volumes.begin(), deleted_volumes.end(), GLVolumeState(it->composite_id), deleted_volumes_lower); + if (it_old_volume != deleted_volumes.end() && it_old_volume->composite_id == it->composite_id) + // If a volume changed its ObjectID, but it reuses a GLVolume's CompositeID, maintain its selection. + map_glvolume_old_to_new[it_old_volume->volume_idx] = m_volumes.volumes.size(); m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_color_by, m_initialized); m_volumes.volumes.back()->geometry_id = key.geometry_id; update_object_list = true; @@ -2102,7 +2385,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); } - m_camera.set_scene_box(scene_bounding_box()); + refresh_camera_scene_box(); if (m_selection.is_empty()) { @@ -2138,12 +2421,12 @@ static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& static void load_gcode_retractions(const GCodePreviewData::Retraction& retractions, GLCanvas3D::GCodePreviewVolumeIndex::EType extrusion_type, GLVolumeCollection &volumes, GLCanvas3D::GCodePreviewVolumeIndex &volume_index, bool gl_initialized) { - volume_index.first_volumes.emplace_back(extrusion_type, 0, (unsigned int)volumes.volumes.size()); - // nothing to render, return if (retractions.positions.empty()) return; + volume_index.first_volumes.emplace_back(extrusion_type, 0, (unsigned int)volumes.volumes.size()); + GLVolume *volume = volumes.new_nontoolpath_volume(retractions.color.rgba, VERTEX_BUFFER_RESERVE_SIZE); GCodePreviewData::Retraction::PositionsList copy(retractions.positions); @@ -2209,6 +2492,9 @@ void GLCanvas3D::load_gcode_preview(const GCodePreviewData& preview_data, const ++ idx_volume_index_src; idx_volume_of_this_type_last = (idx_volume_index_src + 1 == m_gcode_preview_volume_index.first_volumes.size()) ? m_volumes.volumes.size() : m_gcode_preview_volume_index.first_volumes[idx_volume_index_src + 1].id; idx_volume_of_this_type_first_new = idx_volume_dst; + if (idx_volume_src == idx_volume_of_this_type_last) + // Empty sequence of volumes for the current index item. + continue; } if (! m_volumes.volumes[idx_volume_src]->print_zs.empty()) m_volumes.volumes[idx_volume_dst ++] = m_volumes.volumes[idx_volume_src]; @@ -2247,7 +2533,7 @@ void GLCanvas3D::load_sla_preview() } } -void GLCanvas3D::load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<double>& color_print_values) +void GLCanvas3D::load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<Model::CustomGCode>& color_print_values) { const Print *print = this->fff_print(); if (print == nullptr) @@ -2342,14 +2628,21 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) m_dirty |= m_main_toolbar.update_items_state(); m_dirty |= m_undoredo_toolbar.update_items_state(); m_dirty |= m_view_toolbar.update_items_state(); + bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(m_camera); + m_dirty |= mouse3d_controller_applied; if (!m_dirty) return; _refresh_if_shown_on_screen(); - if (m_keep_dirty) + if (m_keep_dirty || mouse3d_controller_applied) + { m_dirty = true; + evt.RequestMore(); + } + else + m_dirty = false; } void GLCanvas3D::on_char(wxKeyEvent& evt) @@ -2394,6 +2687,20 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) #endif /* __APPLE__ */ post_event(SimpleEvent(EVT_GLTOOLBAR_COPY)); break; + +#ifdef __APPLE__ + case 'm': + case 'M': +#else /* __APPLE__ */ + case WXK_CONTROL_M: +#endif /* __APPLE__ */ + { + Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller(); + controller.show_settings_dialog(!controller.is_settings_dialog_shown()); + m_dirty = true; + break; + } + #ifdef __APPLE__ case 'v': case 'V': @@ -2461,11 +2768,11 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) case 'B': case 'b': { zoom_to_bed(); break; } case 'I': - case 'i': { set_camera_zoom(1.0); break; } + case 'i': { _update_camera_zoom(1.0); break; } case 'K': case 'k': { m_camera.select_next_type(); m_dirty = true; break; } case 'O': - case 'o': { set_camera_zoom(-1.0); break; } + case 'o': { _update_camera_zoom(-1.0); break; } #if ENABLE_RENDER_PICKING_PASS case 'T': case 't': { @@ -2568,6 +2875,11 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) { + // try to filter out events coming from mouse 3d + Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller(); + if (controller.process_mouse_wheel()) + return; + if (!m_initialized) return; @@ -2612,7 +2924,7 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) return; // Calculate the zoom delta and apply it to the current zoom factor - set_camera_zoom((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); + _update_camera_zoom((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); } void GLCanvas3D::on_timer(wxTimerEvent& evt) @@ -2804,6 +3116,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_layers_editing.state = LayersEditing::Editing; _perform_layer_editing_action(&evt); } +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE else if ((layer_editing_object_idx != -1) && m_layers_editing.reset_rect_contains(*this, pos(0), pos(1))) { if (evt.LeftDown()) @@ -2816,6 +3129,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_dirty = true; } } +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE else if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) { if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports) @@ -2947,6 +3261,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if ((m_layers_editing.state != LayersEditing::Unknown) && (layer_editing_object_idx != -1)) { + set_tooltip(""); if (m_layers_editing.state == LayersEditing::Editing) _perform_layer_editing_action(&evt); } @@ -3057,6 +3372,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) std::string tooltip = ""; if (tooltip.empty()) + tooltip = m_layers_editing.get_tooltip(*this); + + if (tooltip.empty()) tooltip = m_gizmos.get_tooltip(); if (tooltip.empty()) @@ -3395,13 +3713,6 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) m_dirty = true; } -void GLCanvas3D::set_camera_zoom(double zoom) -{ - const Size& cnv_size = get_canvas_size(); - m_camera.set_zoom(zoom, _max_bounding_box(false, true), cnv_size.get_width(), cnv_size.get_height()); - m_dirty = true; -} - void GLCanvas3D::update_gizmos_on_off_state() { set_as_dirty(); @@ -3533,14 +3844,13 @@ static bool string_getter(const bool is_undo, int idx, const char** out_text) return wxGetApp().plater()->undo_redo_string_getter(is_undo, idx, out_text); } -void GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) +void GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x) const { ImGuiWrapper* imgui = wxGetApp().imgui(); const float x = pos_x * (float)get_camera().get_zoom() + 0.5f * (float)get_canvas_size().get_width(); imgui->set_next_window_pos(x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f); - imgui->set_next_window_bg_alpha(0.5f); - std::string title = is_undo ? L("Undo History") : L("Redo History"); + std::string title = is_undo ? L("Undo History") : L("Redo History"); imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); int hovered = m_imgui_undo_redo_hovered_pos; @@ -3587,8 +3897,7 @@ static void debug_output_thumbnail(const ThumbnailData& thumbnail_data) } #endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT - -static void render_volumes_in_thumbnail(Shader& shader, const GLVolumePtrs& volumes, ThumbnailData& thumbnail_data, bool printable_only, bool parts_only, bool transparent_background) +void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const { auto is_visible = [](const GLVolume& v) -> bool { @@ -3602,7 +3911,7 @@ static void render_volumes_in_thumbnail(Shader& shader, const GLVolumePtrs& volu GLVolumePtrs visible_volumes; - for (GLVolume* vol : volumes) + for (GLVolume* vol : m_volumes.volumes) { if (!vol->is_modifier && !vol->is_wipe_tower && (!parts_only || (vol->composite_id.volume_id >= 0))) { @@ -3625,17 +3934,31 @@ static void render_volumes_in_thumbnail(Shader& shader, const GLVolumePtrs& volu camera.zoom_to_volumes(visible_volumes, thumbnail_data.width, thumbnail_data.height); camera.apply_viewport(0, 0, thumbnail_data.width, thumbnail_data.height); camera.apply_view_matrix(); - camera.apply_projection(box); + + double near_z = -1.0; + double far_z = -1.0; + + if (show_bed) + { + // extends the near and far z of the frustrum to avoid the bed being clipped + + // box in eye space + BoundingBoxf3 t_bed_box = m_bed.get_bounding_box(true).transformed(camera.get_view_matrix()); + near_z = -t_bed_box.max(2); + far_z = -t_bed_box.min(2); + } + + camera.apply_projection(box, near_z, far_z); if (transparent_background) - glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 0.0f)); + 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)); - shader.start_using(); + m_shader.start_using(); - GLint shader_id = shader.get_shader_program_id(); + 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(); @@ -3653,15 +3976,18 @@ static void render_volumes_in_thumbnail(Shader& shader, const GLVolumePtrs& volu vol->render(); } - shader.stop_using(); + m_shader.stop_using(); glsafe(::glDisable(GL_DEPTH_TEST)); + if (show_bed) + _render_bed(camera.get_theta(), false); + if (transparent_background) glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); } -void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background) +void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const { thumbnail_data.set(w, h); if (!thumbnail_data.is_valid()) @@ -3714,7 +4040,7 @@ void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, un if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { - render_volumes_in_thumbnail(m_shader, m_volumes.volumes, thumbnail_data, printable_only, parts_only, transparent_background); + _render_thumbnail_internal(thumbnail_data, printable_only, parts_only, show_bed, transparent_background); if (multisample) { @@ -3765,7 +4091,7 @@ void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, un glsafe(::glDisable(GL_MULTISAMPLE)); } -void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background) +void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData & thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const { thumbnail_data.set(w, h); if (!thumbnail_data.is_valid()) @@ -3818,7 +4144,7 @@ void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) { - render_volumes_in_thumbnail(m_shader, m_volumes.volumes, thumbnail_data, printable_only, parts_only, transparent_background); + _render_thumbnail_internal(thumbnail_data, printable_only, parts_only, show_bed, transparent_background); if (multisample) { @@ -3869,7 +4195,7 @@ void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data glsafe(::glDisable(GL_MULTISAMPLE)); } -void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background) +void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const { // check that thumbnail size does not exceed the default framebuffer size const Size& cnv_size = get_canvas_size(); @@ -3886,7 +4212,7 @@ void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigne if (!thumbnail_data.is_valid()) return; - render_volumes_in_thumbnail(m_shader, m_volumes.volumes, thumbnail_data, printable_only, parts_only, transparent_background); + _render_thumbnail_internal(thumbnail_data, printable_only, parts_only, show_bed, transparent_background); glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data())); #if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT @@ -3906,6 +4232,11 @@ bool GLCanvas3D::_init_toolbars() if (!_init_undoredo_toolbar()) return false; +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + if (!_init_view_toolbar()) + return false; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + return true; } @@ -4046,7 +4377,7 @@ bool GLCanvas3D::_init_main_toolbar() item.name = "layersediting"; item.icon_filename = "layers_white.svg"; - item.tooltip = _utf8(L("Height ranges")); + item.tooltip = _utf8(L("Variable layer height")); item.sprite_id = 10; item.left.toggable = true; item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); }; @@ -4163,6 +4494,13 @@ bool GLCanvas3D::_init_undoredo_toolbar() return true; } +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX +bool GLCanvas3D::_init_view_toolbar() +{ + return wxGetApp().plater()->init_view_toolbar(); +} +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + bool GLCanvas3D::_set_current() { return m_context != nullptr && m_canvas->SetCurrent(*m_context); @@ -4187,8 +4525,6 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h) // updates camera m_camera.apply_viewport(0, 0, w, h); - - m_dirty = false; } BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const @@ -4225,6 +4561,12 @@ void GLCanvas3D::_zoom_to_box(const BoundingBoxf3& box) } #endif // ENABLE_THUMBNAIL_GENERATOR +void GLCanvas3D::_update_camera_zoom(double zoom) +{ + m_camera.update_zoom(zoom); + m_dirty = true; +} + void GLCanvas3D::_refresh_if_shown_on_screen() { if (_is_shown_on_screen()) @@ -4406,13 +4748,13 @@ void GLCanvas3D::_render_background() const glsafe(::glPopMatrix()); } -void GLCanvas3D::_render_bed(float theta) const +void GLCanvas3D::_render_bed(float theta, bool show_axes) const { float scale_factor = 1.0; #if ENABLE_RETINA_GL scale_factor = m_retina_helper->get_scale_factor(); #endif // ENABLE_RETINA_GL - m_bed.render(const_cast<GLCanvas3D&>(*this), theta, scale_factor); + m_bed.render(const_cast<GLCanvas3D&>(*this), theta, scale_factor, show_axes); } void GLCanvas3D::_render_objects() const @@ -5024,7 +5366,7 @@ void GLCanvas3D::_load_print_toolpaths() volume->indexed_vertex_array.finalize_geometry(m_initialized); } -void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const std::vector<std::string>& str_tool_colors, const std::vector<double>& color_print_values) +void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const std::vector<std::string>& str_tool_colors, const std::vector<Model::CustomGCode>& color_print_values) { std::vector<float> tool_colors = _parse_colors(str_tool_colors); @@ -5036,11 +5378,14 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c bool has_infill; bool has_support; const std::vector<float>* tool_colors; - const std::vector<double>* color_print_values; + bool is_single_material_print; + int extruders_cnt; + const std::vector<Model::CustomGCode>* color_print_values; static const float* color_perimeters() { static float color[4] = { 1.0f, 1.0f, 0.0f, 1.f }; return color; } // yellow static const float* color_infill() { static float color[4] = { 1.0f, 0.5f, 0.5f, 1.f }; return color; } // redish static const float* color_support() { static float color[4] = { 0.5f, 1.0f, 0.5f, 1.f }; return color; } // greenish + static const float* color_pause_or_custom_code() { static float color[4] = { 0.5f, 0.5f, 0.5f, 1.f }; return color; } // gray // For cloring by a tool, return a parsed color. bool color_by_tool() const { return tool_colors != nullptr; } @@ -5050,9 +5395,106 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c // For coloring by a color_print(M600), return a parsed color. bool color_by_color_print() const { return color_print_values!=nullptr; } const size_t color_print_color_idx_by_layer_idx(const size_t layer_idx) const { - auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), layers[layer_idx]->print_z + EPSILON); + const Model::CustomGCode value(layers[layer_idx]->print_z + EPSILON, "", 0, ""); + auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); return (it - color_print_values->begin()) % number_tools(); } + + const size_t color_print_color_idx_by_layer_idx_and_extruder(const size_t layer_idx, const int extruder) const + { + const coordf_t print_z = layers[layer_idx]->print_z; + + auto it = std::find_if(color_print_values->begin(), color_print_values->end(), + [print_z](const Model::CustomGCode& code) + { return fabs(code.height - print_z) < EPSILON; }); + if (it != color_print_values->end()) + { + const std::string& code = it->gcode; + // pause print or custom Gcode + if (code == PausePrintCode || + (code != ColorChangeCode && code != ExtruderChangeCode)) + return number_tools()-1; // last color item is a gray color for pause print or custom G-code + + // change tool (extruder) + if (code == ExtruderChangeCode) + return get_color_idx_for_tool_change(it, extruder); + // change color for current extruder + if (code == ColorChangeCode) { + int color_idx = get_color_idx_for_color_change(it, extruder); + if (color_idx >= 0) + return color_idx; + } + } + + const Model::CustomGCode value(print_z + EPSILON, "", 0, ""); + it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value); + while (it != color_print_values->begin()) + { + --it; + // change color for current extruder + if (it->gcode == ColorChangeCode) { + int color_idx = get_color_idx_for_color_change(it, extruder); + if (color_idx >= 0) + return color_idx; + } + // change tool (extruder) + if (it->gcode == ExtruderChangeCode) + return get_color_idx_for_tool_change(it, extruder); + } + + return std::min<int>(extruders_cnt - 1, std::max<int>(extruder - 1, 0));; + } + + private: + int get_m600_color_idx(std::vector<Model::CustomGCode>::const_iterator it) const + { + int shift = 0; + while (it != color_print_values->begin()) { + --it; + if (it->gcode == ColorChangeCode) + shift++; + } + return extruders_cnt + shift; + } + + int get_color_idx_for_tool_change(std::vector<Model::CustomGCode>::const_iterator it, const int extruder) const + { + const int current_extruder = it->extruder == 0 ? extruder : it->extruder; + if (number_tools() == extruders_cnt + 1) // there is no one "M600" + return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0)); + + auto it_n = it; + while (it_n != color_print_values->begin()) { + --it_n; + if (it_n->gcode == ColorChangeCode && it_n->extruder == current_extruder) + return get_m600_color_idx(it_n); + } + + return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0)); + } + + int get_color_idx_for_color_change(std::vector<Model::CustomGCode>::const_iterator it, const int extruder) const + { + if (extruders_cnt == 1) + return get_m600_color_idx(it); + + auto it_n = it; + bool is_tool_change = false; + while (it_n != color_print_values->begin()) { + --it_n; + if (it_n->gcode == ExtruderChangeCode) { + is_tool_change = true; + if (it_n->extruder == it->extruder || (it_n->extruder == 0 && it->extruder == extruder)) + return get_m600_color_idx(it); + break; + } + } + if (!is_tool_change && it->extruder == extruder) + return get_m600_color_idx(it); + + return -1; + } + } ctxt; ctxt.has_perimeters = print_object.is_step_done(posPerimeters); @@ -5060,6 +5502,8 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c ctxt.has_support = print_object.is_step_done(posSupportMaterial); ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors; ctxt.color_print_values = color_print_values.empty() ? nullptr : &color_print_values; + ctxt.is_single_material_print = this->fff_print()->extruders().size()==1; + ctxt.extruders_cnt = wxGetApp().extruders_edited_cnt(); ctxt.shifted_copies = &print_object.copies(); @@ -5083,6 +5527,8 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c // Maximum size of an allocation block: 32MB / sizeof(float) BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info(); + const bool is_selected_separate_extruder = m_selected_extruder > 0 && ctxt.color_by_color_print(); + //FIXME Improve the heuristics for a grain size. size_t grain_size = std::max(ctxt.layers.size() / 16, size_t(1)); tbb::spin_mutex new_volume_mutex; @@ -5100,32 +5546,18 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c const size_t volumes_cnt_initial = m_volumes.volumes.size(); tbb::parallel_for( tbb::blocked_range<size_t>(0, ctxt.layers.size(), grain_size), - [&ctxt, &new_volume](const tbb::blocked_range<size_t>& range) { + [&ctxt, &new_volume, is_selected_separate_extruder, this](const tbb::blocked_range<size_t>& range) { GLVolumePtrs vols; std::vector<size_t> color_print_layer_to_glvolume; - auto volume = [&ctxt, &vols, &color_print_layer_to_glvolume, &range](size_t layer_idx, int extruder, int feature) -> GLVolume& { - return *vols[ctxt.color_by_color_print() ? - color_print_layer_to_glvolume[layer_idx - range.begin()] : + auto volume = [&ctxt, &vols, &color_print_layer_to_glvolume, &range](size_t layer_idx, int extruder, int feature) -> GLVolume& { + return *vols[ctxt.color_by_color_print()? + ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) : ctxt.color_by_tool() ? std::min<int>(ctxt.number_tools() - 1, std::max<int>(extruder - 1, 0)) : feature ]; }; - if (ctxt.color_by_color_print()) { - // Create a map from the layer index to a GLVolume, which is initialized with the correct layer span color. - std::vector<int> color_print_tool_to_glvolume(ctxt.number_tools(), -1); - color_print_layer_to_glvolume.reserve(range.end() - range.begin()); - vols.reserve(ctxt.number_tools()); - for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { - int idx_tool = (int)ctxt.color_print_color_idx_by_layer_idx(idx_layer); - if (color_print_tool_to_glvolume[idx_tool] == -1) { - color_print_tool_to_glvolume[idx_tool] = (int)vols.size(); - vols.emplace_back(new_volume(ctxt.color_tool(idx_tool))); - } - color_print_layer_to_glvolume.emplace_back(color_print_tool_to_glvolume[idx_tool]); - } - } - else if (ctxt.color_by_tool()) { + if (ctxt.color_by_color_print() || ctxt.color_by_tool()) { for (size_t i = 0; i < ctxt.number_tools(); ++i) vols.emplace_back(new_volume(ctxt.color_tool(i))); } @@ -5136,6 +5568,26 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c vol->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6); for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { const Layer *layer = ctxt.layers[idx_layer]; + + if (is_selected_separate_extruder) + { + bool at_least_one_has_correct_extruder = false; + for (const LayerRegion* layerm : layer->regions()) + { + if (layerm->slices.surfaces.empty()) + continue; + const PrintRegionConfig& cfg = layerm->region()->config(); + if (cfg.perimeter_extruder.value == m_selected_extruder || + cfg.infill_extruder.value == m_selected_extruder || + cfg.solid_infill_extruder.value == m_selected_extruder ) { + at_least_one_has_correct_extruder = true; + break; + } + } + if (!at_least_one_has_correct_extruder) + continue; + } + for (GLVolume *vol : vols) if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) { vol->print_zs.push_back(layer->print_z); @@ -5144,6 +5596,14 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c } for (const Point © : *ctxt.shifted_copies) { for (const LayerRegion *layerm : layer->regions()) { + if (is_selected_separate_extruder) + { + const PrintRegionConfig& cfg = layerm->region()->config(); + if (cfg.perimeter_extruder.value != m_selected_extruder || + cfg.infill_extruder.value != m_selected_extruder || + cfg.solid_infill_extruder.value != m_selected_extruder) + continue; + } if (ctxt.has_perimeters) _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, volume(idx_layer, layerm->region()->config().perimeter_extruder.value, 0)); @@ -5427,11 +5887,8 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat case GCodePreviewData::Extrusion::ColorPrint: { int color_cnt = (int)tool_colors.size() / 4; + int val = value > color_cnt ? color_cnt - 1 : value; - int val = int(value); - while (val >= color_cnt) - val -= color_cnt; - GCodePreviewData::Color color; ::memcpy((void*)color.rgba, (const void*)(tool_colors.data() + val * 4), 4 * sizeof(float)); @@ -5492,10 +5949,13 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat BOOST_LOG_TRIVIAL(debug) << "Loading G-code extrusion paths - populate volumes" << m_volumes.log_memory_info() << log_memory_info(); // populates volumes + const bool is_selected_separate_extruder = m_selected_extruder > 0 && preview_data.extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint; for (const GCodePreviewData::Extrusion::Layer& layer : preview_data.extrusion.layers) { for (const GCodePreviewData::Extrusion::Path& path : layer.paths) { + if (is_selected_separate_extruder && path.extruder_id != m_selected_extruder - 1) + continue; std::vector<std::pair<float, GLVolume*>> &filters = roles_filters[size_t(path.extrusion_role)]; auto key = std::make_pair<float, GLVolume*>(Helper::path_filter(preview_data.extrusion.view_type, path), nullptr); auto it_filter = std::lower_bound(filters.begin(), filters.end(), key); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 32974a8df..8df5fb03b 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -81,7 +81,11 @@ template <size_t N> using Vec2dsEvent = ArrayEvent<Vec2d, N>; using Vec3dEvent = Event<Vec3d>; template <size_t N> using Vec3dsEvent = ArrayEvent<Vec3d, N>; +using HeightProfileSmoothEvent = Event<HeightProfileSmoothingParams>; + +#if !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX wxDECLARE_EVENT(EVT_GLCANVAS_INIT, SimpleEvent); +#endif // !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX wxDECLARE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); @@ -104,6 +108,11 @@ wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, wxKeyEvent); wxDECLARE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE +wxDECLARE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent); +wxDECLARE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event<float>); +wxDECLARE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE class GLCanvas3D { @@ -153,13 +162,17 @@ private: private: static const float THICKNESS_BAR_WIDTH; +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static const float THICKNESS_RESET_BUTTON_HEIGHT; +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE bool m_enabled; Shader m_shader; unsigned int m_z_texture_id; +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE mutable GLTexture m_tooltip_texture; mutable GLTexture m_reset_texture; +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // Not owned by LayersEditing. const DynamicPrintConfig *m_config; // ModelObject for the currently selected object (Model::objects[last_object_id]). @@ -168,9 +181,14 @@ private: float m_object_max_z; // Owned by LayersEditing. SlicingParameters *m_slicing_parameters; - std::vector<coordf_t> m_layer_height_profile; + std::vector<double> m_layer_height_profile; bool m_layer_height_profile_modified; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + mutable float m_adaptive_cusp; + mutable HeightProfileSmoothingParams m_smooth_params; +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + class LayersTexture { public: @@ -217,28 +235,44 @@ private: void adjust_layer_height_profile(); void accept_changes(GLCanvas3D& canvas); void reset_layer_height_profile(GLCanvas3D& canvas); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void adaptive_layer_height_profile(GLCanvas3D& canvas, float cusp); + void smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_paramsn); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static float get_cursor_z_relative(const GLCanvas3D& canvas); static bool bar_rect_contains(const GLCanvas3D& canvas, float x, float y); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static bool reset_rect_contains(const GLCanvas3D& canvas, float x, float y); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static Rect get_bar_rect_screen(const GLCanvas3D& canvas); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static Rect get_reset_rect_screen(const GLCanvas3D& canvas); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static Rect get_bar_rect_viewport(const GLCanvas3D& canvas); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static Rect get_reset_rect_viewport(const GLCanvas3D& canvas); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE float object_max_z() const { return m_object_max_z; } + std::string get_tooltip(const GLCanvas3D& canvas) const; + private: - bool _is_initialized() const; + bool is_initialized() const; void generate_layer_height_texture(); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE void _render_tooltip_texture(const GLCanvas3D& canvas, const Rect& bar_rect, const Rect& reset_rect) const; void _render_reset_texture(const Rect& reset_rect) const; - void _render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const; - void _render_profile(const Rect& bar_rect) const; +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const; + void render_profile(const Rect& bar_rect) const; void update_slicing_parameters(); static float thickness_bar_width(const GLCanvas3D &canvas); +#if !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE static float reset_button_height(const GLCanvas3D &canvas); +#endif // !ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE }; struct Mouse @@ -352,8 +386,10 @@ private: public: LegendTexture(); - void fill_color_print_legend_values(const GCodePreviewData& preview_data, const GLCanvas3D& canvas, - std::vector<std::pair<double, double>>& cp_legend_values); + void fill_color_print_legend_items(const GLCanvas3D& canvas, + const std::vector<float>& colors_in, + std::vector<float>& colors, + std::vector<std::string>& cp_legend_items); bool generate(const GCodePreviewData& preview_data, const std::vector<float>& tool_colors, const GLCanvas3D& canvas, bool compress); @@ -383,7 +419,6 @@ private: std::unique_ptr<RetinaHelper> m_retina_helper; #endif bool m_in_render; - bool m_render_enabled; LegendTexture m_legend_texture; WarningTexture m_warning_texture; wxTimer m_timer; @@ -442,7 +477,8 @@ private: RenderStats m_render_stats; #endif // ENABLE_RENDER_STATISTICS - int m_imgui_undo_redo_hovered_pos{ -1 }; + mutable int m_imgui_undo_redo_hovered_pos{ -1 }; + int m_selected_extruder; public: GLCanvas3D(wxGLCanvas* canvas, Bed3D& bed, Camera& camera, GLToolbar& view_toolbar); @@ -494,6 +530,7 @@ public: const Camera& get_camera() const { return m_camera; } const Shader& get_shader() const { return m_shader; } + Camera& get_camera() { return m_camera; } BoundingBoxf3 volumes_bounding_box() const; BoundingBoxf3 scene_bounding_box() const; @@ -501,6 +538,12 @@ public: bool is_layers_editing_enabled() const; bool is_layers_editing_allowed() const; +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + void reset_layer_height_profile(); + void adaptive_layer_height_profile(float cusp); + void smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + bool is_reload_delayed() const; void enable_layers_editing(bool enable); @@ -514,9 +557,6 @@ public: void enable_dynamic_background(bool enable); void allow_multisample(bool allow); - void enable_render(bool enable) { m_render_enabled = enable; } - bool is_render_enabled() const { return m_render_enabled; } - void zoom_to_bed(); void zoom_to_volumes(); void zoom_to_selection(); @@ -530,7 +570,7 @@ public: #if ENABLE_THUMBNAIL_GENERATOR // printable_only == false -> render also non printable volumes as grayed // parts_only == false -> render also sla support and pad - void render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background); + void render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const; #endif // ENABLE_THUMBNAIL_GENERATOR void select_all(); @@ -550,7 +590,7 @@ public: void load_gcode_preview(const GCodePreviewData& preview_data, const std::vector<std::string>& str_tool_colors); void load_sla_preview(); - void load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<double>& color_print_values); + void load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<Model::CustomGCode>& color_print_values); void bind_event_handlers(); void unbind_event_handlers(); @@ -577,8 +617,6 @@ public: void do_flatten(const Vec3d& normal, const std::string& snapshot_type); void do_mirror(const std::string& snapshot_type); - void set_camera_zoom(double zoom); - void update_gizmos_on_off_state(); void reset_all_gizmos() { m_gizmos.reset_all_states(); } @@ -591,6 +629,7 @@ public: int get_move_volume_id() const { return m_mouse.drag.move_volume_idx; } int get_first_hover_volume_idx() const { return m_hover_volume_idxs.empty() ? -1 : m_hover_volume_idxs.front(); } + void set_selected_extruder(int extruder) { m_selected_extruder = extruder;} class WipeTowerInfo { protected: @@ -645,6 +684,9 @@ private: bool _init_toolbars(); bool _init_main_toolbar(); bool _init_undoredo_toolbar(); +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + bool _init_view_toolbar(); +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX bool _set_current(); void _resize(unsigned int w, unsigned int h); @@ -656,13 +698,14 @@ private: #else void _zoom_to_box(const BoundingBoxf3& box); #endif // ENABLE_THUMBNAIL_GENERATOR + void _update_camera_zoom(double zoom); void _refresh_if_shown_on_screen(); void _picking_pass() const; void _rectangular_selection_picking_pass() const; void _render_background() const; - void _render_bed(float theta) const; + void _render_bed(float theta, bool show_axes) const; void _render_objects() const; void _render_selection() const; #if ENABLE_RENDER_SELECTION_CENTER @@ -682,14 +725,15 @@ private: #endif // ENABLE_SHOW_CAMERA_TARGET void _render_sla_slices() const; void _render_selection_sidebar_hints() const; - void _render_undo_redo_stack(const bool is_undo, float pos_x); + void _render_undo_redo_stack(const bool is_undo, float pos_x) const; #if ENABLE_THUMBNAIL_GENERATOR + void _render_thumbnail_internal(ThumbnailData& thumbnail_data, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const; // render thumbnail using an off-screen framebuffer - void _render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background); + void _render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const; // render thumbnail using an off-screen framebuffer when GLEW_EXT_framebuffer_object is supported - void _render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background); + void _render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const; // render thumbnail using the default framebuffer - void _render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background); + void _render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) const; #endif // ENABLE_THUMBNAIL_GENERATOR void _update_volumes_hover_state() const; @@ -713,7 +757,7 @@ private: // Adds a new Slic3r::GUI::3DScene::Volume to $self->volumes, // one for perimeters, one for infill and one for supports. void _load_print_object_toolpaths(const PrintObject& print_object, const std::vector<std::string>& str_tool_colors, - const std::vector<double>& color_print_values); + const std::vector<Model::CustomGCode>& color_print_values); // Create 3D thick extrusion lines for wipe tower extrusions void _load_wipe_tower_toolpaths(const std::vector<std::string>& str_tool_colors); diff --git a/src/slic3r/GUI/GLShader.cpp b/src/slic3r/GUI/GLShader.cpp index 577f6e1b5..11f109fd5 100644 --- a/src/slic3r/GUI/GLShader.cpp +++ b/src/slic3r/GUI/GLShader.cpp @@ -118,9 +118,9 @@ bool GLShader::load_from_text(const char *fragment_shader, const char *vertex_sh glsafe(::glGetProgramiv(this->shader_program_id, GL_LINK_STATUS, ¶ms)); if (params == GL_FALSE) { // Linking failed. Get the log. - glsafe(::glGetProgramiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, ¶ms)); + glsafe(::glGetProgramiv(this->shader_program_id, GL_INFO_LOG_LENGTH, ¶ms)); std::vector<char> msg(params); - glsafe(::glGetProgramInfoLog(this->vertex_program_id, params, ¶ms, msg.data())); + glsafe(::glGetProgramInfoLog(this->shader_program_id, params, ¶ms, msg.data())); this->last_error = std::string("Shader linking failed:\n") + msg.data(); this->release(); return false; diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp index 51d787d9d..2fc0590d5 100644 --- a/src/slic3r/GUI/GLToolbar.cpp +++ b/src/slic3r/GUI/GLToolbar.cpp @@ -368,7 +368,7 @@ void GLToolbar::get_additional_tooltip(int item_id, std::string& text) } } - text = L(""); + text.clear(); } void GLToolbar::set_additional_tooltip(int item_id, const std::string& text) @@ -443,7 +443,7 @@ bool GLToolbar::on_mouse(wxMouseEvent& evt, GLCanvas3D& parent) if (item_id == -1) { // mouse is outside the toolbar - m_tooltip = L(""); + m_tooltip.clear(); } else { @@ -610,7 +610,7 @@ void GLToolbar::do_action(GLToolbarItem::EActionType type, int item_id, GLCanvas std::string GLToolbar::update_hover_state(const Vec2d& mouse_pos, GLCanvas3D& parent) { if (!m_enabled) - return L(""); + return ""; switch (m_layout.type) { @@ -665,7 +665,7 @@ std::string GLToolbar::update_hover_state_horizontal(const Vec2d& mouse_pos, GLC { const std::string& additional_tooltip = item->get_additional_tooltip(); if (!additional_tooltip.empty()) - tooltip += L("\n") + additional_tooltip; + tooltip += "\n" + additional_tooltip; } } @@ -769,7 +769,7 @@ std::string GLToolbar::update_hover_state_vertical(const Vec2d& mouse_pos, GLCan { const std::string& additional_tooltip = item->get_additional_tooltip(); if (!additional_tooltip.empty()) - tooltip += L("\n") + additional_tooltip; + tooltip += "\n" + additional_tooltip; } } @@ -1194,7 +1194,12 @@ bool GLToolbar::generate_icons_texture() const states.push_back(std::make_pair(1, true)); } - bool res = m_icons_texture.load_from_svg_files_as_sprites_array(filenames, states, (unsigned int)(m_layout.icons_size * m_layout.scale), true); + unsigned int sprite_size_px = (unsigned int)(m_layout.icons_size * m_layout.scale); +// // force even size +// if (sprite_size_px % 2 != 0) +// sprite_size_px += 1; + + bool res = m_icons_texture.load_from_svg_files_as_sprites_array(filenames, states, sprite_size_px, false); if (res) m_icons_texture_dirty = false; diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp index 07da836fc..eb73dcdfd 100644 --- a/src/slic3r/GUI/GLToolbar.hpp +++ b/src/slic3r/GUI/GLToolbar.hpp @@ -293,6 +293,9 @@ public: bool is_any_item_pressed() const; +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + unsigned int get_items_count() const { return (unsigned int)m_items.size(); } +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX int get_item_id(const std::string& name) const; void force_left_action(int item_id, GLCanvas3D& parent) { do_action(GLToolbarItem::Left, item_id, parent, false); } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7d37baa66..e02dd5f6e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -51,10 +51,10 @@ #include <Shlobj.h> #endif // __WXMSW__ -#if ENABLE_THUMBNAIL_GENERATOR +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG #include <boost/beast/core/detail/base64.hpp> #include <boost/nowide/fstream.hpp> -#endif // ENABLE_THUMBNAIL_GENERATOR +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG namespace Slic3r { namespace GUI { @@ -185,7 +185,9 @@ bool GUI_App::on_init_inner() wxCHECK_MSG(wxDirExists(resources_dir), false, wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - SetAppName(SLIC3R_APP_KEY); + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + // SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); SetAppDisplayName(SLIC3R_APP_NAME); // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. @@ -281,7 +283,7 @@ bool GUI_App::on_init_inner() PresetUpdater::UpdateResult updater_result; try { - updater_result = preset_updater->config_update(); + updater_result = preset_updater->config_update(app_config->orig_version()); if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { mainframe->Close(); } else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { @@ -464,6 +466,9 @@ void GUI_App::recreate_GUI() dlg.Update(30, _(L("Recreating")) + dots); topwindow->Destroy(); + + // For this moment ConfigWizard is deleted, invalidate it + m_wizard = nullptr; } dlg.Update(80, _(L("Loading of current presets")) + dots); @@ -1126,7 +1131,6 @@ void GUI_App::gcode_thumbnails_debug() } else if (reading_image && boost::starts_with(gcode_line, END_MASK)) { -#if ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); if (out_file.good()) @@ -1138,46 +1142,6 @@ void GUI_App::gcode_thumbnails_debug() out_file.write(decoded.c_str(), decoded.size()); out_file.close(); } -#else - if (!row.empty()) - { - rows.push_back(row); - row.clear(); - } - - if ((unsigned int)rows.size() == height) - { - std::vector<unsigned char> thumbnail(4 * width * height, 0); - for (unsigned int r = 0; r < (unsigned int)rows.size(); ++r) - { - std::string decoded_row; - decoded_row.resize(boost::beast::detail::base64::decoded_size(rows[r].size())); - decoded_row.resize(boost::beast::detail::base64::decode((void*)&decoded_row[0], rows[r].data(), rows[r].size()).first); - - if ((unsigned int)decoded_row.size() == width * 4) - { - void* image_ptr = (void*)(thumbnail.data() + r * width * 4); - ::memcpy(image_ptr, (const void*)decoded_row.c_str(), width * 4); - } - } - - wxImage image(width, height); - image.InitAlpha(); - - for (unsigned int r = 0; r < height; ++r) - { - unsigned int rr = r * width; - for (unsigned int c = 0; c < width; ++c) - { - unsigned char* px = thumbnail.data() + 4 * (rr + c); - image.SetRGB((int)c, (int)r, px[0], px[1], px[2]); - image.SetAlpha((int)c, (int)r, px[3]); - } - } - - image.SaveFile(out_path + std::to_string(width) + "x" + std::to_string(height) + ".png", wxBITMAP_TYPE_PNG); - } -#endif // ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE reading_image = false; width = 0; @@ -1185,17 +1149,7 @@ void GUI_App::gcode_thumbnails_debug() rows.clear(); } else if (reading_image) - { -#if !ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE - if (!row.empty() && (gcode_line[1] == ' ')) - { - rows.push_back(row); - row.clear(); - } -#endif // !ENABLE_THUMBNAIL_GENERATOR_PNG_TO_GCODE - row += gcode_line.substr(2); - } } } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bc912086e..a730ff95f 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -188,10 +188,10 @@ public: void open_web_page_localized(const std::string &http_address); bool run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page = ConfigWizard::SP_WELCOME); -#if ENABLE_THUMBNAIL_GENERATOR +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG // temporary and debug only -> extract thumbnails from selected gcode and save them as png files void gcode_thumbnails_debug(); -#endif // ENABLE_THUMBNAIL_GENERATOR +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG private: bool on_init_inner(); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 45d55c0b9..025262a23 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -59,6 +59,11 @@ static PrinterTechnology printer_technology() return wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology(); } +static const Selection& scene_selection() +{ + return wxGetApp().plater()->canvas3D()->get_selection(); +} + // Config from current edited printer preset static DynamicPrintConfig& printer_config() { @@ -329,7 +334,7 @@ wxString ObjectList::get_mesh_errors_list(const int obj_idx, const int vol_idx / return ""; // hide tooltip // Create tooltip string, if there are errors - wxString tooltip = wxString::Format(_(L("Auto-repaired (%d errors):\n")), errors); + wxString tooltip = wxString::Format(_(L("Auto-repaired (%d errors):")), errors) + "\n"; const stl_stats& stats = vol_idx == -1 ? (*m_objects)[obj_idx]->get_object_stl_stats() : @@ -526,6 +531,8 @@ void ObjectList::update_extruder_in_config(const wxDataViewItem& item) if (!m_config) return; + take_snapshot(_(L("Change Extruder"))); + const int extruder = m_objects_model->GetExtruderNumber(item); m_config->set_key_value("extruder", new ConfigOptionInt(extruder)); @@ -806,7 +813,7 @@ void ObjectList::list_manipulation(bool evt_context_menu/* = false*/) if (!item) { if (col == nullptr) { - if (wxOSX) + if (wxOSX && !multiple_selection()) UnselectAll(); else if (!evt_context_menu) // Case, when last item was deleted and under GTK was called wxEVT_DATAVIEW_SELECTION_CHANGED, @@ -821,8 +828,13 @@ void ObjectList::list_manipulation(bool evt_context_menu/* = false*/) } if (wxOSX && item && col) { + wxDataViewItemArray sels; + GetSelections(sels); UnselectAll(); - Select(item); + if (sels.Count() > 1) + SetSelections(sels); + else + Select(item); } const wxString title = col->GetTitle(); @@ -896,10 +908,6 @@ void ObjectList::extruder_editing() if (!item || !(m_objects_model->GetItemType(item) & (itVolume | itObject))) return; - std::vector<wxBitmap*> icons = get_extruder_color_icons(); - if (icons.empty()) - return; - const int column_width = GetColumn(colExtruder)->GetWidth() + wxSystemSettings::GetMetric(wxSYS_VSCROLL_X) + 5; wxPoint pos = get_mouse_position_in_control(); @@ -907,29 +915,10 @@ void ObjectList::extruder_editing() pos.x = GetColumn(colName)->GetWidth() + GetColumn(colPrint)->GetWidth() + 5; pos.y -= GetTextExtent("m").y; - if (!m_extruder_editor) - m_extruder_editor = new wxBitmapComboBox(this, wxID_ANY, wxEmptyString, pos, size, - 0, nullptr, wxCB_READONLY); - else - { - m_extruder_editor->SetPosition(pos); - m_extruder_editor->SetMinSize(size); - m_extruder_editor->SetSize(size); - m_extruder_editor->Clear(); - m_extruder_editor->Show(); - } - - int i = 0; - for (wxBitmap* bmp : icons) { - if (i == 0) { - m_extruder_editor->Append(_(L("default")), *bmp); - ++i; - } + apply_extruder_selector(&m_extruder_editor, this, L("default"), pos, size); - m_extruder_editor->Append(wxString::Format("%d", i), *bmp); - ++i; - } m_extruder_editor->SetSelection(m_objects_model->GetExtruderNumber(item)); + m_extruder_editor->Show(); auto set_extruder = [this]() { @@ -941,6 +930,7 @@ void ObjectList::extruder_editing() m_objects_model->SetExtruder(m_extruder_editor->GetString(selection), item); m_extruder_editor->Hide(); + update_extruder_in_config(item); }; // to avoid event propagation to other sidebar items @@ -949,13 +939,6 @@ void ObjectList::extruder_editing() set_extruder(); evt.StopPropagation(); }); - /* - m_extruder_editor->Bind(wxEVT_KILL_FOCUS, [set_extruder](wxFocusEvent& evt) - { - set_extruder(); - evt.Skip(); - });*/ - } void ObjectList::copy() @@ -1335,7 +1318,10 @@ void ObjectList::get_settings_choice(const wxString& category_name) void ObjectList::get_freq_settings_choice(const wxString& bundle_name) { std::vector<std::string> options = get_options_for_bundle(bundle_name); - wxDataViewItem item = GetSelection(); + const Selection& selection = scene_selection(); + wxDataViewItem item = GetSelectedItemsCount() > 1 && selection.is_single_full_object() ? + m_objects_model->GetItemById(selection.get_object_idx()) : + GetSelection(); ItemType item_type = m_objects_model->GetItemType(item); @@ -1423,17 +1409,23 @@ void ObjectList::append_menu_items_add_volume(wxMenu* menu) const ConfigOptionMode mode = wxGetApp().get_mode(); + wxWindow* parent = wxGetApp().plater(); + if (mode == comAdvanced) { append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].first), "", - [this](wxCommandEvent&) { load_subobject(ModelVolumeType::MODEL_PART); }, ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].second); + [this](wxCommandEvent&) { load_subobject(ModelVolumeType::MODEL_PART); }, + ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].second, nullptr, + [this]() { return is_instance_or_object_selected(); }, parent); } if (mode == comSimple) { append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "", [this](wxCommandEvent&) { load_generic_subobject(L("Box"), ModelVolumeType::SUPPORT_ENFORCER); }, - ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].second); + ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].second, nullptr, + [this]() { return is_instance_or_object_selected(); }, parent); append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_BLOCKER)].first), "", [this](wxCommandEvent&) { load_generic_subobject(L("Box"), ModelVolumeType::SUPPORT_BLOCKER); }, - ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_BLOCKER)].second); + ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_BLOCKER)].second, nullptr, + [this]() { return is_instance_or_object_selected(); }, parent); return; } @@ -1443,7 +1435,8 @@ void ObjectList::append_menu_items_add_volume(wxMenu* menu) auto& item = ADD_VOLUME_MENU_ITEMS[type]; wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); - append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second); + append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, + [this]() { return is_instance_or_object_selected(); }, parent); } } @@ -1454,10 +1447,17 @@ wxMenuItem* ObjectList::append_menu_item_split(wxMenu* menu) [this]() { return is_splittable(); }, wxGetApp().plater()); } -wxMenuItem* ObjectList::append_menu_item_layers_editing(wxMenu* menu) +bool ObjectList::is_instance_or_object_selected() +{ + const Selection& selection = scene_selection(); + return selection.is_single_full_instance() || selection.is_single_full_object(); +} + +wxMenuItem* ObjectList::append_menu_item_layers_editing(wxMenu* menu, wxWindow* parent) { return append_menu_item(menu, wxID_ANY, _(L("Height range Modifier")), "", - [this](wxCommandEvent&) { layers_editing(); }, "edit_layers_all", menu); + [this](wxCommandEvent&) { layers_editing(); }, "edit_layers_all", menu, + [this]() { return is_instance_or_object_selected(); }, parent); } wxMenuItem* ObjectList::append_menu_item_settings(wxMenu* menu_) @@ -1498,6 +1498,12 @@ wxMenuItem* ObjectList::append_menu_item_settings(wxMenu* menu_) #endif menu->DestroySeparators(); // delete old separators + // If there are selected more then one instance but not all of them + // don't add settings menu items + const Selection& selection = scene_selection(); + if (selection.is_multiple_full_instance() && !selection.is_single_full_object()) + return nullptr; + const auto sel_vol = get_selected_model_volume(); if (sel_vol && sel_vol->type() >= ModelVolumeType::SUPPORT_ENFORCER) return nullptr; @@ -1513,7 +1519,8 @@ wxMenuItem* ObjectList::append_menu_item_settings(wxMenu* menu_) menu->SetFirstSeparator(); // Add frequently settings - const bool is_object_settings = m_objects_model->GetItemType(GetSelection()) == itObject; + const ItemType item_type = m_objects_model->GetItemType(GetSelection()); + const bool is_object_settings = item_type & itObject || item_type & itInstance || selection.is_single_full_object(); create_freq_settings_popupmenu(menu, is_object_settings); if (mode == comAdvanced) @@ -1539,15 +1546,35 @@ wxMenuItem* ObjectList::append_menu_item_change_type(wxMenu* menu) wxMenuItem* ObjectList::append_menu_item_instance_to_object(wxMenu* menu, wxWindow* parent) { - return append_menu_item(menu, wxID_ANY, _(L("Set as a Separated Object")), "", + wxMenuItem* menu_item = append_menu_item(menu, wxID_ANY, _(L("Set as a Separated Object")), "", [this](wxCommandEvent&) { split_instances(); }, "", menu, [](){return wxGetApp().plater()->can_set_instance_to_object(); }, parent); + + /* New behavior logic: + * 1. Split Object to several separated object, if ALL instances are selected + * 2. Separate selected instances from the initial object to the separated object, + * if some (not all) instances are selected + */ + parent->Bind(wxEVT_UPDATE_UI, [](wxUpdateUIEvent& evt) { + evt.SetText(wxGetApp().plater()->canvas3D()->get_selection().is_single_full_object() ? + _(L("Set as a Separated Objects")) : _(L("Set as a Separated Object"))); + }, menu_item->GetId()); + + return menu_item; } wxMenuItem* ObjectList::append_menu_item_printable(wxMenu* menu, wxWindow* /*parent*/) { - return append_menu_check_item(menu, wxID_ANY, _(L("Printable")), "", [](wxCommandEvent&) { - wxGetApp().plater()->canvas3D()->get_selection().toggle_instance_printable_state(); - }, menu); + return append_menu_check_item(menu, wxID_ANY, _(L("Printable")), "", [this](wxCommandEvent&) { + const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + wxDataViewItem item; + if (GetSelectedItemsCount() > 1 && selection.is_single_full_object()) + item = m_objects_model->GetItemById(selection.get_object_idx()); + else + item = GetSelection(); + + if (item) + toggle_printable_state(item); + }, menu); } void ObjectList::append_menu_items_osx(wxMenu* menu) @@ -1643,7 +1670,7 @@ void ObjectList::create_object_popupmenu(wxMenu *menu) menu->AppendSeparator(); // Layers Editing for object - append_menu_item_layers_editing(menu); + append_menu_item_layers_editing(menu, wxGetApp().plater()); menu->AppendSeparator(); // rest of a object_menu will be added later in: @@ -1687,17 +1714,6 @@ void ObjectList::create_part_popupmenu(wxMenu *menu) void ObjectList::create_instance_popupmenu(wxMenu*menu) { m_menu_item_split_instances = append_menu_item_instance_to_object(menu, wxGetApp().plater()); - - /* New behavior logic: - * 1. Split Object to several separated object, if ALL instances are selected - * 2. Separate selected instances from the initial object to the separated object, - * if some (not all) instances are selected - */ - wxGetApp().plater()->Bind(wxEVT_UPDATE_UI, [](wxUpdateUIEvent& evt) { -// evt.Enable(can_split_instances()); }, m_menu_item_split_instances->GetId()); - evt.SetText(wxGetApp().plater()->canvas3D()->get_selection().is_single_full_object() ? - _(L("Set as a Separated Objects")) : _(L("Set as a Separated Object"))); - }, m_menu_item_split_instances->GetId()); } void ObjectList::create_default_popupmenu(wxMenu*menu) @@ -1711,7 +1727,7 @@ wxMenu* ObjectList::create_settings_popupmenu(wxMenu *parent_menu) wxMenu *menu = new wxMenu; settings_menu_hierarchy settings_menu; - const bool is_part = m_objects_model->GetParent(GetSelection()) != wxDataViewItem(nullptr); + const bool is_part = !(m_objects_model->GetItemType(GetSelection()) == itObject || scene_selection().is_single_full_object()); get_options_menu(settings_menu, is_part); for (auto cat : settings_menu) { @@ -1880,7 +1896,7 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode if (obj_idx < 0) return; - const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + const Selection& selection = scene_selection(); assert(obj_idx == selection.get_object_idx()); /** Any changes of the Object's composition is duplicated for all Object's Instances @@ -2205,9 +2221,13 @@ void ObjectList::split() void ObjectList::layers_editing() { - const auto item = GetSelection(); - const int obj_idx = get_selected_obj_idx(); - if (!item || obj_idx < 0) + const Selection& selection = scene_selection(); + const int obj_idx = selection.get_object_idx(); + wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ? + m_objects_model->GetItemById(obj_idx) : + GetSelection(); + + if (!item) return; const wxDataViewItem obj_item = m_objects_model->GetTopParent(item); @@ -2320,7 +2340,7 @@ bool ObjectList::selected_instances_of_same_object() bool ObjectList::can_split_instances() { - const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + const Selection& selection = scene_selection(); return selection.is_multiple_full_instance() || selection.is_single_full_instance(); } @@ -2348,7 +2368,7 @@ void ObjectList::part_selection_changed() { og_name = _(L("Group manipulation")); - const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + const Selection& selection = scene_selection(); // don't show manipulation panel for case of all Object's parts selection update_and_show_manipulations = !selection.is_single_full_instance(); } @@ -2956,7 +2976,7 @@ int ObjectList::get_selected_layers_range_idx() const void ObjectList::update_selections() { - const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + const Selection& selection = scene_selection(); wxDataViewItemArray sels; if ( ( m_selection_mode & (smSettings|smLayer|smLayerRoot) ) == 0) @@ -3663,7 +3683,7 @@ void ObjectList::instances_to_separated_objects(const int obj_idx) void ObjectList::split_instances() { - const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + const Selection& selection = scene_selection(); const int obj_idx = selection.get_object_idx(); if (obj_idx == -1) return; @@ -3876,6 +3896,9 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) const wxDataViewItemArray sels; GetSelections(sels); + if (!sels.empty()) + take_snapshot(_(L("Change Extruders"))); + for (const wxDataViewItem& item : sels) { DynamicPrintConfig& config = get_item_config(item); diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index 0874343b6..73b23453b 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -226,11 +226,12 @@ public: void get_settings_choice(const wxString& category_name); void get_freq_settings_choice(const wxString& bundle_name); void show_settings(const wxDataViewItem settings_item); + bool is_instance_or_object_selected(); wxMenu* append_submenu_add_generic(wxMenu* menu, const ModelVolumeType type); void append_menu_items_add_volume(wxMenu* menu); wxMenuItem* append_menu_item_split(wxMenu* menu); - wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); + wxMenuItem* append_menu_item_layers_editing(wxMenu* menu, wxWindow* parent); wxMenuItem* append_menu_item_settings(wxMenu* menu); wxMenuItem* append_menu_item_change_type(wxMenu* menu); wxMenuItem* append_menu_item_instance_to_object(wxMenu* menu, wxWindow* parent); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 937e3dbdc..36293525a 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -243,11 +243,13 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : // Add Axes labels with icons static const char axes[] = { 'X', 'Y', 'Z' }; +// std::vector<wxString> axes_color = {"#990000", "#009900","#000099"}; for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) { const char label = axes[axis_idx]; wxStaticText* axis_name = new wxStaticText(m_parent, wxID_ANY, wxString(label)); set_font_and_background_style(axis_name, wxGetApp().bold_font()); +// axis_name->SetForegroundColour(wxColour(axes_color[axis_idx])); sizer = new wxBoxSizer(wxHORIZONTAL); // Under OSX we use font, smaller than default font, so diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index d89ac1bcb..edb244b34 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -492,18 +492,23 @@ void Preview::show_hide_ui_elements(const std::string& what) m_choice_view_type->Show(visible); } -void Preview::reset_sliders() +void Preview::reset_sliders(bool reset_all) { m_enabled = false; // reset_double_slider(); - m_double_slider_sizer->Hide((size_t)0); + if (reset_all) + m_double_slider_sizer->Hide((size_t)0); + else + m_double_slider_sizer->GetItem(size_t(0))->GetSizer()->Hide(1); } void Preview::update_sliders(const std::vector<double>& layers_z, bool keep_z_range) { m_enabled = true; + update_double_slider(layers_z, keep_z_range); m_double_slider_sizer->Show((size_t)0); + Layout(); } @@ -560,12 +565,12 @@ void Preview::on_checkbox_legend(wxCommandEvent& evt) m_canvas_widget->Refresh(); } -void Preview::update_view_type() +void Preview::update_view_type(bool slice_completed) { const DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; - const wxString& choice = !config.option<ConfigOptionFloats>("colorprint_heights")->values.empty() && - wxGetApp().extruders_edited_cnt()==1 ? + const wxString& choice = !wxGetApp().plater()->model().custom_gcode_per_height.empty() /*&& + (wxGetApp().extruders_edited_cnt()==1 || !slice_completed) */? _(L("Color Print")) : config.option<ConfigOptionFloats>("wiping_volumes_matrix")->values.size() > 1 ? _(L("Tool")) : @@ -583,6 +588,8 @@ void Preview::update_view_type() void Preview::create_double_slider() { m_slider = new DoubleSlider(this, wxID_ANY, 0, 0, 0, 100); + m_slider->EnableTickManipulation(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF); + m_double_slider_sizer->Add(m_slider, 0, wxEXPAND, 0); // sizer, m_canvas_widget @@ -592,10 +599,11 @@ void Preview::create_double_slider() Bind(wxCUSTOMEVT_TICKSCHANGED, [this](wxEvent&) { - wxGetApp().preset_bundle->project_config.option<ConfigOptionFloats>("colorprint_heights")->values = m_slider->GetTicksValues(); + Model& model = wxGetApp().plater()->model(); + model.custom_gcode_per_height = m_slider->GetTicksValues(); m_schedule_background_process(); - update_view_type(); + update_view_type(false); reload_print(); }); @@ -628,6 +636,24 @@ static int find_close_layer_idx(const std::vector<double>& zs, double &z, double return -1; } +void Preview::check_slider_values(std::vector<Model::CustomGCode>& ticks_from_model, + const std::vector<double>& layers_z) +{ + // All ticks that would end up outside the slider range should be erased. + // TODO: this should be placed into more appropriate part of code, + // this function is e.g. not called when the last object is deleted + unsigned int old_size = ticks_from_model.size(); + ticks_from_model.erase(std::remove_if(ticks_from_model.begin(), ticks_from_model.end(), + [layers_z](Model::CustomGCode val) + { + auto it = std::lower_bound(layers_z.begin(), layers_z.end(), val.height - DoubleSlider::epsilon()); + return it == layers_z.end(); + }), + ticks_from_model.end()); + if (ticks_from_model.size() != old_size) + m_schedule_background_process(); +} + void Preview::update_double_slider(const std::vector<double>& layers_z, bool keep_z_range) { // Save the initial slider span. @@ -643,8 +669,8 @@ void Preview::update_double_slider(const std::vector<double>& layers_z, bool kee bool snap_to_min = force_sliders_full_range || m_slider->is_lower_at_min(); bool snap_to_max = force_sliders_full_range || m_slider->is_higher_at_max(); - std::vector<double> &ticks_from_config = (wxGetApp().preset_bundle->project_config.option<ConfigOptionFloats>("colorprint_heights"))->values; - check_slider_values(ticks_from_config, layers_z); + std::vector<Model::CustomGCode> &ticks_from_model = wxGetApp().plater()->model().custom_gcode_per_height; + check_slider_values(ticks_from_model, layers_z); m_slider->SetSliderValues(layers_z); assert(m_slider->GetMinValue() == 0); @@ -666,33 +692,12 @@ void Preview::update_double_slider(const std::vector<double>& layers_z, bool kee } m_slider->SetSelectionSpan(idx_low, idx_high); - m_slider->SetTicksValues(ticks_from_config); + m_slider->SetTicksValues(ticks_from_model); bool color_print_enable = (wxGetApp().plater()->printer_technology() == ptFFF); - if (color_print_enable) { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - if (cfg.opt<ConfigOptionFloats>("nozzle_diameter")->values.size() > 1) - color_print_enable = false; - } - m_slider->EnableTickManipulation(color_print_enable); -} -void Preview::check_slider_values(std::vector<double>& ticks_from_config, - const std::vector<double> &layers_z) -{ - // All ticks that would end up outside the slider range should be erased. - // TODO: this should be placed into more appropriate part of code, - // this function is e.g. not called when the last object is deleted - unsigned int old_size = ticks_from_config.size(); - ticks_from_config.erase(std::remove_if(ticks_from_config.begin(), ticks_from_config.end(), - [layers_z](double val) - { - auto it = std::lower_bound(layers_z.begin(), layers_z.end(), val - DoubleSlider::epsilon()); - return it == layers_z.end(); - }), - ticks_from_config.end()); - if (ticks_from_config.size() != old_size) - m_schedule_background_process(); + m_slider->EnableTickManipulation(color_print_enable); + m_slider->SetManipulationState(wxGetApp().extruders_edited_cnt()); } void Preview::reset_double_slider() @@ -753,7 +758,7 @@ void Preview::load_print_as_fff(bool keep_z_range) if (! has_layers) { - reset_sliders(); + reset_sliders(true); m_canvas->reset_legend_texture(); m_canvas_widget->Refresh(); return; @@ -776,51 +781,30 @@ void Preview::load_print_as_fff(bool keep_z_range) bool gcode_preview_data_valid = print->is_step_done(psGCodeExport) && ! m_gcode_preview_data->empty(); // Collect colors per extruder. std::vector<std::string> colors; - std::vector<double> color_print_values = {}; + std::vector<Model::CustomGCode> color_print_values = {}; // set color print values, if it si selected "ColorPrint" view type if (m_gcode_preview_data->extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint) { - colors = GCodePreviewData::ColorPrintColors(); - if (! gcode_preview_data_valid) { - //FIXME accessing full_config() is pretty expensive. - // Only initialize color_print_values for the initial preview, not for the full preview where the color_print_values is extracted from the G-code. - const auto& config = wxGetApp().preset_bundle->project_config; - color_print_values = config.option<ConfigOptionFloats>("colorprint_heights")->values; - } + colors = wxGetApp().plater()->get_colors_for_color_print(); + colors.push_back("#808080"); // gray color for pause print or custom G-code + + if (!gcode_preview_data_valid) + color_print_values = wxGetApp().plater()->model().custom_gcode_per_height; } else if (gcode_preview_data_valid || (m_gcode_preview_data->extrusion.view_type == GCodePreviewData::Extrusion::Tool) ) { - const ConfigOptionStrings* extruders_opt = dynamic_cast<const ConfigOptionStrings*>(m_config->option("extruder_colour")); - const ConfigOptionStrings* filamemts_opt = dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour")); - unsigned int colors_count = std::max((unsigned int)extruders_opt->values.size(), (unsigned int)filamemts_opt->values.size()); - - unsigned char rgb[3]; - for (unsigned int i = 0; i < colors_count; ++i) - { - std::string color = m_config->opt_string("extruder_colour", i); - if (!PresetBundle::parse_color(color, rgb)) - { - color = m_config->opt_string("filament_colour", i); - if (!PresetBundle::parse_color(color, rgb)) - color = "#FFFFFF"; - } - - colors.emplace_back(color); - } + colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(); color_print_values.clear(); } if (IsShown()) { + m_canvas->set_selected_extruder(0); if (gcode_preview_data_valid) { // Load the real G-code preview. m_canvas->load_gcode_preview(*m_gcode_preview_data, colors); m_loaded = true; } else { - // disable color change information for multi-material presets - if (wxGetApp().extruders_edited_cnt() > 1) - color_print_values.clear(); - // Load the initial preview based on slices, not the final G-code. m_canvas->load_preview(colors, color_print_values); } @@ -829,7 +813,7 @@ void Preview::load_print_as_fff(bool keep_z_range) std::vector<double> zs = m_canvas->get_current_print_zs(true); if (zs.empty()) { // all layers filtered out - reset_sliders(); + reset_sliders(true); m_canvas_widget->Refresh(); } else update_sliders(zs, keep_z_range); @@ -860,7 +844,7 @@ void Preview::load_print_as_sla() n_layers = (unsigned int)zs.size(); if (n_layers == 0) { - reset_sliders(); + reset_sliders(true); m_canvas_widget->Refresh(); } diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index 08d5991b4..b0dac4223 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -5,6 +5,7 @@ #include "libslic3r/Point.hpp" #include <string> +#include "libslic3r/Model.hpp" class wxNotebook; class wxGLCanvas; @@ -12,6 +13,7 @@ class wxBoxSizer; class wxStaticText; class wxChoice; class wxComboCtrl; +class wxBitmapComboBox; class wxCheckBox; class DoubleSlider; @@ -101,7 +103,7 @@ class Preview : public wxPanel bool m_loaded; bool m_enabled; - DoubleSlider* m_slider {nullptr}; + DoubleSlider* m_slider {nullptr}; public: Preview(wxWindow* parent, Bed3D& bed, Camera& camera, GLToolbar& view_toolbar, Model* model, DynamicPrintConfig* config, @@ -128,7 +130,7 @@ public: void move_double_slider(wxKeyEvent& evt); void edit_double_slider(wxKeyEvent& evt); - void update_view_type(); + void update_view_type(bool slice_completed); bool is_loaded() const { return m_loaded; } @@ -140,7 +142,7 @@ private: void show_hide_ui_elements(const std::string& what); - void reset_sliders(); + void reset_sliders(bool reset_all); void update_sliders(const std::vector<double>& layers_z, bool keep_z_range = false); void on_size(wxSizeEvent& evt); @@ -154,9 +156,9 @@ private: // Create/Update/Reset double slider on 3dPreview void create_double_slider(); + void check_slider_values(std::vector<Model::CustomGCode> &ticks_from_model, + const std::vector<double> &layers_z); void update_double_slider(const std::vector<double>& layers_z, bool keep_z_range = false); - void check_slider_values(std::vector<double> &ticks_from_config, - const std::vector<double> &layers_z); void reset_double_slider(); // update DoubleSlider after keyDown in canvas void update_double_slider_from_canvas(wxKeyEvent& event); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 76a9ed603..4406b1bf5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -140,17 +140,22 @@ void GLGizmoCut::on_render_input_window(float x, float y, float bottom_limit) y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->set_next_window_bg_alpha(0.5f); + m_imgui->begin(_(L("Cut")), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - m_imgui->begin(_(L("Cut")), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::AlignTextToFramePadding(); + m_imgui->text("Z"); + ImGui::SameLine(); + ImGui::PushItemWidth(m_imgui->get_style_scaling() * 150.0f); + ImGui::InputDouble("", &m_cut_z, 0.0f, 0.0f, "%.2f"); - ImGui::PushItemWidth(m_imgui->scaled(5.0f)); - ImGui::InputDouble("Z", &m_cut_z, 0.0f, 0.0f, "%.2f"); + ImGui::Separator(); m_imgui->checkbox(_(L("Keep upper part")), m_keep_upper); m_imgui->checkbox(_(L("Keep lower part")), m_keep_lower); m_imgui->checkbox(_(L("Rotate lower part upwards")), m_rotate_lower); + ImGui::Separator(); + m_imgui->disabled_begin(!m_keep_upper && !m_keep_lower); const bool cut_clicked = m_imgui->button(_(L("Perform cut"))); m_imgui->disabled_end(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 862ffe41a..925727e76 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -48,6 +48,13 @@ std::string GLGizmoMove3D::on_get_name() const return (_(L("Move")) + " [M]").ToUTF8().data(); } +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE +bool GLGizmoMove3D::on_is_activable() const +{ + return !m_parent.get_selection().is_empty(); +} +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + void GLGizmoMove3D::on_start_dragging() { if (m_hover_id != -1) @@ -166,25 +173,6 @@ void GLGizmoMove3D::on_render_for_picking() const render_grabber_extension(Z, box, true); } -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI -void GLGizmoMove3D::on_render_input_window(float x, float y, float bottom_limit) -{ - const Selection& selection = m_parent.get_selection(); - bool show_position = selection.is_single_full_instance(); - const Vec3d& position = selection.get_bounding_box().center(); - - Vec3d displacement = show_position ? position : m_displacement; - wxString label = show_position ? _(L("Position (mm)")) : _(L("Displacement (mm)")); - - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->set_next_window_bg_alpha(0.5f); - m_imgui->begin(label, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - m_imgui->input_vec3("", displacement, 100.0f, "%.2f"); - - m_imgui->end(); -} -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - double GLGizmoMove3D::calc_projection(const UpdateData& data) const { double projection = 0.0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index 21b1d397b..19958615e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -33,14 +33,14 @@ public: protected: virtual bool on_init(); virtual std::string on_get_name() const; +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + virtual bool on_is_activable() const; +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE virtual void on_start_dragging(); virtual void on_stop_dragging(); virtual void on_update(const UpdateData& data); virtual void on_render() const; virtual void on_render_for_picking() const; -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - virtual void on_render_input_window(float x, float y, float bottom_limit); -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI private: double calc_projection(const UpdateData& data) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 9a2c72633..5d3b6f365 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -449,6 +449,13 @@ std::string GLGizmoRotate3D::on_get_name() const return (_(L("Rotate")) + " [R]").ToUTF8().data(); } +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE +bool GLGizmoRotate3D::on_is_activable() const +{ + return !m_parent.get_selection().is_empty(); +} +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + void GLGizmoRotate3D::on_start_dragging() { if ((0 <= m_hover_id) && (m_hover_id < 3)) @@ -475,21 +482,5 @@ void GLGizmoRotate3D::on_render() const m_gizmos[Z].render(); } -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI -void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) -{ - Vec3d rotation(Geometry::rad2deg(m_gizmos[0].get_angle()), Geometry::rad2deg(m_gizmos[1].get_angle()), Geometry::rad2deg(m_gizmos[2].get_angle())); - wxString label = _(L("Rotation (deg)")); - - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->set_next_window_bg_alpha(0.5f); - m_imgui->begin(label, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - m_imgui->input_vec3("", rotation, 100.0f, "%.2f"); - m_imgui->end(); -} -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - - - } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index c856e5465..930a76e2e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -104,6 +104,9 @@ protected: if (id < 3) m_gizmos[id].disable_grabber(0); } +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + virtual bool on_is_activable() const; +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE virtual void on_start_dragging(); virtual void on_stop_dragging(); virtual void on_update(const UpdateData& data) @@ -121,13 +124,8 @@ protected: g.render_for_picking(); } } -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - virtual void on_render_input_window(float x, float y, float bottom_limit); -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI }; - - } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index a685fb774..fa49b06ce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -49,7 +49,12 @@ std::string GLGizmoScale3D::on_get_name() const bool GLGizmoScale3D::on_is_activable() const { +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + const Selection& selection = m_parent.get_selection(); + return !selection.is_empty() && !selection.is_wipe_tower(); +#else return !m_parent.get_selection().is_wipe_tower(); +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE } void GLGizmoScale3D::on_start_dragging() @@ -284,21 +289,6 @@ void GLGizmoScale3D::on_render_for_picking() const render_grabbers_for_picking(m_parent.get_selection().get_bounding_box()); } -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI -void GLGizmoScale3D::on_render_input_window(float x, float y, float bottom_limit) -{ - const Selection& selection = m_parent.get_selection(); - bool single_instance = selection.is_single_full_instance(); - wxString label = _(L("Scale (%)")); - - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->set_next_window_bg_alpha(0.5f); - m_imgui->begin(label, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - m_imgui->input_vec3("", m_scale * 100.f, 100.0f, "%.2f"); - m_imgui->end(); -} -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - void GLGizmoScale3D::render_grabbers_connection(unsigned int id_1, unsigned int id_2) const { unsigned int grabbers_count = (unsigned int)m_grabbers.size(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index 7b77fe559..d49770dce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -50,9 +50,6 @@ protected: virtual void on_update(const UpdateData& data); virtual void on_render() const; virtual void on_render_for_picking() const; -#if !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI - virtual void on_render_input_window(float x, float y, float bottom_limit); -#endif // !DISABLE_MOVE_ROTATE_SCALE_GIZMOS_IMGUI private: void render_grabbers_connection(unsigned int id_1, unsigned int id_2) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index cf3f91dac..b10f57108 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -741,7 +741,6 @@ RENDER_AGAIN: const float approx_height = m_imgui->scaled(18.0f); y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - m_imgui->set_next_window_bg_alpha(0.5f); m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: @@ -765,6 +764,7 @@ RENDER_AGAIN: float diameter_upper_cap = static_cast<ConfigOptionFloat*>(wxGetApp().preset_bundle->sla_prints.get_edited_preset().config.option("support_pillar_diameter"))->value; if (m_new_point_head_diameter > diameter_upper_cap) m_new_point_head_diameter = diameter_upper_cap; + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("head_diameter")); ImGui::SameLine(diameter_slider_left); ImGui::PushItemWidth(window_width - diameter_slider_left); @@ -825,6 +825,7 @@ RENDER_AGAIN: } } else { // not in editing mode: + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("minimal_distance")); ImGui::SameLine(settings_sliders_left); ImGui::PushItemWidth(window_width - settings_sliders_left); @@ -838,6 +839,7 @@ RENDER_AGAIN: bool slider_edited = ImGui::IsItemEdited(); // someone is dragging the slider bool slider_released = ImGui::IsItemDeactivatedAfterEdit(); // someone has just released the slider + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("points_density")); ImGui::SameLine(settings_sliders_left); @@ -868,7 +870,7 @@ RENDER_AGAIN: if (generate) auto_generate(); - m_imgui->text(""); + ImGui::Separator(); if (m_imgui->button(m_desc.at("manual_editing"))) switch_to_editing_mode(); @@ -885,9 +887,12 @@ RENDER_AGAIN: // Following is rendered in both editing and non-editing mode: - m_imgui->text(""); + ImGui::Separator(); if (m_clipping_plane_distance == 0.f) + { + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("clipping_of_view")); + } else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ @@ -1226,10 +1231,10 @@ void GLGizmoSlaSupports::get_data_from_backend() void GLGizmoSlaSupports::auto_generate() { - wxMessageDialog dlg(GUI::wxGetApp().plater(), _(L( - "Autogeneration will erase all manually edited points.\n\n" - "Are you sure you want to do it?\n" - )), _(L("Warning")), wxICON_WARNING | wxYES | wxNO); + wxMessageDialog dlg(GUI::wxGetApp().plater(), + _(L("Autogeneration will erase all manually edited points.")) + "\n\n" + + _(L("Are you sure you want to do it?")) + "\n", + _(L("Warning")), wxICON_WARNING | wxYES | wxNO); if (m_model_object->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Autogenerate support points"))); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 1ca764a03..89a313445 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -905,7 +905,11 @@ void GLGizmosManager::do_render_overlay() const GLGizmoBase* gizmo = m_gizmos[idx].get(); unsigned int sprite_id = gizmo->get_sprite_id(); +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + int icon_idx = (m_current == idx) ? 2 : ((m_hover == idx) ? 1 : (gizmo->is_activable()? 0 : 3)); +#else int icon_idx = m_current == idx ? 2 : (m_hover == idx ? 1 : 0); +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE float u_icon_size = m_overlay_icons_size * m_overlay_scale * inv_tex_width; float v_icon_size = m_overlay_icons_size * m_overlay_scale * inv_tex_height; @@ -971,11 +975,19 @@ bool GLGizmosManager::generate_icons_texture() const } std::vector<std::pair<int, bool>> states; - states.push_back(std::make_pair(1, false)); - states.push_back(std::make_pair(0, false)); - states.push_back(std::make_pair(0, true)); - - bool res = m_icons_texture.load_from_svg_files_as_sprites_array(filenames, states, (unsigned int)(m_overlay_icons_size * m_overlay_scale), true); + states.push_back(std::make_pair(1, false)); // Activable + states.push_back(std::make_pair(0, false)); // Hovered + states.push_back(std::make_pair(0, true)); // Selected +#if ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + states.push_back(std::make_pair(2, false)); // Disabled +#endif // ENABLE_GIZMO_ICONS_NON_ACTIVABLE_STATE + + unsigned int sprite_size_px = (unsigned int)(m_overlay_icons_size * m_overlay_scale); +// // force even size +// if (sprite_size_px % 2 != 0) +// sprite_size_px += 1; + + bool res = m_icons_texture.load_from_svg_files_as_sprites_array(filenames, states, sprite_size_px, false); if (res) m_icons_texture_dirty = false; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 8e4d9eebf..33e526083 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -96,7 +96,7 @@ void ImGuiWrapper::set_language(const std::string &language) ranges = ranges_turkish; } else if (lang == "vi") { ranges = ranges_vietnamese; - } else if (lang == "jp") { + } else if (lang == "ja") { ranges = ImGui::GetIO().Fonts->GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs m_font_cjk = true; } else if (lang == "ko") { @@ -317,6 +317,22 @@ void ImGuiWrapper::text(const wxString &label) this->text(label_utf8.c_str()); } +bool ImGuiWrapper::slider_float(const char* label, float* v, float v_min, float v_max, const char* format/* = "%.3f"*/, float power/* = 1.0f*/) +{ + return ImGui::SliderFloat(label, v, v_min, v_max, format, power); +} + +bool ImGuiWrapper::slider_float(const std::string& label, float* v, float v_min, float v_max, const char* format/* = "%.3f"*/, float power/* = 1.0f*/) +{ + return this->slider_float(label.c_str(), v, v_min, v_max, format, power); +} + +bool ImGuiWrapper::slider_float(const wxString& label, float* v, float v_min, float v_max, const char* format/* = "%.3f"*/, float power/* = 1.0f*/) +{ + auto label_utf8 = into_u8(label); + return this->slider_float(label_utf8.c_str(), v, v_min, v_max, format, power); +} + bool ImGuiWrapper::combo(const wxString& label, const std::vector<std::string>& options, int& selection) { // this is to force the label to the left of the widget: @@ -498,13 +514,18 @@ void ImGuiWrapper::init_style() (hex_color & 0xff) / 255.0f); }; - static const unsigned COL_GREY_DARK = 0x444444ff; + static const unsigned COL_WINDOW_BACKGROND = 0x222222cc; + static const unsigned COL_GREY_DARK = 0x555555ff; static const unsigned COL_GREY_LIGHT = 0x666666ff; static const unsigned COL_ORANGE_DARK = 0xc16737ff; static const unsigned COL_ORANGE_LIGHT = 0xff7d38ff; - // Generics + // Window + style.WindowRounding = 4.0f; + set_color(ImGuiCol_WindowBg, COL_WINDOW_BACKGROND); set_color(ImGuiCol_TitleBgActive, COL_ORANGE_DARK); + + // Generics set_color(ImGuiCol_FrameBg, COL_GREY_DARK); set_color(ImGuiCol_FrameBgHovered, COL_GREY_LIGHT); set_color(ImGuiCol_FrameBgActive, COL_GREY_LIGHT); @@ -528,6 +549,9 @@ void ImGuiWrapper::init_style() // Slider set_color(ImGuiCol_SliderGrab, COL_ORANGE_DARK); set_color(ImGuiCol_SliderGrabActive, COL_ORANGE_LIGHT); + + // Separator + set_color(ImGuiCol_Separator, COL_ORANGE_LIGHT); } void ImGuiWrapper::render_draw_data(ImDrawData *draw_data) diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index c6550351e..7cce60367 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -66,6 +66,9 @@ public: void text(const char *label); void text(const std::string &label); void text(const wxString &label); + bool slider_float(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); + bool slider_float(const std::string& label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); + bool slider_float(const wxString& label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); bool combo(const wxString& label, const std::vector<std::string>& options, int& selection); // Use -1 to not mark any option as selected bool undo_redo_list(const ImVec2& size, const bool is_undo, bool (*items_getter)(const bool, int, const char**), int& hovered, int& selected); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index e3ef335e6..268682b81 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -157,6 +157,7 @@ void KBShortcutsDialog::fill_shortcuts() plater_shortcuts.push_back(Shortcut("Z", L("Zoom to selected object"))); plater_shortcuts.push_back(Shortcut("I", L("Zoom in"))); plater_shortcuts.push_back(Shortcut("O", L("Zoom out"))); + plater_shortcuts.push_back(Shortcut(ctrl+"M", L("Show/Hide 3Dconnexion devices settings dialog"))); plater_shortcuts.push_back(Shortcut("ESC", L("Unselect gizmo / Clear selection"))); #if ENABLE_RENDER_PICKING_PASS // Don't localize debugging texts. diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index b76110a87..e613e6915 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -24,6 +24,7 @@ #include "PrintHostDialogs.hpp" #include "wxExtensions.hpp" #include "GUI_ObjectList.hpp" +#include "Mouse3DController.hpp" #include "I18N.hpp" #include <fstream> @@ -684,7 +685,7 @@ void MainFrame::init_menubar() [this](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); }); #if ENABLE_THUMBNAIL_GENERATOR_DEBUG helpMenu->AppendSeparator(); - append_menu_item(helpMenu, wxID_ANY, _(L("DEBUG gcode thumbnails")), _(L("DEBUG ONLY - read the selected gcode file and generates png for the contained thumbnails")), + append_menu_item(helpMenu, wxID_ANY, "DEBUG gcode thumbnails", "DEBUG ONLY - read the selected gcode file and generates png for the contained thumbnails", [this](wxCommandEvent&) { wxGetApp().gcode_thumbnails_debug(); }); #endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG } diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 8b37161e9..573952f75 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -95,6 +95,7 @@ void MeshClipper::recalculate_triangles() } + bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane) const { diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 5900cb820..56d726596 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -18,7 +18,7 @@ namespace GUI { struct Camera; - +// lm_FIXME: Following class might possibly be replaced by Eigen::Hyperplane class ClippingPlane { double m_data[4]; @@ -68,13 +68,23 @@ public: }; - +// MeshClipper class cuts a mesh and is able to return a triangulated cut. class MeshClipper { public: + // Inform MeshClipper about which plane we want to use to cut the mesh + // This is supposed to be in world coordinates. void set_plane(const ClippingPlane& plane); + + // Which mesh to cut. MeshClipper remembers const * to it, caller + // must make sure that it stays valid. void set_mesh(const TriangleMesh& mesh); + + // Inform the MeshClipper about the transformation that transforms the mesh + // into world coordinates. void set_transformation(const Geometry::Transformation& trafo); + // Return the triangulated cut. The points are returned directly + // in world coordinates. const std::vector<Vec3f>& get_triangles(); private: @@ -91,21 +101,39 @@ private: - +// MeshRaycaster class answers queries such as where on the mesh someone clicked, +// whether certain points are visible or obscured by the mesh etc. class MeshRaycaster { public: + // The class saves a const* to the mesh, calledz is responsible + // for making sure it does not get invalid. MeshRaycaster(const TriangleMesh& mesh) - : m_mesh(&mesh), m_emesh(mesh) + : m_emesh(mesh), m_mesh(&mesh) {} - void set_transformation(const Geometry::Transformation& trafo); - void set_camera(const Camera& camera); - - bool unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, - Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane = nullptr) const; - - std::vector<unsigned> get_unobscured_idxs(const Geometry::Transformation& trafo, const Camera& camera, - const std::vector<Vec3f>& points, const ClippingPlane* clipping_plane = nullptr) const; + // Given a mouse position, this returns true in case it is on the mesh. + bool unproject_on_mesh( + const Vec2d& mouse_pos, + const Transform3d& trafo, // how to get the mesh into world coords + const Camera& camera, // current camera position + Vec3f& position, // where to save the positibon of the hit (mesh coords) + Vec3f& normal, // normal of the triangle that was hit + const ClippingPlane* clipping_plane = nullptr // clipping plane (if active) + ) const; + + // Given a vector of points in woorld coordinates, this returns vector + // of indices of points that are visible (i.e. not cut by clipping plane + // or obscured by part of the mesh. + std::vector<unsigned> get_unobscured_idxs( + const Geometry::Transformation& trafo, // how to get the mesh into world coords + const Camera& camera, // current camera position + const std::vector<Vec3f>& points, // points in world coords + const ClippingPlane* clipping_plane = nullptr // clipping plane (if active) + ) const; + + // Given a point in world coords, the method returns closest point on the mesh. + // The output is in mesh coords. + // normal* can be used to also get normal of the respective triangle. Vec3f get_closest_point(const Vec3f& point, Vec3f* normal = nullptr) const; private: diff --git a/src/slic3r/GUI/Mouse3DController.cpp b/src/slic3r/GUI/Mouse3DController.cpp new file mode 100644 index 000000000..b3d7c0e4f --- /dev/null +++ b/src/slic3r/GUI/Mouse3DController.cpp @@ -0,0 +1,816 @@ +#include "libslic3r/libslic3r.h" +#include "Mouse3DController.hpp" + +#include "Camera.hpp" +#include "GUI_App.hpp" +#include "PresetBundle.hpp" +#include "AppConfig.hpp" + +#include <wx/glcanvas.h> + +#include <boost/nowide/convert.hpp> +#include <boost/log/trivial.hpp> +#include "I18N.hpp" + +#include <bitset> + +// WARN: If updating these lists, please also update resources/udev/90-3dconnexion.rules + +static const std::vector<int> _3DCONNEXION_VENDORS = +{ + 0x046d, // LOGITECH = 1133 // Logitech (3Dconnexion is made by Logitech) + 0x256F // 3DCONNECTION = 9583 // 3Dconnexion +}; + +// See: https://github.com/FreeSpacenav/spacenavd/blob/a9eccf34e7cac969ee399f625aef827f4f4aaec6/src/dev.c#L202 +static const std::vector<int> _3DCONNEXION_DEVICES = +{ + 0xc603, /* 50691 spacemouse plus XT */ + 0xc605, /* 50693 cadman */ + 0xc606, /* 50694 spacemouse classic */ + 0xc621, /* 50721 spaceball 5000 */ + 0xc623, /* 50723 space traveller */ + 0xc625, /* 50725 space pilot */ + 0xc626, /* 50726 space navigator *TESTED* */ + 0xc627, /* 50727 space explorer */ + 0xc628, /* 50728 space navigator for notebooks*/ + 0xc629, /* 50729 space pilot pro*/ + 0xc62b, /* 50731 space mouse pro*/ + 0xc62e, /* 50734 spacemouse wireless (USB cable) *TESTED* */ + 0xc62f, /* 50735 spacemouse wireless receiver */ + 0xc631, /* 50737 spacemouse pro wireless *TESTED* */ + 0xc632, /* 50738 spacemouse pro wireless receiver */ + 0xc633, /* 50739 spacemouse enterprise */ + 0xc635, /* 50741 spacemouse compact *TESTED* */ + 0xc636, /* 50742 spacemouse module */ + 0xc640, /* 50752 nulooq */ + 0xc652, /* 50770 3Dconnexion universal receiver *TESTED* */ +}; + +namespace Slic3r { +namespace GUI { + +const double Mouse3DController::State::DefaultTranslationScale = 2.5; +const double Mouse3DController::State::MaxTranslationDeadzone = 0.2; +const double Mouse3DController::State::DefaultTranslationDeadzone = 0.5 * Mouse3DController::State::MaxTranslationDeadzone; +const float Mouse3DController::State::DefaultRotationScale = 1.0f; +const float Mouse3DController::State::MaxRotationDeadzone = (float)Mouse3DController::State::MaxTranslationDeadzone; +const float Mouse3DController::State::DefaultRotationDeadzone = 0.5f * Mouse3DController::State::MaxRotationDeadzone; + +Mouse3DController::State::State() + : m_buttons_enabled(false) + , m_translation_params(DefaultTranslationScale, DefaultTranslationDeadzone) + , m_rotation_params(DefaultRotationScale, DefaultRotationDeadzone) + , m_mouse_wheel_counter(0) +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + , m_translation_queue_max_size(0) + , m_rotation_queue_max_size(0) + , m_buttons_queue_max_size(0) +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT +{ +} + +void Mouse3DController::State::append_translation(const Vec3d& translation) +{ + while (m_translation.queue.size() >= m_translation.max_size) + { + m_translation.queue.pop(); + } + m_translation.queue.push(translation); +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_translation_queue_max_size = std::max(m_translation_queue_max_size, m_translation.queue.size()); +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT +} + +void Mouse3DController::State::append_rotation(const Vec3f& rotation) +{ + while (m_rotation.queue.size() >= m_rotation.max_size) + { + m_rotation.queue.pop(); + } + m_rotation.queue.push(rotation); +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_rotation_queue_max_size = std::max(m_rotation_queue_max_size, m_rotation.queue.size()); +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + if (rotation(0) != 0.0f) + ++m_mouse_wheel_counter; +} + +void Mouse3DController::State::append_button(unsigned int id) +{ + if (!m_buttons_enabled) + return; + + m_buttons.push(id); +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_buttons_queue_max_size = std::max(m_buttons_queue_max_size, m_buttons.size()); +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT +} + +bool Mouse3DController::State::process_mouse_wheel() +{ + if (m_mouse_wheel_counter == 0) + return false; + else if (!m_rotation.queue.empty()) + { + --m_mouse_wheel_counter; + return true; + } + + m_mouse_wheel_counter = 0; + return true; +} + +void Mouse3DController::State::set_queues_max_size(size_t size) +{ + if (size > 0) + { + m_translation.max_size = size; + m_rotation.max_size = size; + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_translation_queue_max_size = 0; + m_rotation_queue_max_size = 0; + m_buttons_queue_max_size = 0; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + } +} + +bool Mouse3DController::State::apply(Camera& camera) +{ + if (!wxGetApp().IsActive()) + return false; + + bool ret = false; + + if (has_translation()) + { + const Vec3d& translation = m_translation.queue.front(); + camera.set_target(camera.get_target() + m_translation_params.scale * (translation(0) * camera.get_dir_right() + translation(1) * camera.get_dir_forward() + translation(2) * camera.get_dir_up())); + m_translation.queue.pop(); + ret = true; + } + + if (has_rotation()) + { + const Vec3f& rotation = m_rotation.queue.front(); + float theta = m_rotation_params.scale * rotation(0); + float phi = m_rotation_params.scale * rotation(2); + float sign = camera.inverted_phi ? -1.0f : 1.0f; + camera.phi += sign * phi; + camera.set_theta(camera.get_theta() + theta, wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA); + m_rotation.queue.pop(); + ret = true; + } + + if (m_buttons_enabled && has_button()) + { + unsigned int button = m_buttons.front(); + switch (button) + { + case 0: { camera.update_zoom(1.0); break; } + case 1: { camera.update_zoom(-1.0); break; } + default: { break; } + } + m_buttons.pop(); + ret = true; + } + + return ret; +} + +Mouse3DController::Mouse3DController() + : m_initialized(false) + , m_device(nullptr) + , m_device_str("") + , m_running(false) + , m_settings_dialog(false) +{ + m_last_time = std::chrono::high_resolution_clock::now(); +} + +void Mouse3DController::init() +{ + if (m_initialized) + return; + + // Initialize the hidapi library + int res = hid_init(); + if (res != 0) + { + BOOST_LOG_TRIVIAL(error) << "Unable to initialize hidapi library"; + return; + } + + m_initialized = true; +} + +void Mouse3DController::shutdown() +{ + if (!m_initialized) + return; + + stop(); + disconnect_device(); + + // Finalize the hidapi library + hid_exit(); + m_initialized = false; +} + +bool Mouse3DController::apply(Camera& camera) +{ + if (!m_initialized) + return false; + + std::lock_guard<std::mutex> lock(m_mutex); + + // check if the user unplugged the device + if (!m_running && is_device_connected()) + { + disconnect_device(); + // hides the settings dialog if the user re-plug the device + m_settings_dialog = false; + } + + // check if the user plugged the device + if (connect_device()) + start(); + + return is_device_connected() ? m_state.apply(camera) : false; +} + +void Mouse3DController::render_settings_dialog(unsigned int canvas_width, unsigned int canvas_height) const +{ + if (!m_running || !m_settings_dialog) + return; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + + imgui.set_next_window_pos(0.5f * (float)canvas_width, 0.5f * (float)canvas_height, ImGuiCond_Always, 0.5f, 0.5f); + + imgui.begin(_(L("3Dconnexion settings")), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + + const ImVec4& color = ImGui::GetStyleColorVec4(ImGuiCol_Separator); + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text(_(L("Device:"))); + ImGui::PopStyleColor(); + ImGui::SameLine(); + imgui.text(m_device_str); + + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text(_(L("Speed:"))); + ImGui::PopStyleColor(); + + float translation_scale = (float)m_state.get_translation_scale() / State::DefaultTranslationScale; + if (imgui.slider_float(_(L("Translation")) + "##1", &translation_scale, 0.5f, 2.0f, "%.1f")) + m_state.set_translation_scale(State::DefaultTranslationScale * (double)translation_scale); + + float rotation_scale = m_state.get_rotation_scale() / State::DefaultRotationScale; + if (imgui.slider_float(_(L("Rotation")) + "##1", &rotation_scale, 0.5f, 2.0f, "%.1f")) + m_state.set_rotation_scale(State::DefaultRotationScale * rotation_scale); + + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text(_(L("Deadzone:"))); + ImGui::PopStyleColor(); + + float translation_deadzone = (float)m_state.get_translation_deadzone(); + if (imgui.slider_float(_(L("Translation")) + "##2", &translation_deadzone, 0.0f, (float)State::MaxTranslationDeadzone, "%.2f")) + m_state.set_translation_deadzone((double)translation_deadzone); + + float rotation_deadzone = m_state.get_rotation_deadzone(); + if (imgui.slider_float(_(L("Rotation")) + "##2", &rotation_deadzone, 0.0f, State::MaxRotationDeadzone, "%.2f")) + m_state.set_rotation_deadzone(rotation_deadzone); + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + ImGui::Separator(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text("DEBUG:"); + imgui.text("Vectors:"); + ImGui::PopStyleColor(); + Vec3f translation = m_state.get_translation().cast<float>(); + Vec3f rotation = m_state.get_rotation(); + ImGui::InputFloat3("Translation##3", translation.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); + ImGui::InputFloat3("Rotation##3", rotation.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); + + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text("Queue size:"); + ImGui::PopStyleColor(); + + int translation_size[2] = { (int)m_state.get_translation_queue_size(), (int)m_state.get_translation_queue_max_size() }; + int rotation_size[2] = { (int)m_state.get_rotation_queue_size(), (int)m_state.get_rotation_queue_max_size() }; + int buttons_size[2] = { (int)m_state.get_buttons_queue_size(), (int)m_state.get_buttons_queue_max_size() }; + + ImGui::InputInt2("Translation##4", translation_size, ImGuiInputTextFlags_ReadOnly); + ImGui::InputInt2("Rotation##4", rotation_size, ImGuiInputTextFlags_ReadOnly); + ImGui::InputInt2("Buttons", buttons_size, ImGuiInputTextFlags_ReadOnly); + + int queue_size = (int)m_state.get_queues_max_size(); + if (ImGui::InputInt("Max size", &queue_size, 1, 1, ImGuiInputTextFlags_ReadOnly)) + { + if (queue_size > 0) + m_state.set_queues_max_size(queue_size); + } + + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + imgui.text("Camera:"); + ImGui::PopStyleColor(); + Vec3f target = wxGetApp().plater()->get_camera().get_target().cast<float>(); + ImGui::InputFloat3("Target", target.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + imgui.end(); +} + +bool Mouse3DController::connect_device() +{ + static const long long DETECTION_TIME_MS = 2000; // seconds + + if (is_device_connected()) + return false; + + // check time since last detection took place + if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - m_last_time).count() < DETECTION_TIME_MS) + return false; + + m_last_time = std::chrono::high_resolution_clock::now(); + + // Enumerates devices + hid_device_info* devices = hid_enumerate(0, 0); + if (devices == nullptr) + { + BOOST_LOG_TRIVIAL(error) << "Unable to enumerate HID devices"; + return false; + } + + // Searches for 1st connected 3Dconnexion device + struct DeviceData + { + std::string path; + unsigned short usage_page; + unsigned short usage; + + DeviceData() + : path(""), usage_page(0), usage(0) + {} + DeviceData(const std::string& path, unsigned short usage_page, unsigned short usage) + : path(path), usage_page(usage_page), usage(usage) + {} + + bool has_valid_usage() const { return (usage_page == 1) && (usage == 8); } + }; + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + hid_device_info* cur = devices; + std::cout << std::endl << "======================================================================================================================================" << std::endl; + std::cout << "Detected devices:" << std::endl; + while (cur != nullptr) + { + std::cout << "\""; + std::wcout << ((cur->manufacturer_string != nullptr) ? cur->manufacturer_string : L"Unknown"); + std::cout << "/"; + std::wcout << ((cur->product_string != nullptr) ? cur->product_string : L"Unknown"); + std::cout << "\" code: " << cur->vendor_id << "/" << cur->product_id << " (" << std::hex << cur->vendor_id << "/" << cur->product_id << std::dec << ")"; + std::cout << " serial number: '"; + std::wcout << ((cur->serial_number != nullptr) ? cur->serial_number : L"Unknown"); + std::cout << "' usage page: " << cur->usage_page << " usage: " << cur->usage << " interface number: " << cur->interface_number << std::endl; + + cur = cur->next; + } +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + // When using 3Dconnexion universal receiver, multiple devices are detected sharing the same vendor_id and product_id. + // To choose from them the right one we use: + // On Windows and Mac: usage_page == 1 and usage == 8 + // On Linux: as usage_page and usage are not defined (see hidapi.h) we try all detected devices until one is succesfully open + // When only a single device is detected, as for wired connections, vendor_id and product_id are enough + + // First we count all the valid devices from the enumerated list, + + hid_device_info* current = devices; + typedef std::pair<unsigned short, unsigned short> DeviceIds; + typedef std::vector<DeviceData> DeviceDataList; + typedef std::map<DeviceIds, DeviceDataList> DetectedDevices; + DetectedDevices detected_devices; +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << std::endl << "Detected 3D connexion devices:" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + while (current != nullptr) + { + unsigned short vendor_id = 0; + unsigned short product_id = 0; + + for (size_t i = 0; i < _3DCONNEXION_VENDORS.size(); ++i) + { + if (_3DCONNEXION_VENDORS[i] == current->vendor_id) + { + vendor_id = current->vendor_id; + break; + } + } + + if (vendor_id != 0) + { + for (size_t i = 0; i < _3DCONNEXION_DEVICES.size(); ++i) + { + if (_3DCONNEXION_DEVICES[i] == current->product_id) + { + product_id = current->product_id; + DeviceIds detected_device(vendor_id, product_id); + DetectedDevices::iterator it = detected_devices.find(detected_device); + if (it == detected_devices.end()) + it = detected_devices.insert(DetectedDevices::value_type(detected_device, DeviceDataList())).first; + + it->second.emplace_back(current->path, current->usage_page, current->usage); + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::wcout << "\"" << ((current->manufacturer_string != nullptr) ? current->manufacturer_string : L"Unknown"); + std::cout << "/"; + std::wcout << ((current->product_string != nullptr) ? current->product_string : L"Unknown"); + std::cout << "\" code: " << current->vendor_id << "/" << current->product_id << " (" << std::hex << current->vendor_id << "/" << current->product_id << std::dec << ")"; + std::cout << " serial number: '"; + std::wcout << ((current->serial_number != nullptr) ? current->serial_number : L"Unknown"); + std::cout << "' usage page: " << current->usage_page << " usage: " << current->usage << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + } + } + } + + current = current->next; + } + + // Free enumerated devices + hid_free_enumeration(devices); + + if (detected_devices.empty()) + return false; + + std::string path = ""; + unsigned short vendor_id = 0; + unsigned short product_id = 0; + + // Then we'll decide the choosing logic to apply in dependence of the device count and operating system + + for (const DetectedDevices::value_type& device : detected_devices) + { + if (device.second.size() == 1) + { +#ifdef __linux__ + hid_device* test_device = hid_open(device.first.first, device.first.second, nullptr); + if (test_device != nullptr) + { + hid_close(test_device); +#else + if (device.second.front().has_valid_usage()) + { +#endif // __linux__ + vendor_id = device.first.first; + product_id = device.first.second; + break; + } + } + else + { + bool found = false; +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + for (const DeviceData& data : device.second) + { +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << "Test device: " << std::hex << device.first.first << std::dec << "/" << std::hex << device.first.second << std::dec << " \"" << data.path << "\""; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT +#ifdef __linux__ + hid_device* test_device = hid_open_path(data.path.c_str()); + if (test_device != nullptr) + { + path = data.path; + vendor_id = device.first.first; + product_id = device.first.second; + found = true; +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << "-> PASSED" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + hid_close(test_device); + break; + } +#else + if (data.has_valid_usage()) + { + path = data.path; + vendor_id = device.first.first; + product_id = device.first.second; + found = true; +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << "-> PASSED" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + break; + } +#endif // __linux__ +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + else + std::cout << "-> NOT PASSED" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + } + + if (found) + break; + } + } + + if (path.empty()) + { + if ((vendor_id != 0) && (product_id != 0)) + { + // Open the 3Dconnexion device using vendor_id and product_id +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << std::endl << "Opening device: " << std::hex << vendor_id << std::dec << "/" << std::hex << product_id << std::dec << " using hid_open()" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_device = hid_open(vendor_id, product_id, nullptr); + } + else + return false; + } + else + { + // Open the 3Dconnexion device using the device path +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << std::endl << "Opening device: " << std::hex << vendor_id << std::dec << "/" << std::hex << product_id << std::dec << "\"" << path << "\" using hid_open_path()" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + m_device = hid_open_path(path.c_str()); + } + + if (m_device != nullptr) + { + std::vector<wchar_t> manufacturer(1024, 0); + hid_get_manufacturer_string(m_device, manufacturer.data(), 1024); + m_device_str = boost::nowide::narrow(manufacturer.data()); + + std::vector<wchar_t> product(1024, 0); + hid_get_product_string(m_device, product.data(), 1024); + m_device_str += "/" + boost::nowide::narrow(product.data()); + + BOOST_LOG_TRIVIAL(info) << "Connected device: " << m_device_str; + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << std::endl << "Connected device:" << std::endl; + std::cout << "Manufacturer/product: " << m_device_str << std::endl; + std::cout << "Manufacturer id.....: " << vendor_id << " (" << std::hex << vendor_id << std::dec << ")" << std::endl; + std::cout << "Product id..........: " << product_id << " (" << std::hex << product_id << std::dec << ")" << std::endl; + std::cout << "Path................: '" << path << "'" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + // get device parameters from the config, if present + double translation_speed = 1.0; + float rotation_speed = 1.0; + double translation_deadzone = State::DefaultTranslationDeadzone; + float rotation_deadzone = State::DefaultRotationDeadzone; + wxGetApp().app_config->get_mouse_device_translation_speed(m_device_str, translation_speed); + wxGetApp().app_config->get_mouse_device_translation_deadzone(m_device_str, translation_deadzone); + wxGetApp().app_config->get_mouse_device_rotation_speed(m_device_str, rotation_speed); + wxGetApp().app_config->get_mouse_device_rotation_deadzone(m_device_str, rotation_deadzone); + // clamp to valid values + m_state.set_translation_scale(State::DefaultTranslationScale * std::max(0.5, std::min(2.0, translation_speed))); + m_state.set_translation_deadzone(std::max(0.0, std::min(State::MaxTranslationDeadzone, translation_deadzone))); + m_state.set_rotation_scale(State::DefaultRotationScale * std::max(0.5f, std::min(2.0f, rotation_speed))); + m_state.set_rotation_deadzone(std::max(0.0f, std::min(State::MaxRotationDeadzone, rotation_deadzone))); + } +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + else + { + std::cout << std::endl << "Unable to connect to device:" << std::endl; + std::cout << "Manufacturer/product: " << m_device_str << std::endl; + std::cout << "Manufacturer id.....: " << vendor_id << " (" << std::hex << vendor_id << std::dec << ")" << std::endl; + std::cout << "Product id..........: " << product_id << " (" << std::hex << product_id << std::dec << ")" << std::endl; + std::cout << "Path................: '" << path << "'" << std::endl; + } +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + return (m_device != nullptr); +} + +void Mouse3DController::disconnect_device() +{ + if (!is_device_connected()) + return; + + // Stop the secondary thread, if running + if (m_thread.joinable()) + m_thread.join(); + + // Store current device parameters into the config + wxGetApp().app_config->set_mouse_device(m_device_str, m_state.get_translation_scale() / State::DefaultTranslationScale, m_state.get_translation_deadzone(), + m_state.get_rotation_scale() / State::DefaultRotationScale, m_state.get_rotation_deadzone()); + wxGetApp().app_config->save(); + + // Close the 3Dconnexion device + hid_close(m_device); + m_device = nullptr; + + BOOST_LOG_TRIVIAL(info) << "Disconnected device: " << m_device_str; + + m_device_str = ""; +} + +void Mouse3DController::start() +{ + if (!is_device_connected() || m_running) + return; + + m_thread = std::thread(&Mouse3DController::run, this); +} + +void Mouse3DController::run() +{ + m_running = true; + while (m_running) + { + collect_input(); + } +} + +void Mouse3DController::collect_input() +{ + DataPacket packet = { 0 }; + int res = hid_read_timeout(m_device, packet.data(), packet.size(), 100); + if (res < 0) + { + // An error occourred (device detached from pc ?) + stop(); + return; + } + + if (!wxGetApp().IsActive()) + return; + + std::lock_guard<std::mutex> lock(m_mutex); + + bool updated = false; + + if (res == 7) + updated = handle_packet(packet); + else if (res == 13) + updated = handle_wireless_packet(packet); + else if ((res == 3) && (packet[0] == 3)) + // On Mac button packets can be 3 bytes long + updated = handle_packet(packet); +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + else if (res > 0) + std::cout << "Got unknown data packet of length: " << res << ", code:" << (int)packet[0] << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + if (updated) + // ask for an idle event to update 3D scene + wxWakeUpIdle(); +} + +bool Mouse3DController::handle_packet(const DataPacket& packet) +{ + switch (packet[0]) + { + case 1: // Translation + { + if (handle_packet_translation(packet)) + return true; + + break; + } + case 2: // Rotation + { + if (handle_packet_rotation(packet, 1)) + return true; + + break; + } + case 3: // Button + { + if (handle_packet_button(packet, packet.size() - 1)) + return true; + + break; + } + case 23: // Battery charge + { +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << m_device_str << " - battery level: " << (int)packet[1] << " percent" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + break; + } + default: + { +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << "Got unknown data packet of code: " << (int)packet[0] << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + break; + } + } + + return false; +} + +bool Mouse3DController::handle_wireless_packet(const DataPacket& packet) +{ + switch (packet[0]) + { + case 1: // Translation + Rotation + { + bool updated = handle_packet_translation(packet); + updated |= handle_packet_rotation(packet, 7); + + if (updated) + return true; + + break; + } + case 3: // Button + { + if (handle_packet_button(packet, 12)) + return true; + + break; + } + case 23: // Battery charge + { +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << m_device_str << " - battery level: " << (int)packet[1] << " percent" << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + break; + } + default: + { +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + std::cout << "Got unknown data packet of code: " << (int)packet[0] << std::endl; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + break; + } + } + + return false; +} + +double convert_input(unsigned char first, unsigned char second, double deadzone) +{ + short value = first | second << 8; + double ret = (double)value / 350.0; + return (std::abs(ret) > deadzone) ? ret : 0.0; +} + +bool Mouse3DController::handle_packet_translation(const DataPacket& packet) +{ + double deadzone = m_state.get_translation_deadzone(); + Vec3d translation(-convert_input(packet[1], packet[2], deadzone), + convert_input(packet[3], packet[4], deadzone), + convert_input(packet[5], packet[6], deadzone)); + + if (!translation.isApprox(Vec3d::Zero())) + { + m_state.append_translation(translation); + return true; + } + + return false; +} + +bool Mouse3DController::handle_packet_rotation(const DataPacket& packet, unsigned int first_byte) +{ + double deadzone = (double)m_state.get_rotation_deadzone(); + Vec3f rotation(-(float)convert_input(packet[first_byte + 0], packet[first_byte + 1], deadzone), + (float)convert_input(packet[first_byte + 2], packet[first_byte + 3], deadzone), + -(float)convert_input(packet[first_byte + 4], packet[first_byte + 5], deadzone)); + + if (!rotation.isApprox(Vec3f::Zero())) + { + m_state.append_rotation(rotation); + return true; + } + + return false; +} + +bool Mouse3DController::handle_packet_button(const DataPacket& packet, unsigned int packet_size) +{ + unsigned int data = 0; + for (unsigned int i = 1; i < packet_size; ++i) + { + data |= packet[i] << 8 * (i - 1); + } + + const std::bitset<32> data_bits{ data }; + for (size_t i = 0; i < data_bits.size(); ++i) + { + if (data_bits.test(i)) + { + m_state.append_button((unsigned int)i); + return true; + } + } + + return false; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Mouse3DController.hpp b/src/slic3r/GUI/Mouse3DController.hpp new file mode 100644 index 000000000..cc03d4a24 --- /dev/null +++ b/src/slic3r/GUI/Mouse3DController.hpp @@ -0,0 +1,175 @@ +#ifndef slic3r_Mouse3DController_hpp_ +#define slic3r_Mouse3DController_hpp_ + +// Enabled debug output to console and extended imgui dialog +#define ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT 0 + +#include "libslic3r/Point.hpp" + +#include "hidapi.h" + +#include <queue> +#include <thread> +#include <mutex> +#include <vector> +#include <chrono> + +namespace Slic3r { +namespace GUI { + +struct Camera; + +class Mouse3DController +{ + class State + { + public: + static const double DefaultTranslationScale; + static const double MaxTranslationDeadzone; + static const double DefaultTranslationDeadzone; + static const float DefaultRotationScale; + static const float MaxRotationDeadzone; + static const float DefaultRotationDeadzone; + + private: + template <typename Number> + struct CustomParameters + { + Number scale; + Number deadzone; + + CustomParameters(Number scale, Number deadzone) : scale(scale), deadzone(deadzone) {} + }; + + template <class T> + struct InputQueue + { + size_t max_size; + std::queue<T> queue; + + // The default value of 5 for max_size seems to work fine on all platforms + // The effects of changing this value can be tested by setting ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT to 1 + // and playing with the imgui dialog which shows by pressing CTRL+M + InputQueue() : max_size(5) {} + }; + + InputQueue<Vec3d> m_translation; + InputQueue<Vec3f> m_rotation; + std::queue<unsigned int> m_buttons; + + bool m_buttons_enabled; + + CustomParameters<double> m_translation_params; + CustomParameters<float> m_rotation_params; + + // When the 3Dconnexion driver is running the system gets, by default, mouse wheel events when rotations around the X axis are detected. + // We want to filter these out because we are getting the data directly from the device, bypassing the driver, and those mouse wheel events interfere + // by triggering unwanted zoom in/out of the scene + // The following variable is used to count the potential mouse wheel events triggered and is updated by: + // Mouse3DController::collect_input() through the call to the append_rotation() method + // GLCanvas3D::on_mouse_wheel() through the call to the process_mouse_wheel() method + // GLCanvas3D::on_idle() through the call to the apply() method + unsigned int m_mouse_wheel_counter; + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + size_t m_translation_queue_max_size; + size_t m_rotation_queue_max_size; + size_t m_buttons_queue_max_size; +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + public: + State(); + + void append_translation(const Vec3d& translation); + void append_rotation(const Vec3f& rotation); + void append_button(unsigned int id); + + bool has_translation() const { return !m_translation.queue.empty(); } + bool has_rotation() const { return !m_rotation.queue.empty(); } + bool has_button() const { return !m_buttons.empty(); } + + bool process_mouse_wheel(); + + double get_translation_scale() const { return m_translation_params.scale; } + void set_translation_scale(double scale) { m_translation_params.scale = scale; } + + float get_rotation_scale() const { return m_rotation_params.scale; } + void set_rotation_scale(float scale) { m_rotation_params.scale = scale; } + + double get_translation_deadzone() const { return m_translation_params.deadzone; } + void set_translation_deadzone(double deadzone) { m_translation_params.deadzone = deadzone; } + + float get_rotation_deadzone() const { return m_rotation_params.deadzone; } + void set_rotation_deadzone(float deadzone) { m_rotation_params.deadzone = deadzone; } + +#if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + Vec3d get_translation() const { return has_translation() ? m_translation.queue.front() : Vec3d::Zero(); } + Vec3f get_rotation() const { return has_rotation() ? m_rotation.queue.front() : Vec3f::Zero(); } + unsigned int get_button() const { return has_button() ? m_buttons.front() : 0; } + + unsigned int get_translation_queue_size() const { return (unsigned int)m_translation.queue.size(); } + unsigned int get_rotation_queue_size() const { return (unsigned int)m_rotation.queue.size(); } + unsigned int get_buttons_queue_size() const { return (unsigned int)m_buttons.size(); } + + size_t get_translation_queue_max_size() const { return m_translation_queue_max_size; } + size_t get_rotation_queue_max_size() const { return m_rotation_queue_max_size; } + size_t get_buttons_queue_max_size() const { return m_buttons_queue_max_size; } +#endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT + + size_t get_queues_max_size() const { return m_translation.max_size; } + void set_queues_max_size(size_t size); + + // return true if any change to the camera took place + bool apply(Camera& camera); + }; + + bool m_initialized; + mutable State m_state; + std::mutex m_mutex; + std::thread m_thread; + hid_device* m_device; + std::string m_device_str; + bool m_running; + bool m_settings_dialog; + std::chrono::time_point<std::chrono::high_resolution_clock> m_last_time; + +public: + Mouse3DController(); + + void init(); + void shutdown(); + + bool is_device_connected() const { return m_device != nullptr; } + bool is_running() const { return m_running; } + + bool process_mouse_wheel() { std::lock_guard<std::mutex> lock(m_mutex); return m_state.process_mouse_wheel(); } + + bool apply(Camera& camera); + + bool is_settings_dialog_shown() const { return m_settings_dialog; } + void show_settings_dialog(bool show) { m_settings_dialog = show && is_running(); } + void render_settings_dialog(unsigned int canvas_width, unsigned int canvas_height) const; + +private: + bool connect_device(); + void disconnect_device(); + void start(); + void stop() { m_running = false; } + + // secondary thread methods + void run(); + void collect_input(); + + typedef std::array<unsigned char, 13> DataPacket; + bool handle_packet(const DataPacket& packet); + bool handle_wireless_packet(const DataPacket& packet); + bool handle_packet_translation(const DataPacket& packet); + bool handle_packet_rotation(const DataPacket& packet, unsigned int first_byte); + bool handle_packet_button(const DataPacket& packet, unsigned int packet_size); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_Mouse3DController_hpp_ + diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d6c776692..df705f25f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -67,6 +67,7 @@ #include "GUI_Preview.hpp" #include "3DBed.hpp" #include "Camera.hpp" +#include "Mouse3DController.hpp" #include "Tab.hpp" #include "PresetBundle.hpp" #include "BackgroundSlicingProcess.hpp" @@ -353,7 +354,10 @@ wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(15 * // Call select_preset() only if there is new preset and not just modified if ( !boost::algorithm::ends_with(selected_preset, Preset::suffix_modified()) ) - tab->select_preset(selected_preset); + { + const std::string& preset_name = wxGetApp().preset_bundle->filaments.get_preset_name_by_alias(selected_preset); + tab->select_preset(/*selected_preset*/preset_name); + } } })); } @@ -1389,9 +1393,6 @@ struct Plater::priv Slic3r::Model model; PrinterTechnology printer_technology = ptFFF; Slic3r::GCodePreviewData gcode_preview_data; -#if ENABLE_THUMBNAIL_GENERATOR - std::vector<Slic3r::ThumbnailData> thumbnail_data; -#endif // ENABLE_THUMBNAIL_GENERATOR // GUI elements wxSizer* panel_sizer{ nullptr }; @@ -1400,6 +1401,7 @@ struct Plater::priv Sidebar *sidebar; Bed3D bed; Camera camera; + Mouse3DController mouse3d_controller; View3D* view3D; GLToolbar view_toolbar; Preview *preview; @@ -1839,6 +1841,10 @@ struct Plater::priv bool is_preview_loaded() const { return preview->is_loaded(); } bool is_view3D_shown() const { return current_panel == view3D; } +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + bool init_view_toolbar(); +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + void reset_all_gizmos(); void update_ui_from_settings(); ProgressStatusBar* statusbar(); @@ -1968,7 +1974,8 @@ struct Plater::priv bool can_reload_from_disk() const; #if ENABLE_THUMBNAIL_GENERATOR - void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background); + void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background); + void generate_thumbnails(ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background); #endif // ENABLE_THUMBNAIL_GENERATOR void msw_rescale_object_menu(); @@ -1984,7 +1991,9 @@ private: bool complit_init_object_menu(); bool complit_init_sla_object_menu(); bool complit_init_part_menu(); +#if !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX void init_view_toolbar(); +#endif // !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX bool can_split() const; bool layers_height_allowed() const; @@ -2039,7 +2048,15 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) background_process.set_sla_print(&sla_print); background_process.set_gcode_preview_data(&gcode_preview_data); #if ENABLE_THUMBNAIL_GENERATOR - background_process.set_thumbnail_data(&thumbnail_data); + background_process.set_thumbnail_cb([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) + { + std::packaged_task<void(ThumbnailsList&, const Vec2ds&, bool, bool, bool, bool)> task([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) { + generate_thumbnails(thumbnails, sizes, printable_only, parts_only, show_bed, transparent_background); + }); + std::future<void> result = task.get_future(); + wxTheApp->CallAfter([&]() { task(thumbnails, sizes, printable_only, parts_only, show_bed, transparent_background); }); + result.wait(); + }); #endif // ENABLE_THUMBNAIL_GENERATOR background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); @@ -2110,6 +2127,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_RESETGIZMOS, [this](SimpleEvent&) { reset_all_gizmos(); }); view3D_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); }); view3D_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); }); +#if ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE + view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, [this](Event<float>& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); +#endif // ENABLE_ADAPTIVE_LAYER_HEIGHT_PROFILE // 3DScene/Toolbar: view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); @@ -2123,7 +2145,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); +#if !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX view3D_canvas->Bind(EVT_GLCANVAS_INIT, [this](SimpleEvent&) { init_view_toolbar(); }); +#endif // !ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [this](SimpleEvent&) { set_bed_shape(config->option<ConfigOptionPoints>("bed_shape")->values, @@ -2159,12 +2183,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // updates camera type from .ini file camera.set_type(get_config("use_perspective_camera")); + mouse3d_controller.init(); + // Initialize the Undo / Redo stack with a first snapshot. this->take_snapshot(_(L("New Project"))); } Plater::priv::~priv() { + mouse3d_controller.shutdown(); + if (config != nullptr) delete config; } @@ -2287,7 +2315,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ for (size_t i = 0; i < input_files.size(); i++) { const auto &path = input_files[i]; const auto filename = path.filename(); - const auto dlg_info = wxString::Format(_(L("Processing input file %s\n")), from_path(filename)); + const auto dlg_info = wxString::Format(_(L("Processing input file %s")), from_path(filename)) + "\n"; dlg.Update(100 * i / input_files.size(), dlg_info); const bool type_3mf = std::regex_match(path.string(), pattern_3mf); @@ -2328,6 +2356,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ // and place the loaded config over the base. config += std::move(config_loaded); } + + this->model.custom_gcode_per_height = model.custom_gcode_per_height; } if (load_config) @@ -2359,17 +2389,17 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ if (! is_project_file) { if (model.looks_like_multipart_object()) { wxMessageDialog msg_dlg(q, _(L( - "This file contains several objects positioned at multiple heights. " + "This file contains several objects positioned at multiple heights.\n" "Instead of considering them as multiple objects, should I consider\n" - "this file as a single object having multiple parts?\n" - )), _(L("Multi-part object detected")), wxICON_WARNING | wxYES | wxNO); + "this file as a single object having multiple parts?")) + "\n", + _(L("Multi-part object detected")), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { model.convert_multipart_object(nozzle_dmrs->values.size()); } } } else if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { - wxMessageDialog msg_dlg(q, _(L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?\n")), + wxMessageDialog msg_dlg(q, _(L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?"))+"\n", _(L("Detected advanced data")), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { @@ -2414,8 +2444,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ wxMessageDialog msg_dlg(q, _(L( "Multiple objects were loaded for a multi-material printer.\n" "Instead of considering them as multiple objects, should I consider\n" - "these files to represent a single object having multiple parts?\n" - )), _(L("Multi-part object detected")), wxICON_WARNING | wxYES | wxNO); + "these files to represent a single object having multiple parts?")) + "\n", + _(L("Multi-part object detected")), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { new_model->convert_multipart_object(nozzle_dmrs->values.size()); } @@ -2746,8 +2776,7 @@ void Plater::priv::reset() // The hiding of the slicing results, if shown, is not taken care by the background process, so we do it here this->sidebar->show_sliced_info_sizer(false); - auto& config = wxGetApp().preset_bundle->project_config; - config.option<ConfigOptionFloats>("colorprint_heights")->values.clear(); + model.custom_gcode_per_height.clear(); } void Plater::priv::mirror(Axis axis) @@ -3046,6 +3075,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->update_print_volume_state(); // Apply new config to the possibly running background task. bool was_running = this->background_process.running(); + this->background_process.set_force_update_print_regions(force_validation); Print::ApplyStatus invalidated = this->background_process.apply(this->q->model(), wxGetApp().preset_bundle->full_config()); // Just redraw the 3D canvas without reloading the scene to consume the update of the layer height profile. @@ -3153,37 +3183,6 @@ bool Plater::priv::restart_background_process(unsigned int state) ( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) || (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { -#if ENABLE_THUMBNAIL_GENERATOR - if (((state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) == 0) && - (this->background_process.state() != BackgroundSlicingProcess::STATE_RUNNING)) - { - // update thumbnail data - const std::vector<Vec2d> &thumbnail_sizes = this->background_process.current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values; - if (this->printer_technology == ptFFF) - { - // for ptFFF we need to generate the thumbnails before the export of gcode starts - this->thumbnail_data.clear(); - for (const Vec2d &sized : thumbnail_sizes) - { - this->thumbnail_data.push_back(ThumbnailData()); - Point size(sized); // round to ints - generate_thumbnail(this->thumbnail_data.back(), size.x(), size.y(), true, true, false); - } - } - else if (this->printer_technology == ptSLA) - { - // for ptSLA generate thumbnails without supports and pad (not yet calculated) - // to render also supports and pad see on_slicing_update() - this->thumbnail_data.clear(); - for (const Vec2d &sized : thumbnail_sizes) - { - this->thumbnail_data.push_back(ThumbnailData()); - Point size(sized); // round to ints - generate_thumbnail(this->thumbnail_data.back(), size.x(), size.y(), true, true, false); - } - } - } -#endif // ENABLE_THUMBNAIL_GENERATOR // The print is valid and it can be started. if (this->background_process.start()) { this->statusbar()->set_cancel_callback([this]() { @@ -3282,21 +3281,80 @@ void Plater::priv::reload_from_disk() for (unsigned int idx : selected_volumes_idxs) { const GLVolume* v = selection.get_volume(idx); - int o_idx = v->object_idx(); int v_idx = v->volume_idx(); - selected_volumes.push_back({ o_idx, v_idx }); + if (v_idx >= 0) + { + int o_idx = v->object_idx(); + if ((0 <= o_idx) && (o_idx < (int)model.objects.size())) + selected_volumes.push_back({ o_idx, v_idx }); + } } std::sort(selected_volumes.begin(), selected_volumes.end()); selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end()), selected_volumes.end()); // collects paths of files to load std::vector<fs::path> input_paths; +#if ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION + std::vector<fs::path> missing_input_paths; +#endif // ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION for (const SelectedVolume& v : selected_volumes) { +#if ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION + const ModelObject* object = model.objects[v.object_idx]; + const ModelVolume* volume = object->volumes[v.volume_idx]; + + if (!volume->source.input_file.empty()) + { + if (fs::exists(volume->source.input_file)) + input_paths.push_back(volume->source.input_file); + else + missing_input_paths.push_back(volume->source.input_file); + } +#else const ModelVolume* volume = model.objects[v.object_idx]->volumes[v.volume_idx]; if (!volume->source.input_file.empty() && boost::filesystem::exists(volume->source.input_file)) input_paths.push_back(volume->source.input_file); +#endif // ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION } + +#if ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION + std::sort(missing_input_paths.begin(), missing_input_paths.end()); + missing_input_paths.erase(std::unique(missing_input_paths.begin(), missing_input_paths.end()), missing_input_paths.end()); + + while (!missing_input_paths.empty()) + { + // ask user to select the missing file + std::string search = missing_input_paths.back().string(); + wxFileDialog dialog(q, _(L("Please select the file to reload:")), "", search, file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string sel_filename_path = dialog.GetPath().ToUTF8().data(); + std::string sel_filename = fs::path(sel_filename_path).filename().string(); + if (boost::algorithm::iends_with(search, sel_filename)) + { + input_paths.push_back(sel_filename_path); + missing_input_paths.pop_back(); + + std::string sel_path = fs::path(sel_filename_path).remove_filename().string(); + + std::vector<fs::path>::iterator it = missing_input_paths.begin(); + while (it != missing_input_paths.end()) + { + // try to use the path of the selected file with all remaining missing files + std::string repathed_filename = sel_path + "/" + it->filename().string(); + if (fs::exists(repathed_filename)) + { + input_paths.push_back(repathed_filename); + it = missing_input_paths.erase(it); + } + else + ++it; + } + } + } +#endif // ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION + std::sort(input_paths.begin(), input_paths.end()); input_paths.erase(std::unique(input_paths.begin(), input_paths.end()), input_paths.end()); @@ -3317,7 +3375,6 @@ void Plater::priv::reload_from_disk() catch (std::exception&) { // error while loading - view3D->get_canvas3d()->enable_render(true); return; } @@ -3331,22 +3388,20 @@ void Plater::priv::reload_from_disk() if (old_volume->source.input_file == path) { - if (new_object_idx < (int)new_model.objects.size()) + assert(new_object_idx < (int)new_model.objects.size()); + ModelObject* new_model_object = new_model.objects[new_object_idx]; + if (new_volume_idx < (int)new_model_object->volumes.size()) { - ModelObject* new_model_object = new_model.objects[new_object_idx]; - if (new_volume_idx < (int)new_model_object->volumes.size()) - { - old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); - ModelVolume* new_volume = old_model_object->volumes.back(); - new_volume->set_new_unique_id(); - new_volume->config.apply(old_volume->config); - new_volume->set_type(old_volume->type()); - new_volume->set_material_id(old_volume->material_id()); - new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); - std::swap(old_model_object->volumes[old_v.volume_idx], old_model_object->volumes.back()); - old_model_object->delete_volume(old_model_object->volumes.size() - 1); - } + old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); + ModelVolume* new_volume = old_model_object->volumes.back(); + new_volume->set_new_unique_id(); + new_volume->config.apply(old_volume->config); + new_volume->set_type(old_volume->type()); + new_volume->set_material_id(old_volume->material_id()); + new_volume->set_transformation(old_volume->get_transformation()); + new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + std::swap(old_model_object->volumes[old_v.volume_idx], old_model_object->volumes.back()); + old_model_object->delete_volume(old_model_object->volumes.size() - 1); } } } @@ -3430,6 +3485,7 @@ void Plater::priv::set_current_panel(wxPanel* panel) } else view3D->reload_scene(true); } + // sets the canvas as dirty to force a render at the 1st idle event (wxWidgets IsShownOnScreen() is buggy and cannot be used reliably) view3D->set_as_dirty(); view_toolbar.select_item("3D"); @@ -3444,6 +3500,7 @@ void Plater::priv::set_current_panel(wxPanel* panel) this->q->reslice(); // keeps current gcode preview, if any preview->reload_print(true); + preview->set_canvas_as_dirty(); view_toolbar.select_item("Preview"); } @@ -3466,9 +3523,10 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) //! combo->GetStringSelection().ToUTF8().data()); const std::string& selected_string = combo->GetString(combo->GetSelection()).ToUTF8().data(); + const std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, selected_string); if (preset_type == Preset::TYPE_FILAMENT) { - wxGetApp().preset_bundle->set_filament_preset(idx, selected_string); + wxGetApp().preset_bundle->set_filament_preset(idx, preset_name); } // TODO: ? @@ -3478,7 +3536,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) } else { wxWindowUpdateLocker noUpdates(sidebar->presets_panel()); - wxGetApp().get_tab(preset_type)->select_preset(selected_string); + wxGetApp().get_tab(preset_type)->select_preset(preset_name); } // update plater with new config @@ -3489,7 +3547,6 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) * and for SLA presets they should be deleted */ if (preset_type == Preset::TYPE_PRINTER) -// wxGetApp().obj_list()->update_settings_items(); wxGetApp().obj_list()->update_object_list_by_printer_technology(); } @@ -3521,25 +3578,6 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) } else if (evt.status.flags & PrintBase::SlicingStatus::RELOAD_SLA_PREVIEW) { // Update the SLA preview. Only called if not RELOAD_SLA_SUPPORT_POINTS, as the block above will refresh the preview anyways. this->preview->reload_print(); - - // uncomment the following lines if you want to render into the thumbnail also supports and pad for SLA printer -/* -#if ENABLE_THUMBNAIL_GENERATOR - // update thumbnail data - // for ptSLA generate the thumbnail after supports and pad have been calculated to have them rendered - if ((this->printer_technology == ptSLA) && (evt.status.percent == -3)) - { - const std::vector<Vec2d>& thumbnail_sizes = this->background_process.current_print()->full_print_config().option<ConfigOptionPoints>("thumbnails")->values; - this->thumbnail_data.clear(); - for (const Vec2d &sized : thumbnail_sizes) - { - this->thumbnail_data.push_back(ThumbnailData()); - Point size(sized); // round to ints - generate_thumbnail(this->thumbnail_data.back(), size.x(), size.y(), true, false, false); - } - } -#endif // ENABLE_THUMBNAIL_GENERATOR -*/ } } @@ -3655,17 +3693,29 @@ void Plater::priv::on_right_click(RBtnEvent& evt) wxMenu* menu = nullptr; - if (obj_idx == -1) - menu = &default_menu; + if (obj_idx == -1) // no one or several object are selected + { + if (evt.data.second) // right button was clicked on empty space + menu = &default_menu; + else + return; + } else { // If in 3DScene is(are) selected volume(s), but right button was clicked on empty space if (evt.data.second) return; - menu = printer_technology == ptSLA ? &sla_object_menu : - get_selection().is_single_full_instance() ? // show "Object menu" for each FullInstance instead of FullObject - &object_menu : &part_menu; + if (printer_technology == ptSLA) + menu = &sla_object_menu; + else + { + // show "Object menu" for each one or several FullInstance instead of FullObject + const bool is_some_full_instances = get_selection().is_single_full_instance() || + get_selection().is_single_full_object() || + get_selection().is_multiple_full_instance(); + menu = is_some_full_instances ? &object_menu : &part_menu; + } sidebar->obj_list()->append_menu_item_settings(menu); @@ -3766,9 +3816,22 @@ bool Plater::priv::init_object_menu() } #if ENABLE_THUMBNAIL_GENERATOR -void Plater::priv::generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool transparent_background) +void Plater::priv::generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) +{ + view3D->get_canvas3d()->render_thumbnail(data, w, h, printable_only, parts_only, show_bed, transparent_background); +} + +void Plater::priv::generate_thumbnails(ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) { - view3D->get_canvas3d()->render_thumbnail(data, w, h, printable_only, parts_only, transparent_background); + thumbnails.clear(); + for (const Vec2d& size : sizes) + { + thumbnails.push_back(ThumbnailData()); + Point isize(size); // round to ints + generate_thumbnail(thumbnails.back(), isize.x(), isize.y(), printable_only, parts_only, show_bed, transparent_background); + if (!thumbnails.back().is_valid()) + thumbnails.pop_back(); + } } #endif // ENABLE_THUMBNAIL_GENERATOR @@ -3845,14 +3908,18 @@ bool Plater::priv::init_common_menu(wxMenu* menu, const bool is_part/* = false*/ [this](wxCommandEvent&) { reload_from_disk(); }, "", nullptr, [this]() { return can_reload_from_disk(); }, q); append_menu_item(menu, wxID_ANY, _(L("Export as STL")) + dots, _(L("Export the selected object as STL file")), - [this](wxCommandEvent&) { q->export_stl(false, true); }); + [this](wxCommandEvent&) { q->export_stl(false, true); }, "", nullptr, + [this]() { + const Selection& selection = get_selection(); + return selection.is_single_full_instance() || selection.is_single_full_object(); + }, q); menu->AppendSeparator(); q->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { const Selection& selection = get_selection(); int instance_idx = selection.get_instance_idx(); - evt.Enable(instance_idx != -1); + evt.Enable(selection.is_single_full_instance() || selection.is_single_full_object()); if (instance_idx != -1) { evt.Check(model.objects[selection.get_object_idx()]->instances[instance_idx]->printable); @@ -3898,7 +3965,7 @@ bool Plater::priv::complit_init_object_menu() object_menu.AppendSeparator(); // Layers Editing for object - sidebar->obj_list()->append_menu_item_layers_editing(&object_menu); + sidebar->obj_list()->append_menu_item_layers_editing(&object_menu, q); object_menu.AppendSeparator(); // "Add (volumes)" popupmenu will be added later in append_menu_items_add_volume() @@ -3933,8 +4000,18 @@ bool Plater::priv::complit_init_part_menu() return true; } +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX +bool Plater::priv::init_view_toolbar() +#else void Plater::priv::init_view_toolbar() +#endif //!ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX { +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + if (view_toolbar.get_items_count() > 0) + // already initialized + return true; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + BackgroundTexture::Metadata background_data; background_data.filename = "toolbar_background.png"; background_data.left = 16; @@ -3943,7 +4020,11 @@ void Plater::priv::init_view_toolbar() background_data.bottom = 16; if (!view_toolbar.init(background_data)) +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + return false; +#else return; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX view_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left); view_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Bottom); @@ -3958,7 +4039,11 @@ void Plater::priv::init_view_toolbar() item.sprite_id = 0; item.left.action_callback = [this]() { if (this->q != nullptr) wxPostEvent(this->q, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); }; if (!view_toolbar.add_item(item)) +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + return false; +#else return; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX item.name = "Preview"; item.icon_filename = "preview.svg"; @@ -3966,10 +4051,18 @@ void Plater::priv::init_view_toolbar() item.sprite_id = 1; item.left.action_callback = [this]() { if (this->q != nullptr) wxPostEvent(this->q, SimpleEvent(EVT_GLVIEWTOOLBAR_PREVIEW)); }; if (!view_toolbar.add_item(item)) +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + return false; +#else return; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX view_toolbar.select_item("3D"); view_toolbar.set_enabled(true); + +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + return true; +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX } bool Plater::priv::can_set_instance_to_object() const @@ -4030,7 +4123,11 @@ bool Plater::priv::can_reload_from_disk() const for (const SelectedVolume& v : selected_volumes) { const ModelVolume* volume = model.objects[v.object_idx]->volumes[v.volume_idx]; +#if ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION + if (!volume->source.input_file.empty()) +#else if (!volume->source.input_file.empty() && boost::filesystem::exists(volume->source.input_file)) +#endif // ENABLE_RELOAD_FROM_DISK_MISSING_SELECTION paths.push_back(volume->source.input_file); } std::sort(paths.begin(), paths.end()); @@ -4282,6 +4379,7 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator // Disable layer editing before the Undo / Redo jump. if (!new_variable_layer_editing_active && view3D->is_layers_editing_enabled()) view3D->get_canvas3d()->force_main_toolbar_left_action(view3D->get_canvas3d()->get_main_toolbar_item_id("layersediting")); + // Make a copy of the snapshot, undo/redo could invalidate the iterator const UndoRedo::Snapshot snapshot_copy = *it_snapshot; // Do the jump in time. @@ -4812,7 +4910,7 @@ void Plater::export_3mf(const boost::filesystem::path& output_path) wxBusyCursor wait; #if ENABLE_THUMBNAIL_GENERATOR ThumbnailData thumbnail_data; - p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, false, true, true); + p->generate_thumbnail(thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, false, true, true, true); if (Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr, &thumbnail_data)) { #else if (Slic3r::store_3mf(path_u8.c_str(), &p->model, export_config ? &cfg : nullptr)) { @@ -4893,7 +4991,7 @@ void Plater::reslice() p->show_action_buttons(true); // update type of preview - p->preview->update_view_type(); + p->preview->update_view_type(true); } void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) @@ -5172,6 +5270,9 @@ std::vector<std::string> Plater::get_extruder_colors_from_plater_config() const return extruder_colors; extruder_colors = (config->option<ConfigOptionStrings>("extruder_colour"))->values; + if (!wxGetApp().plater()) + return extruder_colors; + const std::vector<std::string>& filament_colours = (p->config->option<ConfigOptionStrings>("filament_colour"))->values; for (size_t i = 0; i < extruder_colors.size(); ++i) if (extruder_colors[i] == "" && i < filament_colours.size()) @@ -5180,6 +5281,17 @@ std::vector<std::string> Plater::get_extruder_colors_from_plater_config() const return extruder_colors; } +std::vector<std::string> Plater::get_colors_for_color_print() const +{ + std::vector<std::string> colors = get_extruder_colors_from_plater_config(); + + for (const Model::CustomGCode& code : p->model.custom_gcode_per_height) + if (code.gcode == ColorChangeCode) + colors.push_back(code.color); + + return colors; +} + wxString Plater::get_project_filename(const wxString& extension) const { return p->get_project_filename(extension); @@ -5332,11 +5444,28 @@ void Plater::msw_rescale() GetParent()->Layout(); } +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX +bool Plater::init_view_toolbar() +{ + return p->init_view_toolbar(); +} +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + const Camera& Plater::get_camera() const { return p->camera; } +const Mouse3DController& Plater::get_mouse3d_controller() const +{ + return p->mouse3d_controller; +} + +Mouse3DController& Plater::get_mouse3d_controller() +{ + return p->mouse3d_controller; +} + bool Plater::can_delete() const { return p->can_delete(); } bool Plater::can_delete_all() const { return p->can_delete_all(); } bool Plater::can_increase_instances() const { return p->can_increase_instances(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 50a4d1f17..730ba1097 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -41,6 +41,7 @@ class ObjectSettings; class ObjectLayers; class ObjectList; class GLCanvas3D; +class Mouse3DController; using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>; @@ -223,6 +224,7 @@ public: void on_activate(); const DynamicPrintConfig* get_plater_config() const; std::vector<std::string> get_extruder_colors_from_plater_config() const; + std::vector<std::string> get_colors_for_color_print() const; void update_object_menu(); @@ -260,7 +262,13 @@ public: void msw_rescale(); +#if ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + bool init_view_toolbar(); +#endif // ENABLE_VIEW_TOOLBAR_BACKGROUND_FIX + const Camera& get_camera() const; + const Mouse3DController& get_mouse3d_controller() const; + Mouse3DController& get_mouse3d_controller(); // ROII wrapper for suppressing the Undo / Redo snapshot to be taken. class SuppressSnapshots diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index baf7edbfa..4f07a7cf0 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -303,60 +303,58 @@ std::string Preset::label() const return this->name + (this->is_dirty ? g_suffix_modified : ""); } -bool Preset::is_compatible_with_print(const Preset &active_print) const +bool is_compatible_with_print(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print) { - auto &condition = this->compatible_prints_condition(); - auto *compatible_prints = dynamic_cast<const ConfigOptionStrings*>(this->config.option("compatible_prints")); + if (preset.vendor != nullptr && preset.vendor != active_print.vendor) + // The current profile has a vendor assigned and it is different from the active print's vendor. + return false; + auto &condition = preset.preset.compatible_prints_condition(); + auto *compatible_prints = dynamic_cast<const ConfigOptionStrings*>(preset.preset.config.option("compatible_prints")); bool has_compatible_prints = compatible_prints != nullptr && ! compatible_prints->values.empty(); if (! has_compatible_prints && ! condition.empty()) { try { - return PlaceholderParser::evaluate_boolean_expression(condition, active_print.config); + return PlaceholderParser::evaluate_boolean_expression(condition, active_print.preset.config); } catch (const std::runtime_error &err) { //FIXME in case of an error, return "compatible with everything". - printf("Preset::is_compatible_with_print - parsing error of compatible_prints_condition %s:\n%s\n", active_print.name.c_str(), err.what()); + printf("Preset::is_compatible_with_print - parsing error of compatible_prints_condition %s:\n%s\n", active_print.preset.name.c_str(), err.what()); return true; } } - return this->is_default || active_print.name.empty() || ! has_compatible_prints || - std::find(compatible_prints->values.begin(), compatible_prints->values.end(), active_print.name) != + return preset.preset.is_default || active_print.preset.name.empty() || ! has_compatible_prints || + std::find(compatible_prints->values.begin(), compatible_prints->values.end(), active_print.preset.name) != compatible_prints->values.end(); } -bool Preset::is_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config) const +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config) { - auto &condition = this->compatible_printers_condition(); - auto *compatible_printers = dynamic_cast<const ConfigOptionStrings*>(this->config.option("compatible_printers")); + if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // The current profile has a vendor assigned and it is different from the active print's vendor. + return false; + auto &condition = preset.preset.compatible_printers_condition(); + auto *compatible_printers = dynamic_cast<const ConfigOptionStrings*>(preset.preset.config.option("compatible_printers")); bool has_compatible_printers = compatible_printers != nullptr && ! compatible_printers->values.empty(); if (! has_compatible_printers && ! condition.empty()) { try { - return PlaceholderParser::evaluate_boolean_expression(condition, active_printer.config, extra_config); + return PlaceholderParser::evaluate_boolean_expression(condition, active_printer.preset.config, extra_config); } catch (const std::runtime_error &err) { //FIXME in case of an error, return "compatible with everything". - printf("Preset::is_compatible_with_printer - parsing error of compatible_printers_condition %s:\n%s\n", active_printer.name.c_str(), err.what()); + printf("Preset::is_compatible_with_printer - parsing error of compatible_printers_condition %s:\n%s\n", active_printer.preset.name.c_str(), err.what()); return true; } } - return this->is_default || active_printer.name.empty() || ! has_compatible_printers || - std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.name) != + return preset.preset.is_default || active_printer.preset.name.empty() || ! has_compatible_printers || + std::find(compatible_printers->values.begin(), compatible_printers->values.end(), active_printer.preset.name) != compatible_printers->values.end(); } -bool Preset::is_compatible_with_printer(const Preset &active_printer) const +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer) { DynamicPrintConfig config; - config.set_key_value("printer_preset", new ConfigOptionString(active_printer.name)); - const ConfigOption *opt = active_printer.config.option("nozzle_diameter"); + config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast<const ConfigOptionFloats*>(opt)->values.size())); - return this->is_compatible_with_printer(active_printer, &config); -} - -bool Preset::update_compatible(const Preset &active_printer, const DynamicPrintConfig *extra_config, const Preset *active_print) -{ - this->is_compatible = is_compatible_with_printer(active_printer, extra_config); - if (active_print != nullptr) - this->is_compatible &= is_compatible_with_print(*active_print); - return this->is_compatible; + return is_compatible_with_printer(preset, active_printer, &config); } void Preset::set_visible_from_appconfig(const AppConfig &app_config) @@ -814,6 +812,8 @@ void PresetCollection::save_current_preset(const std::string &new_name) preset.is_external = false; // The newly saved preset will be activated -> make it visible. preset.is_visible = true; + // Just system presets have aliases + preset.alias.clear(); } // 2) Activate the saved preset. this->select_preset_by_name(new_name, true); @@ -907,6 +907,35 @@ const Preset* PresetCollection::get_preset_parent(const Preset& child) const return (preset == nullptr/* || preset->is_default */|| preset->is_external) ? nullptr : preset; } +// Return vendor of the first parent profile, for which the vendor is defined, or null if such profile does not exist. +PresetWithVendorProfile PresetCollection::get_preset_with_vendor_profile(const Preset &preset) const +{ + const Preset *p = &preset; + const VendorProfile *v = nullptr; + do { + if (p->vendor != nullptr) { + v = p->vendor; + break; + } + p = this->get_preset_parent(*p); + } while (p != nullptr); + return PresetWithVendorProfile(preset, v); +} + +const std::string& PresetCollection::get_preset_name_by_alias(const std::string& alias) const +{ + for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++i) { + const Preset& preset = this->m_presets[i]; + if (!preset.is_visible || (!preset.is_compatible && i != m_idx_selected)) + continue; + + if (preset.alias == alias) + return preset.name; + } + + return alias; +} + const std::string& PresetCollection::get_suffix_modified() { return g_suffix_modified; } @@ -944,19 +973,22 @@ void PresetCollection::set_default_suppressed(bool default_suppressed) } } -size_t PresetCollection::update_compatible_internal(const Preset &active_printer, const Preset *active_print, bool unselect_if_incompatible) +size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, bool unselect_if_incompatible) { DynamicPrintConfig config; - config.set_key_value("printer_preset", new ConfigOptionString(active_printer.name)); - const ConfigOption *opt = active_printer.config.option("nozzle_diameter"); + config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast<const ConfigOptionFloats*>(opt)->values.size())); for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) { bool selected = idx_preset == m_idx_selected; Preset &preset_selected = m_presets[idx_preset]; Preset &preset_edited = selected ? m_edited_preset : preset_selected; - if (! preset_edited.update_compatible(active_printer, &config, active_print) && - selected && unselect_if_incompatible) + const PresetWithVendorProfile this_preset_with_vendor_profile = this->get_preset_with_vendor_profile(preset_edited); + preset_edited.is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); + if (active_print != nullptr) + preset_edited.is_compatible &= is_compatible_with_print(this_preset_with_vendor_profile, *active_print); + if (! preset_edited.is_compatible && selected && unselect_if_incompatible) m_idx_selected = -1; if (selected) preset_selected.is_compatible = preset_edited.is_compatible; @@ -982,7 +1014,7 @@ void PresetCollection::update_platter_ui(GUI::PresetComboBox *ui) // Otherwise fill in the list from scratch. ui->Freeze(); ui->Clear(); - size_t selected_preset_item = 0; + size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected const Preset &selected_preset = this->get_selected_preset(); // Show wide icons if the currently selected preset is not compatible with the current printer, @@ -1001,6 +1033,7 @@ void PresetCollection::update_platter_ui(GUI::PresetComboBox *ui) std::map<wxString, wxBitmap*> nonsys_presets; wxString selected = ""; + wxString tooltip = ""; if (!this->m_presets.front().is_visible) ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++ i) { @@ -1029,17 +1062,24 @@ void PresetCollection::update_platter_ui(GUI::PresetComboBox *ui) bmp = m_bitmap_cache->insert(bitmap_key, bmps); } + const std::string name = preset.alias.empty() ? preset.name : preset.alias; if (preset.is_default || preset.is_system) { - ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), + ui->Append(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), (bmp == 0) ? (m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap) : *bmp); - if (i == m_idx_selected) + if (i == m_idx_selected || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX) { selected_preset_item = ui->GetCount() - 1; + tooltip = wxString::FromUTF8(preset.name.c_str()); + } } else { - nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/); - if (i == m_idx_selected) - selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()); + nonsys_presets.emplace(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/); + if (i == m_idx_selected) { + selected = wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()); + tooltip = wxString::FromUTF8(preset.name.c_str()); + } } if (i + 1 == m_num_default_presets) ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); @@ -1049,7 +1089,9 @@ void PresetCollection::update_platter_ui(GUI::PresetComboBox *ui) ui->set_label_marker(ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap)); for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { ui->Append(it->first, *it->second); - if (it->first == selected) + if (it->first == selected || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX) selected_preset_item = ui->GetCount() - 1; } } @@ -1080,8 +1122,15 @@ void PresetCollection::update_platter_ui(GUI::PresetComboBox *ui) ui->set_label_marker(ui->Append(PresetCollection::separator(L("Add/Remove materials")), wxNullBitmap), GUI::PresetComboBox::LABEL_ITEM_WIZARD_MATERIALS); } + /* But, if selected_preset_item is still equal to INT_MAX, it means that + * there is no presets added to the list. + * So, select last combobox item ("Add/Remove preset") + */ + if (selected_preset_item == INT_MAX) + selected_preset_item = ui->GetCount() - 1; + ui->SetSelection(selected_preset_item); - ui->SetToolTip(ui->GetString(selected_preset_item)); + ui->SetToolTip(tooltip.IsEmpty() ? ui->GetString(selected_preset_item) : tooltip); ui->check_selection(); ui->Thaw(); @@ -1096,7 +1145,7 @@ size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompati return 0; ui->Freeze(); ui->Clear(); - size_t selected_preset_item = 0; + size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. * So set sizes for solid_colored(empty) icons used for preset @@ -1131,7 +1180,9 @@ size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompati if (preset.is_default || preset.is_system) { ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), (bmp == 0) ? (m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap) : *bmp); - if (i == m_idx_selected) + if (i == m_idx_selected || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX) selected_preset_item = ui->GetCount() - 1; } else @@ -1148,7 +1199,9 @@ size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompati ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap); for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { ui->Append(it->first, *it->second); - if (it->first == selected) + if (it->first == selected || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX) selected_preset_item = ui->GetCount() - 1; } } @@ -1163,6 +1216,14 @@ size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompati } ui->Append(PresetCollection::separator("Add a new printer"), *bmp); } + + /* But, if selected_preset_item is still equal to INT_MAX, it means that + * there is no presets added to the list. + * So, select last combobox item ("Add/Remove preset") + */ + if (selected_preset_item == INT_MAX) + selected_preset_item = ui->GetCount() - 1; + ui->SetSelection(selected_preset_item); ui->SetToolTip(ui->GetString(selected_preset_item)); ui->Thaw(); diff --git a/src/slic3r/GUI/Preset.hpp b/src/slic3r/GUI/Preset.hpp index e2e4baa88..785f52c42 100644 --- a/src/slic3r/GUI/Preset.hpp +++ b/src/slic3r/GUI/Preset.hpp @@ -79,6 +79,8 @@ public: VendorProfile() {} VendorProfile(std::string id) : id(std::move(id)) {} + bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); } + // Load VendorProfile from an ini file. // If `load_all` is false, only the header with basic info (name, version, URLs) is loaded. static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); @@ -91,6 +93,17 @@ public: bool operator==(const VendorProfile &rhs) const { return this->id == rhs.id; } }; +class Preset; + +// Helper to hold a profile with its vendor definition, where the vendor definition may have been extracted from a parent system preset. +// The parent preset is only accessible through PresetCollection, therefore to allow definition of the various is_compatible_with methods +// outside of the PresetCollection, this composite is returned by PresetCollection::get_preset_with_vendor_profile() when needed. +struct PresetWithVendorProfile { + PresetWithVendorProfile(const Preset &preset, const VendorProfile *vendor) : preset(preset), vendor(vendor) {} + const Preset &preset; + const VendorProfile *vendor; +}; + // Note: it is imporant that map is used here rather than unordered_map, // because we need iterators to not be invalidated, // because Preset and the ConfigWizard hold pointers to VendorProfiles. @@ -148,6 +161,9 @@ public: // Configuration data, loaded from a file, or set from the defaults. DynamicPrintConfig config; + // Alias of the preset + std::string alias = ""; + void save(); // Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty. @@ -158,10 +174,6 @@ public: void set_dirty(bool dirty = true) { this->is_dirty = dirty; } void reset_dirty() { this->is_dirty = false; } - bool is_compatible_with_print(const Preset &active_print) const; - bool is_compatible_with_printer(const Preset &active_printer, const DynamicPrintConfig *extra_config) const; - bool is_compatible_with_printer(const Preset &active_printer) const; - // Returns the name of the preset, from which this preset inherits. static std::string& inherits(DynamicPrintConfig &cfg) { return cfg.option<ConfigOptionString>("inherits", true)->value; } std::string& inherits() { return Preset::inherits(this->config); } @@ -195,9 +207,6 @@ public: // This call returns a reference, it may add a new entry into the DynamicPrintConfig. PrinterTechnology& printer_technology_ref() { return this->config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value; } - // Mark this preset as compatible if it is compatible with active_printer. - bool update_compatible(const Preset &active_printer, const DynamicPrintConfig *extra_config, const Preset *active_print = nullptr); - // Set is_visible according to application config void set_visible_from_appconfig(const AppConfig &app_config); @@ -230,6 +239,10 @@ protected: static std::string remove_suffix_modified(const std::string &name); }; +bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print); +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config); +bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer); + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -326,6 +339,12 @@ public: Preset& get_edited_preset() { return m_edited_preset; } const Preset& get_edited_preset() const { return m_edited_preset; } + // Return vendor of the first parent profile, for which the vendor is defined, or null if such profile does not exist. + PresetWithVendorProfile get_preset_with_vendor_profile(const Preset &preset) const; + PresetWithVendorProfile get_edited_preset_with_vendor_profile() const { return this->get_preset_with_vendor_profile(this->get_edited_preset()); } + + const std::string& get_preset_name_by_alias(const std::string& alias) const; + // used to update preset_choice from Tab const std::deque<Preset>& get_presets() const { return m_presets; } int get_idx_selected() { return m_idx_selected; } @@ -383,13 +402,13 @@ public: // For Print / Filament presets, disable those, which are not compatible with the printer. template<typename PreferedCondition> - void update_compatible(const Preset &active_printer, const Preset *active_print, bool select_other_if_incompatible, PreferedCondition prefered_condition) + void update_compatible(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, bool select_other_if_incompatible, PreferedCondition prefered_condition) { if (this->update_compatible_internal(active_printer, active_print, select_other_if_incompatible) == (size_t)-1) // Find some other compatible preset, or the "-- default --" preset. this->select_preset(this->first_compatible_idx(prefered_condition)); } - void update_compatible(const Preset &active_printer, const Preset *active_print, bool select_other_if_incompatible) + void update_compatible(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, bool select_other_if_incompatible) { this->update_compatible(active_printer, active_print, select_other_if_incompatible, [](const std::string&){return true;}); } size_t num_visible() const { return std::count_if(m_presets.begin(), m_presets.end(), [](const Preset &preset){return preset.is_visible;}); } @@ -472,7 +491,7 @@ private: std::deque<Preset>::const_iterator find_preset_internal(const std::string &name) const { return const_cast<PresetCollection*>(this)->find_preset_internal(name); } - size_t update_compatible_internal(const Preset &active_printer, const Preset *active_print, bool unselect_if_incompatible); + size_t update_compatible_internal(const PresetWithVendorProfile &active_printer, const PresetWithVendorProfile *active_print, bool unselect_if_incompatible); static std::vector<std::string> dirty_options(const Preset *edited, const Preset *reference, const bool is_printer_type = false); diff --git a/src/slic3r/GUI/PresetBundle.cpp b/src/slic3r/GUI/PresetBundle.cpp index 01c42d3de..9b8eaa8ec 100644 --- a/src/slic3r/GUI/PresetBundle.cpp +++ b/src/slic3r/GUI/PresetBundle.cpp @@ -329,58 +329,64 @@ void PresetBundle::load_installed_printers(const AppConfig &config) } } -void PresetBundle::load_installed_filaments(AppConfig &config) +const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) const { - if (! config.has_section(AppConfig::SECTION_FILAMENTS)) { - std::unordered_set<const Preset*> comp_filaments; + // there are not aliases for Printers profiles + if (preset_type == Preset::TYPE_PRINTER || preset_type == Preset::TYPE_INVALID) + return alias; - for (const Preset &printer : printers) { - if (! printer.is_visible || printer.printer_technology() != ptFFF) { - continue; - } + const PresetCollection& presets = preset_type == Preset::TYPE_PRINT ? prints : + preset_type == Preset::TYPE_SLA_PRINT ? sla_prints : + preset_type == Preset::TYPE_FILAMENT ? filaments : + sla_materials; - for (const Preset &filament : filaments) { - if (filament.is_compatible_with_printer(printer)) { - comp_filaments.insert(&filament); - } - } - } + return presets.get_preset_name_by_alias(alias); +} - for (const auto &filament: comp_filaments) { +void PresetBundle::load_installed_filaments(AppConfig &config) +{ + if (! config.has_section(AppConfig::SECTION_FILAMENTS)) { + // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. + // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles + // were installed. + std::unordered_set<const Preset*> compatible_filaments; + for (const Preset &printer : printers) + if (printer.is_visible && printer.printer_technology() == ptFFF) { + const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + for (const Preset &filament : filaments) + if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), printer_with_vendor_profile)) + compatible_filaments.insert(&filament); + } + // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. + for (const auto &filament: compatible_filaments) config.set(AppConfig::SECTION_FILAMENTS, filament->name, "1"); - } } - for (auto &preset : filaments) { + for (auto &preset : filaments) preset.set_visible_from_appconfig(config); - } } void PresetBundle::load_installed_sla_materials(AppConfig &config) { if (! config.has_section(AppConfig::SECTION_MATERIALS)) { std::unordered_set<const Preset*> comp_sla_materials; - - for (const Preset &printer : printers) { - if (! printer.is_visible || printer.printer_technology() != ptSLA) { - continue; - } - - for (const Preset &material : sla_materials) { - if (material.is_compatible_with_printer(printer)) { - comp_sla_materials.insert(&material); - } - } - } - - for (const auto &material: comp_sla_materials) { + // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. + // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles + // were installed. + for (const Preset &printer : printers) + if (printer.is_visible && printer.printer_technology() == ptSLA) { + const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + for (const Preset &material : sla_materials) + if (is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) + comp_sla_materials.insert(&material); + } + // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. + for (const auto &material: comp_sla_materials) config.set(AppConfig::SECTION_MATERIALS, material->name, "1"); - } } - for (auto &preset : sla_materials) { + for (auto &preset : sla_materials) preset.set_visible_from_appconfig(config); - } } // Load selections (current print, current filaments, current printer) from config.ini @@ -1122,6 +1128,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla PresetCollection *presets = nullptr; std::vector<std::string> *loaded = nullptr; std::string preset_name; + std::string alias_name; if (boost::starts_with(section.first, "print:")) { presets = &this->prints; loaded = &loaded_prints; @@ -1130,6 +1137,12 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla presets = &this->filaments; loaded = &loaded_filaments; preset_name = section.first.substr(9); + + for (const auto& item : section.second) + if (boost::starts_with(item.first, "alias")) { + alias_name = item.second.data(); + break; + } } else if (boost::starts_with(section.first, "sla_print:")) { presets = &this->sla_prints; loaded = &loaded_sla_prints; @@ -1138,6 +1151,9 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla presets = &this->sla_materials; loaded = &loaded_sla_materials; preset_name = section.first.substr(13); + + int end_pos = preset_name.find_first_of("0."); + alias_name = preset_name.substr(0, end_pos-1); } else if (boost::starts_with(section.first, "printer:")) { presets = &this->printers; loaded = &loaded_printers; @@ -1283,6 +1299,14 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla loaded.is_system = true; loaded.vendor = vendor_profile; } + + // next step of an preset name aliasing + int end_pos = preset_name.find_first_of("@"); + if (end_pos != std::string::npos) + alias_name = preset_name.substr(0, end_pos - 1); + + loaded.alias = alias_name.empty() ? preset_name : alias_name; + ++ presets_loaded; } } @@ -1353,23 +1377,24 @@ void PresetBundle::update_multi_material_filament_presets() void PresetBundle::update_compatible(bool select_other_if_incompatible) { - const Preset &printer_preset = this->printers.get_edited_preset(); + const Preset &printer_preset = this->printers.get_edited_preset(); + const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); switch (printer_preset.printer_technology()) { case ptFFF: { assert(printer_preset.config.has("default_print_profile")); assert(printer_preset.config.has("default_filament_profile")); - const Preset &print_preset = this->prints.get_edited_preset(); const std::string &prefered_print_profile = printer_preset.config.opt_string("default_print_profile"); const std::vector<std::string> &prefered_filament_profiles = printer_preset.config.option<ConfigOptionStrings>("default_filament_profile")->values; prefered_print_profile.empty() ? - this->prints.update_compatible(printer_preset, nullptr, select_other_if_incompatible) : - this->prints.update_compatible(printer_preset, nullptr, select_other_if_incompatible, + this->prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_if_incompatible) : + this->prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_if_incompatible, [&prefered_print_profile](const std::string& profile_name) { return profile_name == prefered_print_profile; }); + const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); prefered_filament_profiles.empty() ? - this->filaments.update_compatible(printer_preset, &print_preset, select_other_if_incompatible) : - this->filaments.update_compatible(printer_preset, &print_preset, select_other_if_incompatible, + this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, select_other_if_incompatible) : + this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, select_other_if_incompatible, [&prefered_filament_profiles](const std::string& profile_name) { return std::find(prefered_filament_profiles.begin(), prefered_filament_profiles.end(), profile_name) != prefered_filament_profiles.end(); }); if (select_other_if_incompatible) { @@ -1401,16 +1426,16 @@ void PresetBundle::update_compatible(bool select_other_if_incompatible) { assert(printer_preset.config.has("default_sla_print_profile")); assert(printer_preset.config.has("default_sla_material_profile")); - const Preset &sla_print_preset = this->sla_prints.get_edited_preset(); - const std::string &prefered_sla_print_profile = printer_preset.config.opt_string("default_sla_print_profile"); + const PresetWithVendorProfile sla_print_preset_with_vendor_profile = this->sla_prints.get_edited_preset_with_vendor_profile(); + const std::string &prefered_sla_print_profile = printer_preset.config.opt_string("default_sla_print_profile"); (prefered_sla_print_profile.empty()) ? - this->sla_prints.update_compatible(printer_preset, nullptr, select_other_if_incompatible) : - this->sla_prints.update_compatible(printer_preset, nullptr, select_other_if_incompatible, + this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_if_incompatible) : + this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_if_incompatible, [&prefered_sla_print_profile](const std::string& profile_name){ return profile_name == prefered_sla_print_profile; }); const std::string &prefered_sla_material_profile = printer_preset.config.opt_string("default_sla_material_profile"); prefered_sla_material_profile.empty() ? - this->sla_materials.update_compatible(printer_preset, &sla_print_preset, select_other_if_incompatible) : - this->sla_materials.update_compatible(printer_preset, &sla_print_preset, select_other_if_incompatible, + this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, select_other_if_incompatible) : + this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, select_other_if_incompatible, [&prefered_sla_material_profile](const std::string& profile_name){ return profile_name == prefered_sla_material_profile; }); break; } @@ -1538,7 +1563,8 @@ void PresetBundle::update_platter_filament_ui(unsigned int idx_extruder, GUI::Pr // Fill in the list from scratch. ui->Freeze(); ui->Clear(); - size_t selected_preset_item = 0; + size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected + const Preset *selected_preset = this->filaments.find_preset(this->filament_presets[idx_extruder]); // Show wide icons if the currently selected preset is not compatible with the current printer, // and draw a red flag in front of the selected preset. @@ -1568,6 +1594,8 @@ void PresetBundle::update_platter_filament_ui(unsigned int idx_extruder, GUI::Pr // set a bitmap height to m_bitmapLock->GetHeight() const int icon_height = m_bitmapLock->GetHeight();//2 * icon_unit; //16 * scale_f + 0.5f; + wxString tooltip = ""; + for (int i = this->filaments().front().is_visible ? 0 : 1; i < int(this->filaments().size()); ++i) { const Preset &preset = this->filaments.preset(i); bool selected = this->filament_presets[idx_extruder] == preset.name; @@ -1608,18 +1636,25 @@ void PresetBundle::update_platter_filament_ui(unsigned int idx_extruder, GUI::Pr bitmap = m_bitmapCache->insert(bitmap_key, bmps); } - if (preset.is_default || preset.is_system) { - ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), + const std::string name = preset.alias.empty() ? preset.name : preset.alias; + if (preset.is_default || preset.is_system) { + ui->Append(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), (bitmap == 0) ? wxNullBitmap : *bitmap); - if (selected) + if (selected || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX ) { selected_preset_item = ui->GetCount() - 1; + tooltip = wxString::FromUTF8(preset.name.c_str()); + } } else { - nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), + nonsys_presets.emplace(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), (bitmap == 0) ? &wxNullBitmap : bitmap); - if (selected) - selected_str = wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); + if (selected) { + selected_str = wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); + tooltip = wxString::FromUTF8(preset.name.c_str()); + } } if (preset.is_default) ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); @@ -1630,15 +1665,25 @@ void PresetBundle::update_platter_filament_ui(unsigned int idx_extruder, GUI::Pr ui->set_label_marker(ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap)); for (std::map<wxString, wxBitmap*>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { ui->Append(it->first, *it->second); - if (it->first == selected_str) + if (it->first == selected_str || + // just in case: mark selected_preset_item as a first added element + selected_preset_item == INT_MAX) { selected_preset_item = ui->GetCount() - 1; + } } } ui->set_label_marker(ui->Append(PresetCollection::separator(L("Add/Remove filaments")), wxNullBitmap), GUI::PresetComboBox::LABEL_ITEM_WIZARD_FILAMENTS); + /* But, if selected_preset_item is still equal to INT_MAX, it means that + * there is no presets added to the list. + * So, select last combobox item ("Add/Remove filaments") + */ + if (selected_preset_item == INT_MAX) + selected_preset_item = ui->GetCount() - 1; + ui->SetSelection(selected_preset_item); - ui->SetToolTip(ui->GetString(selected_preset_item)); + ui->SetToolTip(tooltip.IsEmpty() ? ui->GetString(selected_preset_item) : tooltip); ui->check_selection(); ui->Thaw(); diff --git a/src/slic3r/GUI/PresetBundle.hpp b/src/slic3r/GUI/PresetBundle.hpp index b1010e07b..5205042c5 100644 --- a/src/slic3r/GUI/PresetBundle.hpp +++ b/src/slic3r/GUI/PresetBundle.hpp @@ -139,6 +139,8 @@ public: // If the "vendor" section is missing, enable all models and variants of the particular vendor. void load_installed_printers(const AppConfig &config); + const std::string& get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const; + static const char *PRUSA_BUNDLE; private: std::string load_system_presets(); diff --git a/src/slic3r/GUI/PresetHints.cpp b/src/slic3r/GUI/PresetHints.cpp index a02772c4d..f6281d7af 100644 --- a/src/slic3r/GUI/PresetHints.cpp +++ b/src/slic3r/GUI/PresetHints.cpp @@ -32,11 +32,11 @@ std::string PresetHints::cooling_description(const Preset &preset) % slowdown_below_layer_time % max_fan_speed % slowdown_below_layer_time % min_print_speed).str(); if (fan_below_layer_time > slowdown_below_layer_time) { - out += (boost::format(_utf8(L("\nIf estimated layer time is greater, but still below ~%1%s, " + out += "\n" + (boost::format(_utf8(L("If estimated layer time is greater, but still below ~%1%s, " "fan will run at a proportionally decreasing speed between %2%%% and %3%%%."))) % fan_below_layer_time % max_fan_speed % min_fan_speed).str(); } - out += _utf8(L("\nDuring the other layers, fan")) + " "; + out += "\n" + _utf8(L("During the other layers, fan")) + " "; } else { out = _utf8(L("Fan")) + " "; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c49cbea1e..c8239e0b7 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -998,6 +998,11 @@ void Tab::update_preset_description_line() default: break; } } + else + { + description_line += "\n\n\t" + _(L("full profile name")) + ": \n\t\t" + parent->name; + description_line += "\n\t" + _(L("symbolic profile name")) + ": \n\t\t" + parent->alias; + } } m_parent_preset_description_line->SetText(description_line, false); @@ -2750,7 +2755,7 @@ void Tab::select_preset(std::string preset_name, bool delete_current) PrinterTechnology printer_technology = m_preset_bundle->printers.get_edited_preset().printer_technology(); PresetCollection &dependent = (printer_technology == ptFFF) ? m_preset_bundle->filaments : m_preset_bundle->sla_materials; bool old_preset_dirty = dependent.current_is_dirty(); - bool new_preset_compatible = dependent.get_edited_preset().is_compatible_with_print(*m_presets->find_preset(preset_name, true)); + bool new_preset_compatible = is_compatible_with_print(dependent.get_edited_preset_with_vendor_profile(), m_presets->get_preset_with_vendor_profile(*m_presets->find_preset(preset_name, true))); if (! canceled) canceled = old_preset_dirty && ! new_preset_compatible && ! may_discard_current_dirty_preset(&dependent, preset_name); if (! canceled) { @@ -2768,6 +2773,7 @@ void Tab::select_preset(std::string preset_name, bool delete_current) // With the introduction of the SLA printer types, we need to support switching between // the FFF and SLA printers. const Preset &new_printer_preset = *m_presets->find_preset(preset_name, true); + const PresetWithVendorProfile new_printer_preset_with_vendor_profile = m_presets->get_preset_with_vendor_profile(new_printer_preset); PrinterTechnology old_printer_technology = m_presets->get_edited_preset().printer_technology(); PrinterTechnology new_printer_technology = new_printer_preset.printer_technology(); if (new_printer_technology == ptSLA && old_printer_technology == ptFFF && !may_switch_to_SLA_preset()) @@ -2788,7 +2794,7 @@ void Tab::select_preset(std::string preset_name, bool delete_current) }; for (PresetUpdate &pu : updates) { pu.old_preset_dirty = (old_printer_technology == pu.technology) && pu.presets->current_is_dirty(); - pu.new_preset_compatible = (new_printer_technology == pu.technology) && pu.presets->get_edited_preset().is_compatible_with_printer(new_printer_preset); + pu.new_preset_compatible = (new_printer_technology == pu.technology) && is_compatible_with_printer(pu.presets->get_edited_preset_with_vendor_profile(), new_printer_preset_with_vendor_profile); if (!canceled) canceled = pu.old_preset_dirty && !pu.new_preset_compatible && !may_discard_current_dirty_preset(pu.presets, preset_name); } @@ -2924,7 +2930,13 @@ void Tab::OnTreeSelChange(wxTreeEvent& event) #ifdef __linux__ std::unique_ptr<wxWindowUpdateLocker> no_updates(new wxWindowUpdateLocker(this)); #else -// wxWindowUpdateLocker noUpdates(this); + /* On Windows we use DoubleBuffering during rendering, + * so on Window is no needed to call a Freeze/Thaw functions. + * But under OSX (builds compiled with MacOSX10.14.sdk) wxStaticBitmap rendering is broken without Freeze/Thaw call. + */ +#ifdef __WXOSX__ + wxWindowUpdateLocker noUpdates(this); +#endif #endif if (m_pages.empty()) @@ -2985,7 +2997,8 @@ void Tab::save_preset(std::string name /*= ""*/) if (name.empty()) { const Preset &preset = m_presets->get_selected_preset(); auto default_name = preset.is_default ? "Untitled" : - preset.is_system ? (boost::format(_utf8(L("%1% - Copy"))) % preset.name).str() : +// preset.is_system ? (boost::format(_utf8(L("%1% - Copy"))) % preset.name).str() : + preset.is_system ? (boost::format(_CTX_utf8(L_CONTEXT("%1% - Copy", "PresetName"), "PresetName")) % preset.name).str() : preset.name; bool have_extention = boost::iends_with(default_name, ".ini"); @@ -3020,7 +3033,7 @@ void Tab::save_preset(std::string name /*= ""*/) show_error(this, _(L("Cannot overwrite an external profile."))); return; } - if (existing && name != preset.name) + if (existing/* && name != preset.name*/) { wxString msg_text = GUI::from_u8((boost::format(_utf8(L("Preset with name \"%1%\" already exist."))) % name).str()); msg_text += "\n" + _(L("Replace?")); diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index d2a5e185a..00e7725ac 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -156,8 +156,8 @@ MsgDataIncompatible::MsgDataIncompatible(const std::unordered_map<std::string, w "This probably happened as a result of running an older %s after using a newer one.\n\n" "You may either exit %s and try again with a newer version, or you may re-run the initial configuration. " - "Doing so will create a backup snapshot of the existing configuration before installing files compatible with this %s.\n" - )), SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); + "Doing so will create a backup snapshot of the existing configuration before installing files compatible with this %s.")) + "\n", + SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); text->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); content_sizer->Add(text); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index eb47fd208..302a5e521 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -11,6 +11,7 @@ #include <wx/statline.h> #include <wx/dcclient.h> #include <wx/numformatter.h> +#include <wx/colordlg.h> #include <boost/algorithm/string/replace.hpp> @@ -22,9 +23,11 @@ #include "I18N.hpp" #include "GUI_Utils.hpp" #include "PresetBundle.hpp" +#include "ExtruderSequenceDialog.hpp" #include "../Utils/MacDarkMode.hpp" using Slic3r::GUI::from_u8; +using Slic3r::GUI::into_u8; wxDEFINE_EVENT(wxCUSTOMEVT_TICKSCHANGED, wxEvent); wxDEFINE_EVENT(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED, wxCommandEvent); @@ -404,6 +407,23 @@ int em_unit(wxWindow* win) return Slic3r::GUI::wxGetApp().em_unit(); } +static float get_svg_scale_factor(wxWindow *win) +{ +#ifdef __APPLE__ + // Note: win->GetContentScaleFactor() is not used anymore here because it tends to + // return bogus results quite often (such as 1.0 on Retina or even 0.0). + // We're using the max scaling factor across all screens because it's very likely to be good enough. + + static float max_scaling_factor = NAN; + if (std::isnan(max_scaling_factor)) { + max_scaling_factor = Slic3r::GUI::mac_max_scaling_factor(); + } + return win != nullptr ? max_scaling_factor : 1.0f; +#else + return 1.0f; +#endif +} + // If an icon has horizontal orientation (width > height) call this function with is_horizontal = true wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name_in, const int px_cnt/* = 16*/, const bool is_horizontal /* = false*/, const bool grayscale/* = false*/) @@ -449,7 +469,7 @@ wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name_in, Slic3r::GUI::BitmapCache* m_bitmap_cache = nullptr; -/*static*/ std::vector<wxBitmap*> get_extruder_color_icons() +std::vector<wxBitmap*> get_extruder_color_icons(bool thin_icon/* = false*/) { // Create the bitmap with color bars. std::vector<wxBitmap*> bmps; @@ -465,16 +485,18 @@ Slic3r::GUI::BitmapCache* m_bitmap_cache = nullptr; * and scale them in respect to em_unit value */ const double em = Slic3r::GUI::wxGetApp().em_unit(); - const int icon_width = lround(3.2 * em); + const int icon_width = lround((thin_icon ? 1 : 3.2) * em); const int icon_height = lround(1.6 * em); for (const std::string& color : colors) { - wxBitmap* bitmap = m_bitmap_cache->find(color); + std::string bitmap_key = color + "-h" + std::to_string(icon_height) + "-w" + std::to_string(icon_width); + + wxBitmap* bitmap = m_bitmap_cache->find(bitmap_key); if (bitmap == nullptr) { // Paint the color icon. Slic3r::PresetBundle::parse_color(color, rgb); - bitmap = m_bitmap_cache->insert(color, m_bitmap_cache->mksolid(icon_width, icon_height, rgb)); + bitmap = m_bitmap_cache->insert(bitmap_key, m_bitmap_cache->mksolid(icon_width, icon_height, rgb)); } bmps.emplace_back(bitmap); } @@ -483,16 +505,62 @@ Slic3r::GUI::BitmapCache* m_bitmap_cache = nullptr; } -static wxBitmap get_extruder_color_icon(size_t extruder_idx) +static wxBitmap get_extruder_color_icon(size_t extruder_idx, bool thin_icon = false) { // Create the bitmap with color bars. - std::vector<wxBitmap*> bmps = get_extruder_color_icons(); + std::vector<wxBitmap*> bmps = get_extruder_color_icons(thin_icon); if (bmps.empty()) return wxNullBitmap; return *bmps[extruder_idx >= bmps.size() ? 0 : extruder_idx]; } +void apply_extruder_selector(wxBitmapComboBox** ctrl, + wxWindow* parent, + const std::string& first_item/* = ""*/, + wxPoint pos/* = wxDefaultPosition*/, + wxSize size/* = wxDefaultSize*/, + bool use_thin_icon/* = false*/) +{ + std::vector<wxBitmap*> icons = get_extruder_color_icons(use_thin_icon); + + if (!*ctrl) + *ctrl = new wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, pos, size, + 0, nullptr, wxCB_READONLY); + else + { + (*ctrl)->SetPosition(pos); + (*ctrl)->SetMinSize(size); + (*ctrl)->SetSize(size); + (*ctrl)->Clear(); + } + if (first_item.empty()) + (*ctrl)->Hide(); // to avoid unwanted rendering before layout (ExtruderSequenceDialog) + + if (icons.empty() && !first_item.empty()) { + (*ctrl)->Append(_(first_item), wxNullBitmap); + return; + } + + // For ObjectList we use short extruder name (just a number) + const bool use_full_item_name = dynamic_cast<Slic3r::GUI::ObjectList*>(parent) == nullptr; + + int i = 0; + wxString str = _(L("Extruder")); + for (wxBitmap* bmp : icons) { + if (i == 0) { + if (!first_item.empty()) + (*ctrl)->Append(_(first_item), *bmp); + ++i; + } + + (*ctrl)->Append(use_full_item_name ? wxString::Format("%s %d", str, i) : std::to_string(i), *bmp); + ++i; + } + (*ctrl)->SetSelection(0); +} + + // ***************************************************************************** // ---------------------------------------------------------------------------- // ObjectDataViewModelNode @@ -2230,26 +2298,31 @@ DoubleSlider::DoubleSlider( wxWindow *parent, if (!is_osx) SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX - m_bmp_thumb_higher = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "right_half_circle.png") : ScalableBitmap(this, "up_half_circle.png", 16, true)); - m_bmp_thumb_lower = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "left_half_circle.png" ) : ScalableBitmap(this, "down_half_circle.png", 16, true)); - m_thumb_size = m_bmp_thumb_lower.bmp().GetSize(); + const float scale_factor = get_svg_scale_factor(this); - m_bmp_add_tick_on = ScalableBitmap(this, "colorchange_add_on.png"); - m_bmp_add_tick_off = ScalableBitmap(this, "colorchange_add_off.png"); - m_bmp_del_tick_on = ScalableBitmap(this, "colorchange_delete_on.png"); - m_bmp_del_tick_off = ScalableBitmap(this, "colorchange_delete_off.png"); - m_tick_icon_dim = m_bmp_add_tick_on.bmp().GetSize().x; + m_bmp_thumb_higher = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "right_half_circle.png") : ScalableBitmap(this, "thumb_up")); + m_bmp_thumb_lower = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "left_half_circle.png" ) : ScalableBitmap(this, "thumb_down")); + m_thumb_size = m_bmp_thumb_lower.bmp().GetSize()*(1.0/scale_factor); - m_bmp_one_layer_lock_on = ScalableBitmap(this, "one_layer_lock_on.png"); - m_bmp_one_layer_lock_off = ScalableBitmap(this, "one_layer_lock_off.png"); - m_bmp_one_layer_unlock_on = ScalableBitmap(this, "one_layer_unlock_on.png"); - m_bmp_one_layer_unlock_off = ScalableBitmap(this, "one_layer_unlock_off.png"); - m_lock_icon_dim = m_bmp_one_layer_lock_on.bmp().GetSize().x; + m_bmp_add_tick_on = ScalableBitmap(this, "colorchange_add"); + m_bmp_add_tick_off = ScalableBitmap(this, "colorchange_add_f"); + m_bmp_del_tick_on = ScalableBitmap(this, "colorchange_del"); + m_bmp_del_tick_off = ScalableBitmap(this, "colorchange_del_f"); + m_tick_icon_dim = int((float)m_bmp_add_tick_on.bmp().GetSize().x / scale_factor); + + m_bmp_one_layer_lock_on = ScalableBitmap(this, "lock_closed"); + m_bmp_one_layer_lock_off = ScalableBitmap(this, "lock_closed_f"); + m_bmp_one_layer_unlock_on = ScalableBitmap(this, "lock_open"); + m_bmp_one_layer_unlock_off = ScalableBitmap(this, "lock_open_f"); + m_lock_icon_dim = int((float)m_bmp_one_layer_lock_on.bmp().GetSize().x / scale_factor); m_bmp_revert = ScalableBitmap(this, "undo"); - m_revert_icon_dim = m_bmp_revert.bmp().GetSize().x; + m_revert_icon_dim = int((float)m_bmp_revert.bmp().GetSize().x / scale_factor); + m_bmp_cog = ScalableBitmap(this, "cog"); + m_cog_icon_dim = int((float)m_bmp_cog.bmp().GetSize().x / scale_factor); m_selection = ssUndef; + m_pause_print_msg = _utf8(L("Place bearings in slots and resume")); // slider events Bind(wxEVT_PAINT, &DoubleSlider::OnPaint, this); @@ -2268,7 +2341,7 @@ DoubleSlider::DoubleSlider( wxWindow *parent, // control's view variables SLIDER_MARGIN = 4 + Slic3r::GUI::wxGetApp().em_unit(); - DARK_ORANGE_PEN = wxPen(wxColour(253, 84, 2)); + DARK_ORANGE_PEN = wxPen(wxColour(237, 107, 33)); ORANGE_PEN = wxPen(wxColour(253, 126, 66)); LIGHT_ORANGE_PEN = wxPen(wxColour(254, 177, 139)); @@ -2306,6 +2379,8 @@ void DoubleSlider::msw_rescale() m_bmp_revert.msw_rescale(); m_revert_icon_dim = m_bmp_revert.bmp().GetSize().x; + m_bmp_cog.msw_rescale(); + m_cog_icon_dim = m_bmp_cog.bmp().GetSize().x; SLIDER_MARGIN = 4 + Slic3r::GUI::wxGetApp().em_unit(); @@ -2453,41 +2528,45 @@ double DoubleSlider::get_double_value(const SelectedSlider& selection) return m_values[selection == ssLower ? m_lower_value : m_higher_value]; } -std::vector<double> DoubleSlider::GetTicksValues() const +using t_custom_code = Slic3r::Model::CustomGCode; +std::vector<t_custom_code> DoubleSlider::GetTicksValues() const { - std::vector<double> values; + std::vector<t_custom_code> values; const int val_size = m_values.size(); if (!m_values.empty()) - for (int tick : m_ticks) { - if (tick > val_size) + for (const TICK_CODE& tick : m_ticks_) { + if (tick.tick > val_size) break; - values.push_back(m_values[tick]); + values.push_back(t_custom_code(m_values[tick.tick], tick.gcode, tick.extruder, tick.color)); } return values; } -void DoubleSlider::SetTicksValues(const std::vector<double>& heights) +void DoubleSlider::SetTicksValues(const std::vector<t_custom_code>& heights) { if (m_values.empty()) return; - const bool was_empty = m_ticks.empty(); + const bool was_empty = m_ticks_.empty(); - m_ticks.clear(); + m_ticks_.clear(); for (auto h : heights) { - auto it = std::lower_bound(m_values.begin(), m_values.end(), h - epsilon()); + auto it = std::lower_bound(m_values.begin(), m_values.end(), h.height - epsilon()); if (it == m_values.end()) continue; - m_ticks.insert(it-m_values.begin()); + m_ticks_.insert(TICK_CODE(it-m_values.begin(), h.gcode, h.extruder, h.color)); } - if (!was_empty && m_ticks.empty()) + if (!was_empty && m_ticks_.empty()) // Switch to the "Feature type"/"Tool" from the very beginning of a new object slicing after deleting of the old one wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); + + Refresh(); + Update(); } void DoubleSlider::get_lower_and_higher_position(int& lower_pos, int& higher_pos) @@ -2538,17 +2617,20 @@ void DoubleSlider::render() // //higher slider: // draw_thumb(dc, higher_pos, ssHigher); - // draw both sliders - draw_thumbs(dc, lower_pos, higher_pos); - //draw color print ticks draw_ticks(dc); + // draw both sliders + draw_thumbs(dc, lower_pos, higher_pos); + //draw lock/unlock draw_one_layer_icon(dc); //draw revert bitmap (if it's shown) draw_revert_icon(dc); + + //draw cog bitmap (if it's shown) + draw_cog_icon(dc); } void DoubleSlider::draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoint pt_end) @@ -2560,7 +2642,7 @@ void DoubleSlider::draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoin return; wxBitmap* icon = m_is_action_icon_focesed ? &m_bmp_add_tick_off.bmp() : &m_bmp_add_tick_on.bmp(); - if (m_ticks.find(tick) != m_ticks.end()) + if (m_ticks_.find(tick) != m_ticks_.end()) icon = m_is_action_icon_focesed ? &m_bmp_del_tick_off.bmp() : &m_bmp_del_tick_on.bmp(); wxCoord x_draw, y_draw; @@ -2634,7 +2716,7 @@ void DoubleSlider::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedS } else { x_draw = pos.x - int(0.5*m_thumb_size.x); - y_draw = pos.y+1; + y_draw = pos.y - int(0.5*m_thumb_size.y); } } else{ @@ -2644,7 +2726,7 @@ void DoubleSlider::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedS } else { x_draw = pos.x - int(0.5*m_thumb_size.x); - y_draw = pos.y - m_thumb_size.y; + y_draw = pos.y - int(0.5*m_thumb_size.y); } } dc.DrawBitmap(selection == ssLower ? m_bmp_thumb_lower.bmp() : m_bmp_thumb_higher.bmp(), x_draw, y_draw); @@ -2703,14 +2785,26 @@ void DoubleSlider::draw_ticks(wxDC& dc) int height, width; get_size(&width, &height); const wxCoord mid = is_horizontal() ? 0.5*height : 0.5*width; - for (auto tick : m_ticks) + for (auto tick : m_ticks_) { - const wxCoord pos = get_position_from_value(tick); + const wxCoord pos = get_position_from_value(tick.tick); is_horizontal() ? dc.DrawLine(pos, mid-14, pos, mid-9) : dc.DrawLine(mid - 14, pos/* - 1*/, mid - 9, pos/* - 1*/); is_horizontal() ? dc.DrawLine(pos, mid+14, pos, mid+9) : dc.DrawLine(mid + 14, pos/* - 1*/, mid + 9, pos/* - 1*/); + + // Draw icon for "Pause print" or "Custom Gcode" + if (tick.gcode != Slic3r::ColorChangeCode && tick.gcode != Slic3r::ExtruderChangeCode) + { + wxBitmap icon = create_scaled_bitmap(this, tick.gcode == Slic3r::PausePrintCode ? "pause_print" : "edit_gcode"); + + wxCoord x_draw, y_draw; + is_horizontal() ? x_draw = pos - 0.5 * m_tick_icon_dim : y_draw = pos - 0.5 * m_tick_icon_dim; + is_horizontal() ? y_draw = mid + 22 : x_draw = mid + m_thumb_size.x + 3; + + dc.DrawBitmap(icon, x_draw, y_draw); + } } } @@ -2722,47 +2816,42 @@ void DoubleSlider::draw_colored_band(wxDC& dc) int height, width; get_size(&width, &height); - wxRect main_band = m_rect_lower_thumb; - if (is_horizontal()) { - main_band.SetLeft(SLIDER_MARGIN); - main_band.SetRight(width - SLIDER_MARGIN + 1); - } - else { - const int cut = 2; - main_band.x += cut; - main_band.width -= 2*cut; - main_band.SetTop(SLIDER_MARGIN); - main_band.SetBottom(height - SLIDER_MARGIN + 1); - } + const wxCoord mid = is_horizontal() ? 0.5 * height : 0.5 * width; - if (m_ticks.empty()) { - dc.SetPen(GetParent()->GetBackgroundColour()); - dc.SetBrush(GetParent()->GetBackgroundColour()); - dc.DrawRectangle(main_band); - return; - } + wxRect main_band = is_horizontal() ? + wxRect(SLIDER_MARGIN, lround(mid - 0.375 * m_thumb_size.y), + width - 2 * SLIDER_MARGIN + 1, lround(0.75 * m_thumb_size.y)) : + wxRect(lround(mid - 0.375 * m_thumb_size.x), SLIDER_MARGIN, + lround(0.75 * m_thumb_size.x), height - 2 * SLIDER_MARGIN + 1); - const std::vector<std::string>& colors = Slic3r::GCodePreviewData::ColorPrintColors(); - const size_t colors_cnt = colors.size(); + auto draw_band = [](wxDC& dc, const wxColour& clr, const wxRect& band_rc) { + dc.SetPen(clr); + dc.SetBrush(clr); + dc.DrawRectangle(band_rc); + }; + + const std::vector<std::string>& colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + int colors_cnt = colors.size(); - wxColour clr(colors[0]); - dc.SetPen(clr); - dc.SetBrush(clr); - dc.DrawRectangle(main_band); + const wxColour bg_clr = GetParent()->GetBackgroundColour(); + + wxColour clr = m_state == msSingleExtruder ? wxColour(colors[0]) : bg_clr; + draw_band(dc, clr, main_band); size_t i = 1; - for (auto tick : m_ticks) + for (auto tick : m_ticks_) { - if (i == colors_cnt) - i = 0; - const wxCoord pos = get_position_from_value(tick); - is_horizontal() ? main_band.SetLeft(SLIDER_MARGIN + pos) : - main_band.SetBottom(pos-1); + if ( (m_state == msSingleExtruder && tick.gcode != Slic3r::ColorChangeCode) || + (m_state == msMultiExtruder && tick.gcode != Slic3r::ExtruderChangeCode) ) + continue; - clr = wxColour(colors[i]); - dc.SetPen(clr); - dc.SetBrush(clr); - dc.DrawRectangle(main_band); + const wxCoord pos = get_position_from_value(tick.tick); + is_horizontal() ? main_band.SetLeft(SLIDER_MARGIN + pos) : + main_band.SetBottom(pos - 1); + + clr = (m_state == msMultiExtruder && tick.color.empty()) ? bg_clr : + m_state == msMultiExtruder ? wxColour(colors[std::min<int>(colors_cnt - 1, tick.extruder-1)]) : wxColour(tick.color); + draw_band(dc, clr, main_band); i++; } } @@ -2788,7 +2877,7 @@ void DoubleSlider::draw_one_layer_icon(wxDC& dc) void DoubleSlider::draw_revert_icon(wxDC& dc) { - if (m_ticks.empty() || !m_is_enabled_tick_manipulation) + if (m_ticks_.empty() || !m_is_enabled_tick_manipulation) return; int width, height; @@ -2804,9 +2893,27 @@ void DoubleSlider::draw_revert_icon(wxDC& dc) m_rect_revert_icon = wxRect(x_draw, y_draw, m_revert_icon_dim, m_revert_icon_dim); } +void DoubleSlider::draw_cog_icon(wxDC& dc) +{ + if (m_state != msMultiExtruder) + return; + + int width, height; + get_size(&width, &height); + + wxCoord x_draw, y_draw; + is_horizontal() ? x_draw = width-2 : x_draw = width - m_cog_icon_dim - 2; + is_horizontal() ? y_draw = height - m_cog_icon_dim - 2 : y_draw = height-2; + + dc.DrawBitmap(m_bmp_cog.bmp(), x_draw, y_draw); + + //update rect of the lock/unlock icon + m_rect_cog_icon = wxRect(x_draw, y_draw, m_cog_icon_dim, m_cog_icon_dim); +} + void DoubleSlider::update_thumb_rect(const wxCoord& begin_x, const wxCoord& begin_y, const SelectedSlider& selection) { - const wxRect& rect = wxRect(begin_x, begin_y, m_thumb_size.x, m_thumb_size.y); + const wxRect& rect = wxRect(begin_x, begin_y, m_thumb_size.x, int(m_thumb_size.y*0.5)); if (selection == ssLower) m_rect_lower_thumb = rect; else @@ -2840,16 +2947,16 @@ bool DoubleSlider::is_point_in_rect(const wxPoint& pt, const wxRect& rect) int DoubleSlider::is_point_near_tick(const wxPoint& pt) { - for (auto tick : m_ticks) { - const wxCoord pos = get_position_from_value(tick); + for (auto tick : m_ticks_) { + const wxCoord pos = get_position_from_value(tick.tick); if (is_horizontal()) { if (pos - 4 <= pt.x && pt.x <= pos + 4) - return tick; + return tick.tick; } else { if (pos - 4 <= pt.y && pt.y <= pos + 4) - return tick; + return tick.tick; } } return -1; @@ -2901,9 +3008,13 @@ void DoubleSlider::OnLeftDown(wxMouseEvent& event) m_selection == ssLower ? correct_lower_value() : correct_higher_value(); if (!m_selection) m_selection = ssHigher; - m_ticks.clear(); + m_ticks_.clear(); wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); } + else if (is_point_in_rect(pos, m_rect_cog_icon) && m_state == msMultiExtruder) { + // show dialog for set extruder sequence + m_edit_extruder_sequence = true; + } else detect_selected_slider(pos); @@ -2916,7 +3027,8 @@ void DoubleSlider::OnLeftDown(wxMouseEvent& event) get_value_from_position(pos.x, pos.y); if (mouse_val >= 0) { - if (abs(mouse_val - m_lower_value) < abs(mouse_val - m_higher_value)) { + // if (abs(mouse_val - m_lower_value) < abs(mouse_val - m_higher_value)) { + if ( mouse_val <= m_lower_value ) { SetLowerValue(mouse_val); correct_lower_value(); m_selection = ssLower; @@ -2956,6 +3068,38 @@ void DoubleSlider::correct_higher_value() m_lower_value = m_higher_value; } +wxString DoubleSlider::get_tooltip(IconFocus icon_focus) +{ + wxString tooltip(wxEmptyString); + if (m_is_one_layer_icon_focesed) + tooltip = _(L("One layer mode")); + + if (icon_focus == ifRevert) + tooltip = _(L("Discard all custom changes")); + if (icon_focus == ifCog) + tooltip = _(L("Set extruder sequence for whole print")); + else if (m_is_action_icon_focesed) + { + const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; + const auto tick_code_it = m_ticks_.find(tick); + tooltip = tick_code_it == m_ticks_.end() ? (m_state == msSingleExtruder ? + _(L("For add color change use left mouse button click")) : + _(L("For add change extruder use left mouse button click"))) + "\n" + + _(L("For add another code use right mouse button click")) : + tick_code_it->gcode == Slic3r::ColorChangeCode ? ( m_state == msSingleExtruder ? + _(L("For Delete color change use left mouse button click\n" + "For Edit color use right mouse button click")) : + from_u8((boost::format(_utf8(L("Delete color change for Extruder %1%"))) % tick_code_it->extruder).str()) ): +// tick_code_it->gcode == Slic3r::PausePrintCode ? _(L("Delete pause")) : + tick_code_it->gcode == Slic3r::ExtruderChangeCode ? + from_u8((boost::format(_utf8(L("Delete extruder change to \"%1%\""))) % tick_code_it->extruder).str()) : + from_u8((boost::format(_utf8(L("For Delete \"%1%\" code use left mouse button click\n" + "For Edit \"%1%\" code use right mouse button click"))) % tick_code_it->gcode ).str()); + } + + return tooltip; +} + void DoubleSlider::OnMotion(wxMouseEvent& event) { bool action = false; @@ -2964,11 +3108,14 @@ void DoubleSlider::OnMotion(wxMouseEvent& event) const wxPoint pos = event.GetLogicalPosition(dc); m_is_one_layer_icon_focesed = is_point_in_rect(pos, m_rect_one_layer_icon); - bool is_revert_icon_focused = false; + IconFocus icon_focus = ifNone; if (!m_is_left_down && !m_is_one_layer) { m_is_action_icon_focesed = is_point_in_rect(pos, m_rect_tick_action); - is_revert_icon_focused = !m_ticks.empty() && is_point_in_rect(pos, m_rect_revert_icon); + if (!m_ticks_.empty() && is_point_in_rect(pos, m_rect_revert_icon)) + icon_focus = ifRevert; + else if (is_point_in_rect(pos, m_rect_cog_icon)) + icon_focus = ifCog; } else if (m_is_left_down || m_is_right_down) { if (m_selection == ssLower) { @@ -2989,10 +3136,7 @@ void DoubleSlider::OnMotion(wxMouseEvent& event) event.Skip(); // Set tooltips with information for each icon - const wxString tooltip = m_is_one_layer_icon_focesed ? _(L("One layer mode")) : - m_is_action_icon_focesed ? _(L("Add/Del color change")) : - is_revert_icon_focused ? _(L("Discard all color changes")) : ""; - this->SetToolTip(tooltip); + this->SetToolTip(get_tooltip(icon_focus)); if (action) { @@ -3009,6 +3153,43 @@ void DoubleSlider::OnLeftUp(wxMouseEvent& event) return; this->ReleaseMouse(); m_is_left_down = false; + + if (m_show_context_menu) + { + if (m_state == msMultiExtruder) + { + wxMenu menu; + const int extruders_cnt = Slic3r::GUI::wxGetApp().extruders_edited_cnt(); + if (extruders_cnt > 1) + { + const int initial_extruder = get_extruder_for_tick(m_selection == ssLower ? m_lower_value : m_higher_value); + + wxMenu* change_extruder_menu = new wxMenu(); + + for (int i = 0; i <= extruders_cnt; i++) { + const wxString item_name = i == 0 ? _(L("Default")) : wxString::Format(_(L("Extruder %d")), i); + + append_menu_radio_item(change_extruder_menu, wxID_ANY, item_name, "", + [this, i](wxCommandEvent&) { change_extruder(i); }, &menu)->Check(i == initial_extruder); + } + + wxMenuItem* change_extruder_menu_item = menu.AppendSubMenu(change_extruder_menu, _(L("Change extruder")), _(L("Use another extruder"))); + change_extruder_menu_item->SetBitmap(create_scaled_bitmap(nullptr, "change_extruder")); + } + + Slic3r::GUI::wxGetApp().plater()->PopupMenu(&menu); + } + else + add_code(Slic3r::ColorChangeCode); + + m_show_context_menu = false; + } + + if (m_edit_extruder_sequence) { + edit_extruder_sequence(); + m_edit_extruder_sequence = false; + } + Refresh(); Update(); event.Skip(); @@ -3059,21 +3240,36 @@ void DoubleSlider::action_tick(const TicksAction action) const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; - if (action == taOnIcon) { - if (!m_ticks.insert(tick).second) - m_ticks.erase(tick); + const auto it = m_ticks_.find(tick); + + if (it != m_ticks_.end()) // erase this tick + { + if (action == taAdd) + return; + m_ticks_.erase(TICK_CODE(tick)); + + wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); + Refresh(); + Update(); + return; } - else { - const auto it = m_ticks.find(tick); - if (it == m_ticks.end() && action == taAdd) - m_ticks.insert(tick); - else if (it != m_ticks.end() && action == taDel) - m_ticks.erase(tick); + + if (action == taDel) + return; + if (action == taAdd) + { + // OnChar() is called immediately after OnKeyDown(), which can cause call of add_code() twice. + // To avoid this case we should suppress second add_code() call. + if (m_suppress_add_code) + return; + m_suppress_add_code = true; + if (m_state != msMultiExtruder) + add_code(Slic3r::ColorChangeCode); + m_suppress_add_code = false; + return; } - wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); - Refresh(); - Update(); + m_show_context_menu = true; } void DoubleSlider::OnWheel(wxMouseEvent& event) @@ -3148,6 +3344,27 @@ void DoubleSlider::OnRightDown(wxMouseEvent& event) this->CaptureMouse(); const wxClientDC dc(this); + + wxPoint pos = event.GetLogicalPosition(dc); + if (is_point_in_rect(pos, m_rect_tick_action) && m_is_enabled_tick_manipulation) + { + const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; + // if on this Z doesn't exist tick + auto it = m_ticks_.find(tick); + if (it == m_ticks_.end()) + { + // show context menu on OnRightUp() + m_show_context_menu = true; + return; + } + if (it->gcode != Slic3r::ExtruderChangeCode) + { + // show "Edit" and "Delete" menu on OnRightUp() + m_show_edit_menu = true; + return; + } + } + detect_selected_slider(event.GetLogicalPosition(dc)); if (!m_selection) return; @@ -3157,13 +3374,29 @@ void DoubleSlider::OnRightDown(wxMouseEvent& event) else m_lower_value = m_higher_value; - m_is_right_down = m_is_one_layer = true; + // set slider to "one layer" mode + m_is_right_down = m_is_one_layer = true; Refresh(); Update(); event.Skip(); } +int DoubleSlider::get_extruder_for_tick(int tick) +{ + if (m_ticks_.empty()) + return 0; + + auto it = m_ticks_.lower_bound(tick); + while (it != m_ticks_.begin()) { + --it; + if(it->gcode == Slic3r::ExtruderChangeCode) + return it->extruder; + } + + return 0; +} + void DoubleSlider::OnRightUp(wxMouseEvent& event) { if (!HasCapture()) @@ -3171,11 +3404,296 @@ void DoubleSlider::OnRightUp(wxMouseEvent& event) this->ReleaseMouse(); m_is_right_down = m_is_one_layer = false; + if (m_show_context_menu) { + wxMenu menu; + + if (m_state == msMultiExtruder) + { + const int extruders_cnt = Slic3r::GUI::wxGetApp().extruders_edited_cnt(); + if (extruders_cnt > 1) + { + const int initial_extruder = get_extruder_for_tick(m_selection == ssLower ? m_lower_value : m_higher_value); + + wxMenu* change_extruder_menu = new wxMenu(); + wxMenu* add_color_change_menu = new wxMenu(); + + for (int i = 0; i <= extruders_cnt; i++) { + const wxString item_name = i == 0 ? _(L("Default")) : wxString::Format(_(L("Extruder %d")), i); + + append_menu_radio_item(change_extruder_menu, wxID_ANY, item_name, "", + [this, i](wxCommandEvent&) { change_extruder(i); }, &menu)->Check(i == initial_extruder); + + if (i==0) // don't use M600 for default extruder, if multimaterial print is selected + continue; + append_menu_item(add_color_change_menu, wxID_ANY, item_name, "", + [this, i](wxCommandEvent&) { add_code(Slic3r::ColorChangeCode, i); }, "", &menu); + } + + wxMenuItem* change_extruder_menu_item = menu.AppendSubMenu(change_extruder_menu, _(L("Change extruder")), _(L("Use another extruder"))); + change_extruder_menu_item->SetBitmap(create_scaled_bitmap(nullptr, "change_extruder")); + + const wxString menu_name = from_u8((boost::format(_utf8(L("Add color change (%1%) for:"))) % Slic3r::ColorChangeCode).str()); + wxMenuItem* add_color_change_menu_item = menu.AppendSubMenu(add_color_change_menu, menu_name, ""); + add_color_change_menu_item->SetBitmap(create_scaled_bitmap(nullptr, "colorchange_add_m")); + } + } + else + append_menu_item(&menu, wxID_ANY, _(L("Add color change")) + " (M600)", "", + [this](wxCommandEvent&) { add_code(Slic3r::ColorChangeCode); }, "colorchange_add_m", &menu); + + append_menu_item(&menu, wxID_ANY, _(L("Add pause print")) + " (M601)", "", + [this](wxCommandEvent&) { add_code(Slic3r::PausePrintCode); }, "pause_print", &menu); + + append_menu_item(&menu, wxID_ANY, _(L("Add custom G-code")), "", + [this](wxCommandEvent&) { add_code(""); }, "edit_gcode", &menu); + + Slic3r::GUI::wxGetApp().plater()->PopupMenu(&menu); + + m_show_context_menu = false; + } + else if (m_show_edit_menu) { + wxMenu menu; + + std::set<TICK_CODE>::iterator it = m_ticks_.find(m_selection == ssLower ? m_lower_value : m_higher_value); + const bool is_color_change = it->gcode == Slic3r::ColorChangeCode; + + append_menu_item(&menu, wxID_ANY, it->gcode == Slic3r::ColorChangeCode ? _(L("Edit color")) : + it->gcode == Slic3r::PausePrintCode ? _(L("Edit pause print message")) : + _(L("Edit custom G-code")), "", + [this](wxCommandEvent&) { edit_tick(); }, "edit_uni", &menu); + + append_menu_item(&menu, wxID_ANY, it->gcode == Slic3r::ColorChangeCode ? _(L("Delete color change")) : + it->gcode == Slic3r::PausePrintCode ? _(L("Delete pause print")) : + _(L("Delete custom G-code")), "", + [this](wxCommandEvent&) { action_tick(taDel); }, "colorchange_del_f", &menu); + + Slic3r::GUI::wxGetApp().plater()->PopupMenu(&menu); + + m_show_edit_menu = false; + } + Refresh(); Update(); event.Skip(); } +static std::string get_new_color(const std::string& color) +{ + wxColour clr(color); + if (!clr.IsOk()) + clr = wxColour(0, 0, 0); // Don't set alfa to transparence + + auto data = new wxColourData(); + data->SetChooseFull(1); + data->SetColour(clr); + + wxColourDialog dialog(nullptr, data); + dialog.CenterOnParent(); + if (dialog.ShowModal() == wxID_OK) + return dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + return ""; +} + +static std::string get_custom_code(const std::string& code_in, double height) +{ + wxString msg_text = from_u8(_utf8(L("Enter custom G-code used on current layer"))) + ":"; + wxString msg_header = from_u8((boost::format(_utf8(L("Custom Gcode on current layer (%1% mm)."))) % height).str()); + + // get custom gcode + wxTextEntryDialog dlg(nullptr, msg_text, msg_header, code_in, + wxTextEntryDialogStyle | wxTE_MULTILINE); + if (dlg.ShowModal() != wxID_OK || dlg.GetValue().IsEmpty()) + return ""; + + return dlg.GetValue().ToStdString(); +} + +static std::string get_pause_print_msg(const std::string& msg_in, double height) +{ + wxString msg_text = from_u8(_utf8(L("Enter short message shown on Printer display during pause print"))) + ":"; + wxString msg_header = from_u8((boost::format(_utf8(L("Message for pause print on current layer (%1% mm)."))) % height).str()); + + // get custom gcode + wxTextEntryDialog dlg(nullptr, msg_text, msg_header, from_u8(msg_in), + wxTextEntryDialogStyle); + if (dlg.ShowModal() != wxID_OK || dlg.GetValue().IsEmpty()) + return ""; + + return into_u8(dlg.GetValue()); +} + +void DoubleSlider::add_code(std::string code, int selected_extruder/* = -1*/) +{ + const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; + // if on this Z doesn't exist tick + auto it = m_ticks_.find(tick); + if (it == m_ticks_.end()) + { + std::string color = ""; + if (code == Slic3r::ColorChangeCode) + { + std::vector<std::string> colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + + if (m_state == msSingleExtruder && !m_ticks_.empty()) { + auto before_tick_it = std::lower_bound(m_ticks_.begin(), m_ticks_.end(), tick); + while (before_tick_it != m_ticks_.begin()) { + --before_tick_it; + if (before_tick_it->gcode == Slic3r::ColorChangeCode) { + color = before_tick_it->color; + break; + } + } + + if (color.empty()) + color = colors[0]; + } + else + color = colors[selected_extruder > 0 ? selected_extruder-1 : 0]; + + color = get_new_color(color); + if (color.empty()) + return; + } + else if (code == Slic3r::PausePrintCode) + { + /* PausePrintCode doesn't need a color, so + * this field is used for save a short message shown on Printer display + * */ + color = get_pause_print_msg(m_pause_print_msg, m_values[tick]); + if (color.empty()) + return; + m_pause_print_msg = color; + } + else if (code.empty()) + { + code = get_custom_code(m_custom_gcode, m_values[tick]); + if (code.empty()) + return; + m_custom_gcode = code; + } + + int extruder = 1; + if (m_state == msMultiExtruder) { + if (code == Slic3r::ColorChangeCode && selected_extruder >= 0) + extruder = selected_extruder; + else + extruder = get_extruder_for_tick(m_selection == ssLower ? m_lower_value : m_higher_value); + } + + m_ticks_.insert(TICK_CODE(tick, code, extruder, color)); + + wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); + Refresh(); + Update(); + } +} + +void DoubleSlider::edit_tick() +{ + const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; + // if on this Z exists tick + std::set<TICK_CODE>::iterator it = m_ticks_.find(tick); + if (it != m_ticks_.end()) + { + std::string edited_value; + if (it->gcode == Slic3r::ColorChangeCode) + edited_value = get_new_color(it->color); + else if (it->gcode == Slic3r::PausePrintCode) + edited_value = get_pause_print_msg(it->color, m_values[it->tick]); + else + edited_value = get_custom_code(it->gcode, m_values[it->tick]); + + if (edited_value.empty()) + return; + + TICK_CODE changed_tick = *it; + if (it->gcode == Slic3r::ColorChangeCode || it->gcode == Slic3r::PausePrintCode) { + if (it->color == edited_value) + return; + changed_tick.color = edited_value; + } + else { + if (it->gcode == edited_value) + return; + changed_tick.gcode = edited_value; + } + + m_ticks_.erase(it); + m_ticks_.insert(changed_tick); + + wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); + } +} + +void DoubleSlider::change_extruder(int extruder) +{ + const int tick = m_selection == ssLower ? m_lower_value : m_higher_value; + + std::vector<std::string> colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + + // if on this Y doesn't exist tick + if (m_ticks_.find(tick) == m_ticks_.end()) + { + m_ticks_.insert(TICK_CODE(tick, Slic3r::ExtruderChangeCode, extruder, extruder == 0 ? "" : colors[extruder-1])); + + wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); + Refresh(); + Update(); + } +} + +void DoubleSlider::edit_extruder_sequence() +{ + Slic3r::GUI::ExtruderSequenceDialog dlg(m_extruders_sequence); + if (dlg.ShowModal() != wxID_OK) + return; + + const ExtrudersSequence& from_dlg_val = dlg.GetValue(); + if (m_extruders_sequence == from_dlg_val) + return; + + m_extruders_sequence = from_dlg_val; + + auto it = m_ticks_.begin(); + while (it != m_ticks_.end()) { + if (it->gcode == Slic3r::ExtruderChangeCode) + it = m_ticks_.erase(it); + else + ++it; + } + + int tick = 0; + double value = 0.0; + int extruder = 0; + const int extr_cnt = m_extruders_sequence.extruders.size(); + + std::vector<std::string> colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + + while (tick <= m_max_value) + { + int cur_extruder = m_extruders_sequence.extruders[extruder]; + m_ticks_.insert(TICK_CODE(tick, Slic3r::ExtruderChangeCode, cur_extruder + 1, colors[cur_extruder])); + + extruder++; + if (extruder == extr_cnt) + extruder = 0; + if (m_extruders_sequence.is_mm_intervals) + { + value += m_extruders_sequence.interval_by_mm; + auto it = std::lower_bound(m_values.begin(), m_values.end(), value - epsilon()); + + if (it == m_values.end()) + break; + + tick = it - m_values.begin(); + } + else + tick += m_extruders_sequence.interval_by_layers; + } + + wxPostEvent(this->GetParent(), wxCommandEvent(wxCUSTOMEVT_TICKSCHANGED)); +} + // ---------------------------------------------------------------------------- // LockButton diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index d4d5e7998..7841b62fe 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -16,6 +16,7 @@ #include <vector> #include <set> #include <functional> +#include "libslic3r/Model.hpp" namespace Slic3r { enum class ModelVolumeType : int; @@ -48,6 +49,8 @@ wxMenuItem* append_menu_check_item(wxMenu* menu, int id, const wxString& string, std::function<void(wxCommandEvent& event)> cb, wxEvtHandler* event_handler); class wxDialog; +class wxBitmapComboBox; + void edit_tooltip(wxString& tooltip); void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector<int>& btn_ids); int em_unit(wxWindow* win); @@ -55,7 +58,13 @@ int em_unit(wxWindow* win); wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name, const int px_cnt = 16, const bool is_horizontal = false, const bool grayscale = false); -std::vector<wxBitmap*> get_extruder_color_icons(); +std::vector<wxBitmap*> get_extruder_color_icons(bool thin_icon = false); +void apply_extruder_selector(wxBitmapComboBox** ctrl, + wxWindow* parent, + const std::string& first_item = "", + wxPoint pos = wxDefaultPosition, + wxSize size = wxDefaultSize, + bool use_thin_icon = false); class wxCheckListBoxComboPopup : public wxCheckListBox, public wxComboPopup { @@ -749,6 +758,11 @@ enum TicksAction{ class DoubleSlider : public wxControl { + enum IconFocus { + ifNone, + ifRevert, + ifCog + }; public: DoubleSlider( wxWindow *parent, @@ -794,8 +808,8 @@ public: m_values = values; } void ChangeOneLayerLock(); - std::vector<double> GetTicksValues() const; - void SetTicksValues(const std::vector<double>& heights); + std::vector<Slic3r::Model::CustomGCode> GetTicksValues() const; + void SetTicksValues(const std::vector<Slic3r::Model::CustomGCode> &heights); void EnableTickManipulation(bool enable = true) { m_is_enabled_tick_manipulation = enable; } @@ -803,6 +817,18 @@ public: EnableTickManipulation(false); } + enum ManipulationState { + msSingleExtruder, // single extruder printer preset is selected + msMultiExtruder // multiple extruder printer preset is selected, and "Whole print" is selected + }; + void SetManipulationState(ManipulationState state) { + m_state = state; + } + void SetManipulationState(int extruders_cnt) { + m_state = extruders_cnt ==1 ? msSingleExtruder : msMultiExtruder; + } + ManipulationState GetManipulationState() const { return m_state; } + bool is_horizontal() const { return m_style == wxSL_HORIZONTAL; } bool is_one_layer() const { return m_is_one_layer; } bool is_lower_at_min() const { return m_lower_value == m_min_value; } @@ -820,7 +846,12 @@ public: void OnKeyUp(wxKeyEvent &event); void OnChar(wxKeyEvent &event); void OnRightDown(wxMouseEvent& event); + int get_extruder_for_tick(int tick); void OnRightUp(wxMouseEvent& event); + void add_code(std::string code, int selected_extruder = -1); + void edit_tick(); + void change_extruder(int extruder); + void edit_extruder_sequence(); protected: @@ -834,6 +865,7 @@ protected: void draw_colored_band(wxDC& dc); void draw_one_layer_icon(wxDC& dc); void draw_revert_icon(wxDC& dc); + void draw_cog_icon(wxDC &dc); void draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection); void draw_info_line_with_icon(wxDC& dc, const wxPoint& pos, SelectedSlider selection); void draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) const; @@ -842,6 +874,7 @@ protected: void detect_selected_slider(const wxPoint& pt); void correct_lower_value(); void correct_higher_value(); + wxString get_tooltip(IconFocus icon_focus); void move_current_thumb(const bool condition); void action_tick(const TicksAction action); void enter_window(wxMouseEvent& event, const bool enter); @@ -876,6 +909,7 @@ private: ScalableBitmap m_bmp_one_layer_unlock_on; ScalableBitmap m_bmp_one_layer_unlock_off; ScalableBitmap m_bmp_revert; + ScalableBitmap m_bmp_cog; SelectedSlider m_selection; bool m_is_left_down = false; bool m_is_right_down = false; @@ -884,16 +918,25 @@ private: bool m_is_action_icon_focesed = false; bool m_is_one_layer_icon_focesed = false; bool m_is_enabled_tick_manipulation = true; + bool m_show_context_menu = false; + bool m_show_edit_menu = false; + bool m_edit_extruder_sequence = false; + bool m_suppress_add_code = false; + ManipulationState m_state = msSingleExtruder; + std::string m_custom_gcode = ""; + std::string m_pause_print_msg; wxRect m_rect_lower_thumb; wxRect m_rect_higher_thumb; wxRect m_rect_tick_action; wxRect m_rect_one_layer_icon; wxRect m_rect_revert_icon; + wxRect m_rect_cog_icon; wxSize m_thumb_size; int m_tick_icon_dim; int m_lock_icon_dim; int m_revert_icon_dim; + int m_cog_icon_dim; long m_style; float m_label_koef = 1.0; @@ -912,6 +955,88 @@ private: std::vector<wxPen*> m_segm_pens; std::set<int> m_ticks; std::vector<double> m_values; + + struct TICK_CODE + { + TICK_CODE(int tick):tick(tick), gcode(Slic3r::ColorChangeCode), extruder(0), color("") {} + TICK_CODE(int tick, const std::string& code) : + tick(tick), gcode(code), extruder(0) {} + TICK_CODE(int tick, int extruder) : + tick(tick), gcode(Slic3r::ColorChangeCode), extruder(extruder) {} + TICK_CODE(int tick, const std::string& code, int extruder, const std::string& color) : + tick(tick), gcode(code), extruder(extruder), color(color) {} + + bool operator<(const TICK_CODE& other) const { return other.tick > this->tick; } + bool operator>(const TICK_CODE& other) const { return other.tick < this->tick; } + TICK_CODE operator=(const TICK_CODE& other) const { + TICK_CODE ret_val(other.tick, other.gcode, other.extruder, other.color); + return ret_val; + } + + int tick; + std::string gcode; + int extruder; + std::string color; + }; + + std::set<TICK_CODE> m_ticks_; + +public: + struct ExtrudersSequence + { + bool is_mm_intervals; + double interval_by_mm; + int interval_by_layers; + std::vector<size_t> extruders; + + ExtrudersSequence() : + is_mm_intervals(true), + interval_by_mm(3.0), + interval_by_layers(10), + extruders({ 0 }) {} + + ExtrudersSequence(const ExtrudersSequence& other) : + is_mm_intervals(other.is_mm_intervals), + interval_by_mm(other.interval_by_mm), + interval_by_layers(other.interval_by_layers), + extruders(other.extruders) {} + + ExtrudersSequence& operator=(const ExtrudersSequence& other) { + this->is_mm_intervals = other.is_mm_intervals; + this->interval_by_mm = other.interval_by_mm; + this->interval_by_layers= other.interval_by_layers; + this->extruders = other.extruders; + + return *this; + } + bool operator==(const ExtrudersSequence& other) const + { + return (other.is_mm_intervals == this->is_mm_intervals ) && + (other.interval_by_mm == this->interval_by_mm ) && + (other.interval_by_layers == this->interval_by_layers ) && + (other.extruders == this->extruders ) ; + } + bool operator!=(const ExtrudersSequence& other) const + { + return (other.is_mm_intervals != this->is_mm_intervals ) && + (other.interval_by_mm != this->interval_by_mm ) && + (other.interval_by_layers != this->interval_by_layers ) && + (other.extruders != this->extruders ) ; + } + + void add_extruder(size_t pos) + { + extruders.insert(extruders.begin() + pos+1, size_t(0)); + } + + void delete_extruder(size_t pos) + { + if (extruders.size() == 1) + return;// last item can't be deleted + extruders.erase(extruders.begin() + pos); + } + } + m_extruders_sequence; }; diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index d955d6a7e..b7eb12e02 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -423,7 +423,7 @@ void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) wxMessageDialog dlg(nullptr, _(L("Model repaired successfully")), _(L("Model Repair by the Netfabb service")), wxICON_INFORMATION | wxOK_DEFAULT); dlg.ShowModal(); } else { - wxMessageDialog dlg(nullptr, _(L("Model repair failed: \n")) + _(progress.message), _(L("Model Repair by the Netfabb service")), wxICON_ERROR | wxOK_DEFAULT); + wxMessageDialog dlg(nullptr, _(L("Model repair failed:")) + " \n" + _(progress.message), _(L("Model Repair by the Netfabb service")), wxICON_ERROR | wxOK_DEFAULT); dlg.ShowModal(); } worker_thread.join(); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 3cebf2f89..55e3a5a73 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -73,6 +73,7 @@ struct Update std::string vendor; std::string changelog_url; + Update() {} Update(fs::path &&source, fs::path &&target, const Version &version, std::string vendor, std::string changelog_url) : source(std::move(source)) , target(std::move(target)) @@ -156,7 +157,7 @@ struct PresetUpdater::priv void sync_config(const VendorMap vendors); void check_install_indices() const; - Updates get_config_updates() const; + Updates get_config_updates(const Semver& old_slic3r_version) const; void perform_updates(Updates &&updates, bool snapshot = true) const; }; @@ -167,7 +168,9 @@ PresetUpdater::priv::priv() , cancel(false) { set_download_prefs(GUI::wxGetApp().app_config); + // Install indicies from resources. Only installs those that are either missing or older than in resources. check_install_indices(); + // Load indices from the cache directory. index_db = Index::load_db(); } @@ -273,6 +276,7 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) if (!enabled_config_update) { return; } // Donwload vendor preset bundles + // Over all indices from the cache directory: for (auto &index : index_db) { if (cancel) { return; } @@ -366,30 +370,33 @@ void PresetUpdater::priv::check_install_indices() const } } -// Generates a list of bundle updates that are to be performed -Updates PresetUpdater::priv::get_config_updates() const +// Generates a list of bundle updates that are to be performed. +// Version of slic3r that was running the last time and which was read out from PrusaSlicer.ini is provided +// as a parameter. +Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version) const { Updates updates; BOOST_LOG_TRIVIAL(info) << "Checking for cached configuration updates..."; + // Over all indices from the cache directory: for (const auto idx : index_db) { auto bundle_path = vendor_path / (idx.vendor() + ".ini"); auto bundle_path_idx = vendor_path / idx.path().filename(); if (! fs::exists(bundle_path)) { - BOOST_LOG_TRIVIAL(info) << "Bundle not present for index, skipping: " << idx.vendor(); + BOOST_LOG_TRIVIAL(info) << "Confing bundle not installed for vendor %1%, skipping: " << idx.vendor(); continue; } - // Perform a basic load and check the version + // Perform a basic load and check the version of the installed preset bundle. auto vp = VendorProfile::from_ini(bundle_path, false); // Getting a recommended version from the latest index, wich may have been downloaded // from the internet, or installed / updated from the installation resources. - const auto recommended = idx.recommended(); + auto recommended = idx.recommended(); if (recommended == idx.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index?") % idx.vendor(); + BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index? Giving up.") % idx.vendor(); // XXX: what should be done here? continue; } @@ -404,6 +411,7 @@ Updates PresetUpdater::priv::get_config_updates() const % recommended->config_version.to_string(); if (! ver_current_found) { + // Any published config shall be always found in the latest config index. auto message = (boost::format("Preset bundle `%1%` version not found in index: %2%") % idx.vendor() % vp.config_version.to_string()).str(); BOOST_LOG_TRIVIAL(error) << message; GUI::show_error(nullptr, GUI::from_u8(message)); @@ -411,11 +419,90 @@ Updates PresetUpdater::priv::get_config_updates() const } if (ver_current_found && !ver_current->is_current_slic3r_supported()) { + // "Reconfigure" situation. BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string(); updates.incompats.emplace_back(std::move(bundle_path), *ver_current, vp.name); - } else if (recommended->config_version > vp.config_version) { - // Config bundle update situation + continue; + } + + if (recommended->config_version < vp.config_version) { + BOOST_LOG_TRIVIAL(warning) << (boost::format("Recommended config version for the currently running PrusaSlicer is older than the currently installed config for vendor %1%. This should not happen.") % idx.vendor()).str(); + continue; + } + + if (recommended->config_version == vp.config_version) { + // The recommended config bundle is already installed. + continue; + } + + // Config bundle update situation. The recommended config bundle version for this PrusaSlicer version from the index from the cache is newer + // than the version of the currently installed config bundle. + + // The config index inside the cache directory (given by idx.path()) is one of the following: + // 1) The last config index downloaded by any previously running PrusaSlicer instance + // 2) The last config index installed by any previously running PrusaSlicer instance (older or newer) from its resources. + // 3) The last config index installed by the currently running PrusaSlicer instance from its resources. + // The config index is always the newest one (given by its newest config bundle referenced), and older config indices shall fully contain + // the content of the older config indices. + + // Config bundle inside the cache directory. + fs::path path_in_cache = cache_path / (idx.vendor() + ".ini"); + // Config bundle inside the resources directory. + fs::path path_in_rsrc = rsrc_path / (idx.vendor() + ".ini"); + // Config index inside the resources directory. + fs::path path_idx_in_rsrc = rsrc_path / (idx.vendor() + ".idx"); + + // Search for a valid config bundle in the cache directory. + bool found = false; + Update new_update; + fs::path bundle_path_idx_to_install; + if (fs::exists(path_in_cache)) { + try { + VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, false); + if (new_vp.config_version == recommended->config_version) { + // The config bundle from the cache directory matches the recommended version of the index from the cache directory. + // This is the newest known recommended config. Use it. + new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url); + // and install the config index from the cache into vendor's directory. + bundle_path_idx_to_install = idx.path(); + found = true; + } + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(info) << boost::format("Failed to load the config bundle `%1%`: %2%") % path_in_cache.string() % ex.what(); + } + } + + // Keep the rsrc_idx outside of the next block, as we will reference the "recommended" version by an iterator. + Index rsrc_idx; + if (! found && fs::exists(path_in_rsrc) && fs::exists(path_idx_in_rsrc)) { + // Trying the config bundle from resources (from the installation). + // In that case, the recommended version number has to be compared against the recommended version reported by the config index from resources as well, + // as the config index in the cache directory may already be newer, recommending a newer config bundle than available in cache or resources. + VendorProfile rsrc_vp; + try { + rsrc_vp = VendorProfile::from_ini(path_in_rsrc, false); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(info) << boost::format("Cannot load the config bundle at `%1%`: %2%") % path_in_rsrc.string() % ex.what(); + } + if (rsrc_vp.valid()) { + try { + rsrc_idx.load(path_idx_in_rsrc); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(info) << boost::format("Cannot load the config index at `%1%`: %2%") % path_idx_in_rsrc.string() % ex.what(); + } + recommended = rsrc_idx.recommended(); + if (recommended != rsrc_idx.end() && recommended->config_version == rsrc_vp.config_version && recommended->config_version > vp.config_version) { + new_update = Update(std::move(path_in_rsrc), std::move(bundle_path), *recommended, vp.name, vp.changelog_url); + bundle_path_idx_to_install = path_idx_in_rsrc; + found = true; + } else { + BOOST_LOG_TRIVIAL(warning) << (boost::format("The recommended config version for vendor `%1%` in resources does not match the recommended\n" + " config version for this version of PrusaSlicer. Corrupted installation?") % idx.vendor()).str(); + } + } + } + if (found) { // Load 'installed' idx, if any. // 'Installed' indices are kept alongside the bundle in the `vendor` subdir // for bookkeeping to remember a cancelled update and not offer it again. @@ -423,15 +510,16 @@ Updates PresetUpdater::priv::get_config_updates() const Index existing_idx; try { existing_idx.load(bundle_path_idx); - - const auto existing_recommended = existing_idx.recommended(); + // Find a recommended config bundle version for the slic3r version last executed. This makes sure that a config bundle update will not be missed + // when upgrading an application. On the other side, the user will be bugged every time he will switch between slic3r versions. + const auto existing_recommended = existing_idx.recommended(old_slic3r_version); if (existing_recommended != existing_idx.end() && recommended->config_version == existing_recommended->config_version) { // The user has already seen (and presumably rejected) this update BOOST_LOG_TRIVIAL(info) << boost::format("Downloaded index for `%1%` is the same as installed one, not offering an update.") % idx.vendor(); continue; } } catch (const std::exception &err) { - BOOST_LOG_TRIVIAL(error) << boost::format("Could not load installed index at `%1%`: %2%") % bundle_path_idx % err.what(); + BOOST_LOG_TRIVIAL(error) << boost::format("Cannot load the installed index at `%1%`: %2%") % bundle_path_idx % err.what(); } } @@ -445,41 +533,14 @@ Updates PresetUpdater::priv::get_config_updates() const continue; } - auto path_src = cache_path / (idx.vendor() + ".ini"); - auto path_in_rsrc = rsrc_path / (idx.vendor() + ".ini"); - if (! fs::exists(path_src)) { - if (! fs::exists(path_in_rsrc)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update, but bundle found in neither cache nor resources") - % idx.vendor(); - continue; - } else { - path_src = std::move(path_in_rsrc); - path_in_rsrc.clear(); - } - } - - auto new_vp = VendorProfile::from_ini(path_src, false); - bool found = false; - if (new_vp.config_version == recommended->config_version) { - updates.updates.emplace_back(std::move(path_src), std::move(bundle_path), *recommended, vp.name, vp.changelog_url); - found = true; - } else if (! path_in_rsrc.empty() && fs::exists(path_in_rsrc)) { - new_vp = VendorProfile::from_ini(path_in_rsrc, false); - if (new_vp.config_version == recommended->config_version) { - updates.updates.emplace_back(std::move(path_in_rsrc), std::move(bundle_path), *recommended, vp.name, vp.changelog_url); - found = true; - } - } - - if (found) { - // 'Install' the index in the vendor directory. This is used to memoize - // offered updates and to not offer the same update again if it was cancelled by the user. - copy_file_fix(idx.path(), bundle_path_idx); - } else { - BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update (%2%) but the new bundle was found neither in cache nor resources") - % idx.vendor() - % recommended->config_version.to_string(); - } + updates.updates.emplace_back(std::move(new_update)); + // 'Install' the index in the vendor directory. This is used to memoize + // offered updates and to not offer the same update again if it was cancelled by the user. + copy_file_fix(bundle_path_idx_to_install, bundle_path_idx); + } else { + BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update (%2%) but the new bundle was found neither in cache nor resources") + % idx.vendor() + % recommended->config_version.to_string(); } } @@ -607,11 +668,11 @@ void PresetUpdater::slic3r_update_notify() } } -PresetUpdater::UpdateResult PresetUpdater::config_update() const +PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver &old_slic3r_version) const { if (! p->enabled_config_update) { return R_NOOP; } - auto updates = p->get_config_updates(); + auto updates = p->get_config_updates(old_slic3r_version); if (updates.incompats.size() > 0) { BOOST_LOG_TRIVIAL(info) << boost::format("%1% bundles incompatible. Asking for action...") % updates.incompats.size(); diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 7c2aab7cc..e18695828 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -11,6 +11,7 @@ namespace Slic3r { class AppConfig; class PresetBundle; +class Semver; class PresetUpdater { @@ -38,7 +39,9 @@ public: // If updating is enabled, check if updates are available in cache, if so, ask about installation. // A false return value implies Slic3r should exit due to incompatibility of configuration. - UpdateResult config_update() const; + // Providing old slic3r version upgrade profiles on upgrade of an application even in case + // that the config index installed from the Internet is equal to the index contained in the installation package. + UpdateResult config_update(const Semver &old_slic3r_version) const; // "Update" a list of bundles from resources (behaves like an online update). void install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot = true) const; |