Skip to content

Commit

Permalink
Update to support klipper thumbnails
Browse files Browse the repository at this point in the history
  • Loading branch information
Molodos committed Aug 29, 2023
1 parent 3fae882 commit 8269976
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 31 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Additional features:
- Display more information about the sliced model on the thumbnail
- Use a graphical user interface for thumbnail configuration
- Thumbnail preview in graphical user interface
- Add thumbnails for Klipper (to display in the Neptune 4 (Pro) web interface)

> **Note:** If you have some idea on how to improve the plugin or found a bug, feel free to create
> a [GitHub issue](https://github.com/Molodos/ElegooNeptuneThumbnails/issues/new/choose) for that
Expand Down Expand Up @@ -152,6 +153,8 @@ No personal data is being collected. The statistics data, that is collected, is
- The information display options you are using (e.g. `Layer Height`)
- Version of Cura
- Operating system (e.g. `Windows 10.0.22621`)
- Enabled state of thumbnails
- Enabled state of Klipper thumbnails

> **Note:** This list might change at some time, so keep an eye on it if you update the plugin. Be aware, that personal
> data will never be added to this list.
Expand Down
4 changes: 4 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[3.2.0]
- Added option to add klipper thumbnails
- Minor other fixes

[3.1.0]
- Now officially sponsored by Elegoo
- Added Neptune 4 printer
Expand Down
24 changes: 18 additions & 6 deletions elegoo_neptune_thumbnails.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def add_snapshot_to_gcode(self, output_device) -> None:
StatisticsSender.send_statistics()

# Cancel if thumbnail is disabled
if not SettingsManager.get_settings().thumbnails_enabled:
if not SettingsManager.get_settings().thumbnails_enabled and not SettingsManager.get_settings().klipper_thumbnails_enabled:
return

# Return if there is no G-code
Expand Down Expand Up @@ -99,7 +99,9 @@ def add_snapshot_to_gcode(self, output_device) -> None:
params_needed.remove(param_needed)

# Check if thumbnail is already present
if ';gimage:' in g_code or ';simage:' in g_code:
if SettingsManager.get_settings().thumbnails_enabled and (";gimage:" in g_code or ";simage:" in g_code):
thumbnail_segments.append(i)
elif SettingsManager.get_settings().klipper_thumbnails_enabled and "; thumbnail begin " in g_code:
thumbnail_segments.append(i)

# Remove thumbnail parts from gcode
Expand Down Expand Up @@ -144,8 +146,18 @@ def add_snapshot_to_gcode(self, output_device) -> None:
model_height=float(g_code_params["maxz"]),
filament_cost=material_cost)

# Add encoded snapshot image (simage and gimage)
thumbnail_prefix: str = ThumbnailGenerator.generate_gcode_prefix(slice_data=slice_data)
# Clear gcode
self.scene.gcode_dict[0] = []

# Add thumbnail if enabled
if SettingsManager.get_settings().thumbnails_enabled:
thumbnail_prefix: str = ThumbnailGenerator.generate_gcode_prefix(slice_data=slice_data)
self.scene.gcode_dict[0].append(thumbnail_prefix)

# Add klipper thumbnails if enabled
if SettingsManager.get_settings().klipper_thumbnails_enabled:
klipper_thumbnail_prefix: str = ThumbnailGenerator.generate_klipper_thumbnail_gcode(slice_data=slice_data)
self.scene.gcode_dict[0].append(klipper_thumbnail_prefix)

# Add image G-code to the beginning of the G-code
self.scene.gcode_dict[0] = [thumbnail_prefix] + g_code_segments
# Add original gcode
self.scene.gcode_dict[0] += g_code_segments
2 changes: 1 addition & 1 deletion package_plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"display_name": "Elegoo Neptune Thumbnails",
"package_id": "ElegooNeptune3Thumbnails",
"package_type": "plugin",
"package_version": "3.1.0",
"package_version": "3.2.0",
"sdk_version": 8,
"sdk_version_semver": "8.0.0",
"website": "https://github.com/Molodos/ElegooNeptuneThumbnails"
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Elegoo Neptune Thumbnails",
"id": "neptune_thumbnails",
"author": "Molodos",
"version": "3.1.0",
"version": "3.2.0",
"description": "Adds Elegoo Neptune printer thumbnail to G-code. Contains options to display parameters like estimated print time or model height on the thumbnail",
"supported_sdk_versions": ["8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"],
"api": 8
Expand Down
16 changes: 16 additions & 0 deletions tools/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def update_gui(self) -> None:
if self._popup:
self._popup.findChild(QQuickItem, "thumbnailsEnabled") \
.setProperty("checked", SettingsManager.get_settings().thumbnails_enabled)
self._popup.findChild(QQuickItem, "klipperThumbnailsEnabled") \
.setProperty("checked", SettingsManager.get_settings().klipper_thumbnails_enabled)
self._popup.findChild(QQuickItem, "printerModel") \
.setProperty("currentIndex", SettingsManager.get_settings().printer_model)
for i, v in enumerate(SettingsManager.get_settings().corner_options):
Expand Down Expand Up @@ -81,6 +83,20 @@ def set_thumbnails_enabled(self, enabled: bool) -> None:
# Update preview
self.render_thumbnail()

# Klipper thumbnails enabled state

@pyqtProperty(bool)
def klipper_thumbnails_enabled(self) -> bool:
return SettingsManager.get_settings().klipper_thumbnails_enabled

@pyqtSlot(bool)
def set_klipper_thumbnails_enabled(self, enabled: bool) -> None:
updated: bool = SettingsManager.get_settings().klipper_thumbnails_enabled != enabled
SettingsManager.get_settings().klipper_thumbnails_enabled = enabled
if updated:
# Update preview
self.render_thumbnail()

# Printer dropdown

@pyqtProperty(list) # List must be untyped!
Expand Down
20 changes: 19 additions & 1 deletion tools/gui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Window
{
id: popupWindow
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height / 11 * 10)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
maximumWidth: minimumWidth
maximumHeight: minimumHeight
modality: Qt.ApplicationModal
Expand Down Expand Up @@ -67,6 +67,24 @@ Window
}
}

// Settings item: Enable klipper checkbox
RowLayout
{
spacing: UM.Theme.getSize("wide_margin").width
width: parent.width

// Checkbox
UM.CheckBox
{
id: klipperThumbnailsEnabled
objectName: "klipperThumbnailsEnabled"
checked: settings.klipper_thumbnails_enabled
onClicked: settings.set_klipper_thumbnails_enabled(klipperThumbnailsEnabled.checked)
text: "Enable Klipper thumbnails"
tooltip: "Klipper thumbnails will be added when saving a G-code file"
}
}

// Settings item: Printer model
RowLayout
{
Expand Down
27 changes: 16 additions & 11 deletions tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ def __init__(self, statistics_id: str, plugin_json: dict[str, Any]):
# Define config
self.thumbnails_enabled: bool = True
self.printer_model: int = list(self.PRINTER_MODELS.keys()).index("elegoo_neptune_3_pro")
self.corner_options: list[int] = [0, 0, 3, 1]
self.corner_options: list[int] = [1, 4, 3, 5]
self.statistics_enabled: bool = True
self.use_current_model: bool = False
self.klipper_thumbnails_enabled: bool = False

def get_printer_model_id(self) -> str:
"""
Expand Down Expand Up @@ -79,9 +80,10 @@ def _set_corner_option_ids(self, corner_option_ids: list[str]) -> None:
"""
Set corner options from ids
"""
option_ids: list[str] = list(self.OPTIONS.keys())
for i, option_id in enumerate(corner_option_ids):
self.corner_options[i] = option_ids.index(option_id)
if corner_option_ids:
option_ids: list[str] = list(self.OPTIONS.keys())
for i, option_id in enumerate(corner_option_ids):
self.corner_options[i] = option_ids.index(option_id)

def is_old_thumbnail(self) -> bool:
"""
Expand All @@ -94,11 +96,12 @@ def load_json(self, data: dict[str, Any]) -> None:
"""
Load from json
"""
self.thumbnails_enabled = data["thumbnails_enabled"]
self._set_printer_model_id(data["printer_model"])
self._set_corner_option_ids(data["corner_options"])
self.statistics_enabled = data["statistics_enabled"]
self.use_current_model = data["use_current_model"]
self.thumbnails_enabled = data.get("thumbnails_enabled", True)
self._set_printer_model_id(data.get("printer_model", "elegoo_neptune_3_pro"))
self._set_corner_option_ids(data.get("corner_options", None))
self.statistics_enabled = data.get("statistics_enabled", True)
self.use_current_model = data.get("use_current_model", False)
self.klipper_thumbnails_enabled = data.get("klipper_thumbnails_enabled", False)

def to_json(self) -> dict[str, Any]:
"""
Expand All @@ -109,7 +112,8 @@ def to_json(self) -> dict[str, Any]:
"printer_model": self.get_printer_model_id(),
"corner_options": self.get_corner_option_ids(),
"statistics_enabled": self.statistics_enabled,
"use_current_model": self.use_current_model
"use_current_model": self.use_current_model,
"klipper_thumbnails_enabled": self.klipper_thumbnails_enabled
}


Expand Down Expand Up @@ -152,9 +156,10 @@ def load(cls) -> None:
cls._settings.thumbnails_enabled = True
# Neptune 3 Pro is most probable
cls._settings.printer_model = list(Settings.PRINTER_MODELS.keys()).index("elegoo_neptune_3_pro")
cls._settings.corner_options = [0, 0, 3, 1]
cls._settings.corner_options = [1, 4, 3, 5]
cls._settings.statistics_enabled = True
cls._settings.use_current_model = False
cls._settings.klipper_thumbnails_enabled = False

# Try to recognize current printer model
printer_id: str = Application.getInstance().getMachineManager().activeMachine.definition.getId()
Expand Down
4 changes: 3 additions & 1 deletion tools/statistics_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def send_statistics(cls) -> None:
"options": SettingsManager.get_settings().get_corner_option_ids(),
"selected_printer": SettingsManager.get_settings().get_printer_model_id(),
"cura_version": SettingsManager.get_settings().cura_version,
"os": f"{platform.system()} {platform.version()}"
"os": f"{platform.system()} {platform.version()}",
"thumbnails_enabled": SettingsManager.get_settings().thumbnails_enabled,
"klipper_thumbnails_enabled": SettingsManager.get_settings().klipper_thumbnails_enabled
}

# Send statistics
Expand Down
48 changes: 38 additions & 10 deletions tools/thumbnail_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ctypes import CDLL
from os import path

from PyQt6.QtCore import Qt
from PyQt6.QtCore import Qt, QByteArray, QBuffer, QIODeviceBase
from PyQt6.QtGui import QImage, QPainter, QColor, QFont

from UM.Application import Application
Expand Down Expand Up @@ -36,6 +36,7 @@ class ThumbnailGenerator:
Thumbnail generator
"""

KLIPPER_THUMBNAIL_BLOCK_SIZE: int = 78
COLORS: dict[str, QColor] = {
"green": QColor(34, 236, 128),
"red": QColor(209, 76, 81),
Expand Down Expand Up @@ -83,20 +84,47 @@ def generate_gcode_prefix(cls, slice_data: SliceData) -> str:
return gcode_prefix

@classmethod
def _render_thumbnail(cls, slice_data: SliceData, is_preview: bool = True) -> QImage:
def generate_klipper_thumbnail_gcode(cls, slice_data: SliceData) -> str:
"""
Generate klipper thumbnail gcode for thumbnails in sizes 32x32 and 300x300
"""
small_icon: QImage = Snapshot.snapshot(width=32, height=32)
big_icon: QImage = cls._render_thumbnail(slice_data=slice_data, is_preview=False, add_background=False)
big_icon = big_icon.scaled(300, 300)
g_code: str = "\r"
for icon in [small_icon, big_icon]:
byte_array: QByteArray = QByteArray()
byte_buffer: QBuffer = QBuffer(byte_array)
byte_buffer.open(QIODeviceBase.OpenModeFlag.WriteOnly)
icon.save(byte_buffer, "PNG")
base64_string: str = str(byte_array.toBase64().data(), "UTF-8")
g_code += f"; thumbnail begin {icon.width()} {icon.height()} {len(base64_string)}\r"
while base64_string:
g_code += f"; {base64_string[0:cls.KLIPPER_THUMBNAIL_BLOCK_SIZE]}\r"
base64_string = base64_string[cls.KLIPPER_THUMBNAIL_BLOCK_SIZE:]
g_code += "; thumbnail end\r\r"
return g_code

@classmethod
def _render_thumbnail(cls, slice_data: SliceData, is_preview: bool = True, add_background: bool = True) -> QImage:
"""
Renders a thumbnail based on settings
"""
# Create background
background: QImage
if SettingsManager.get_settings().is_old_thumbnail():
background = QImage(cls.BACKGROUND_OLD_PATH)
else:
background = QImage(cls.BACKGROUND_NEW_PATH)
background: QImage = QImage(900, 900, QImage.Format.Format_RGBA8888)
if add_background:
if SettingsManager.get_settings().is_old_thumbnail():
painter = QPainter(background)
painter.drawImage(0, 0, QImage(cls.BACKGROUND_OLD_PATH))
painter.end()
else:
painter = QPainter(background)
painter.drawImage(0, 0, QImage(cls.BACKGROUND_NEW_PATH))
painter.end()

# Create foreground
foreground: QImage
if not SettingsManager.get_settings().thumbnails_enabled:
if not SettingsManager.get_settings().thumbnails_enabled and not SettingsManager.get_settings().klipper_thumbnails_enabled:
foreground = QImage(cls.NO_FOREGROUND_IMAGE_PATH)
elif SettingsManager.get_settings().use_current_model or not is_preview:
foreground = Snapshot.snapshot(width=600, height=600)
Expand All @@ -110,15 +138,15 @@ def _render_thumbnail(cls, slice_data: SliceData, is_preview: bool = True) -> QI
painter.end()

# Don't add options if thumbnails disabled
if not SettingsManager.get_settings().thumbnails_enabled:
if not SettingsManager.get_settings().thumbnails_enabled and not SettingsManager.get_settings().klipper_thumbnails_enabled:
return background

# Generate option lines
lines: list[str] = cls._generate_option_lines(slice_data=slice_data)

# Add options
painter = QPainter(background)
font = QFont("Arial", 30)
font = QFont("Arial", 60)
painter.setFont(font)
painter.setPen(cls.COLORS["own_gray"])
for i, line in enumerate(lines):
Expand Down

0 comments on commit 8269976

Please sign in to comment.