From 5c56711c5777393b7df4734f75debab6d9211f42 Mon Sep 17 00:00:00 2001 From: RunLit <41996199+RunLit@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:30:27 +1100 Subject: [PATCH] Color base add support for transparent color (#5) * added db model is transparent and fixed PETG translucent showing black * js and filament form for transparent color * bumped version to v0.1.2 --- bambu_run/forms.py | 11 +- .../management/commands/bambu_collector.py | 5 +- .../commands/bambu_import_colors.py | 9 +- .../0002_filament_is_transparent.py | 27 +++ bambu_run/models.py | 8 + .../static/bambu_run/js/filament_form.js | 156 ++++++++++++++++++ .../static/bambu_run/js/printer_charts.js | 14 +- .../bambu_run/filament_color_list.html | 8 + .../templates/bambu_run/filament_detail.html | 6 +- .../templates/bambu_run/filament_form.html | 115 +++---------- .../templates/bambu_run/filament_list.html | 4 + .../bambu_run/printer_dashboard.html | 2 +- bambu_run/utils.py | 15 +- bambu_run/views.py | 11 ++ docs/Bambu_Color_Catalog/PETG Translucent.txt | 1 + pyproject.toml | 2 +- 16 files changed, 286 insertions(+), 108 deletions(-) create mode 100644 bambu_run/migrations/0002_filament_is_transparent.py create mode 100644 bambu_run/static/bambu_run/js/filament_form.js diff --git a/bambu_run/forms.py b/bambu_run/forms.py index 09ba672..0c81920 100644 --- a/bambu_run/forms.py +++ b/bambu_run/forms.py @@ -52,7 +52,7 @@ class FilamentForm(forms.ModelForm): model = Filament fields = [ 'tray_uuid', 'tag_uid', 'tag_id', 'created_by', - 'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex', + 'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex', 'is_transparent', 'diameter', 'initial_weight_grams', 'remaining_percent', 'remaining_weight_grams', 'is_loaded_in_ams', 'current_tray_id', @@ -71,10 +71,10 @@ class FilamentForm(forms.ModelForm): }), 'tag_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Optional - User-defined ID'}), 'created_by': forms.Select(attrs={'class': 'form-select'}), - 'filament_type': forms.Select(attrs={'class': 'form-select'}), - 'type': forms.HiddenInput(), - 'sub_type': forms.HiddenInput(), - 'brand': forms.HiddenInput(), + 'filament_type': forms.Select(attrs={'class': 'form-select', 'id': 'id_filament_type'}), + 'type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA, PETG, ABS'}), + 'sub_type': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., PLA Basic (optional)'}), + 'brand': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Bambu Lab'}), 'color': forms.Select(attrs={'class': 'form-select', 'id': 'id_color'}), 'color_hex': forms.TextInput(attrs={ 'class': 'form-control', @@ -85,6 +85,7 @@ class FilamentForm(forms.ModelForm): 'initial_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '1000'}), 'remaining_percent': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100'}), 'remaining_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'readonly': 'readonly'}), + 'is_transparent': forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_is_transparent'}), 'is_loaded_in_ams': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}), 'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), diff --git a/bambu_run/management/commands/bambu_collector.py b/bambu_run/management/commands/bambu_collector.py index a94742e..1f097e8 100644 --- a/bambu_run/management/commands/bambu_collector.py +++ b/bambu_run/management/commands/bambu_collector.py @@ -316,7 +316,7 @@ class Command(BaseCommand): def _auto_create_filament(self, tray_data): from bambu_run.models import Filament, FilamentType - from bambu_run.utils import strip_color_padding, match_filament_color + from bambu_run.utils import strip_color_padding, match_filament_color, is_mqtt_color_transparent tray_uuid = tray_data.get('tray_uuid') tag_uid = tray_data.get('tag_uid') @@ -329,6 +329,7 @@ class Command(BaseCommand): default_brand = app_settings.AUTO_CREATE_BRAND + transparent = is_mqtt_color_transparent(mqtt_color) color_code = strip_color_padding(mqtt_color) color_hex = f"#{color_code}" if color_code else None @@ -341,6 +342,7 @@ class Command(BaseCommand): if filament_color: color_name = filament_color.color_name + transparent = transparent or filament_color.is_transparent if self.verbose: logger.info(f"Matched color from database: {color_name} (#{color_code})") else: @@ -368,6 +370,7 @@ class Command(BaseCommand): brand=default_brand, color=color_name, color_hex=color_hex, + is_transparent=transparent, diameter=diameter, initial_weight_grams=initial_weight, remaining_percent=remain_percent, diff --git a/bambu_run/management/commands/bambu_import_colors.py b/bambu_run/management/commands/bambu_import_colors.py index 832c6f1..f6bfe0e 100644 --- a/bambu_run/management/commands/bambu_import_colors.py +++ b/bambu_run/management/commands/bambu_import_colors.py @@ -371,6 +371,11 @@ class Command(BaseCommand): return "created" return "no_type" + # ── Transparent detection ──────────────────────────────────────────── + # "Translucent" (no colour qualifier) + #000000 = clear/transparent filament. + # Bambu Lab AMS reports these as 00000000 (alpha=00). + is_transparent = color_name.strip().lower() == "translucent" and hex_code == "000000" + # ── Duplicate check ────────────────────────────────────────────────── # All five fields must match to be considered a duplicate: # color_code (exact), color_name (case-insensitive), brand, @@ -388,9 +393,10 @@ class Command(BaseCommand): return "duplicate" if dry_run: + transparent_note = " [transparent]" if is_transparent else "" self.stdout.write( f" [dry-run] Would create: {color_name!r} #{hex_code} " - f"({filament_type} / {filament_sub_type})" + f"({filament_type} / {filament_sub_type}){transparent_note}" ) return "created" @@ -404,6 +410,7 @@ class Command(BaseCommand): filament_type=filament_type, filament_sub_type=filament_sub_type, brand=BRAND, + is_transparent=is_transparent, ) self.stdout.write( f" + {color_name!r} #{hex_code} ({filament_type} / {filament_sub_type})" diff --git a/bambu_run/migrations/0002_filament_is_transparent.py b/bambu_run/migrations/0002_filament_is_transparent.py new file mode 100644 index 0000000..2642007 --- /dev/null +++ b/bambu_run/migrations/0002_filament_is_transparent.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bambu_run", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="filamentcolor", + name="is_transparent", + field=models.BooleanField( + default=False, + help_text="True for clear/transparent filaments — display as checkerboard, not solid color", + ), + ), + migrations.AddField( + model_name="filament", + name="is_transparent", + field=models.BooleanField( + default=False, + help_text="True for clear/transparent filaments — display as checkerboard, not solid color", + ), + ), + ] diff --git a/bambu_run/models.py b/bambu_run/models.py index f471fef..38f6b9e 100644 --- a/bambu_run/models.py +++ b/bambu_run/models.py @@ -259,6 +259,10 @@ class FilamentColor(models.Model): default='Bambu Lab', help_text="Manufacturer name" ) + is_transparent = models.BooleanField( + default=False, + help_text="True for clear/transparent filaments — display as checkerboard, not solid color" + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -329,6 +333,10 @@ class Filament(models.Model): max_length=7, null=True, blank=True, help_text="Color hex code for display (#RRGGBB)" ) + is_transparent = models.BooleanField( + default=False, + help_text="True for clear/transparent filaments — display as checkerboard, not solid color" + ) # Physical properties diameter = models.DecimalField( diff --git a/bambu_run/static/bambu_run/js/filament_form.js b/bambu_run/static/bambu_run/js/filament_form.js new file mode 100644 index 0000000..ace6253 --- /dev/null +++ b/bambu_run/static/bambu_run/js/filament_form.js @@ -0,0 +1,156 @@ +/** + * filament_form.js — Filament add/edit form interactions. + * + * Handles: + * - Filament type preset → auto-fill Type / Sub Type / Brand + * - Transparent checkbox → toggle color picker vs. checkerboard swatch + * - Color picker ↔ hex text sync + * - Delete confirmation modal + */ + +document.addEventListener('DOMContentLoaded', function () { + + // ── Filament type preset auto-fill ──────────────────────────────────────── + + const dataEl = document.getElementById('filament-type-data'); + const filamentTypeMap = dataEl ? JSON.parse(dataEl.textContent) : {}; + + const filamentTypeSelect = document.getElementById('id_filament_type'); + const typeField = document.getElementById('id_type'); + const subTypeField = document.getElementById('id_sub_type'); + const brandField = document.getElementById('id_brand'); + + if (filamentTypeSelect) { + filamentTypeSelect.addEventListener('change', function () { + const mapping = filamentTypeMap[this.value]; + if (mapping && typeField && subTypeField && brandField) { + typeField.value = mapping.type; + subTypeField.value = mapping.sub_type; + brandField.value = mapping.brand; + } + }); + } + + // ── Transparent toggle ──────────────────────────────────────────────────── + + const transparentCheckbox = document.getElementById('id_is_transparent'); + const transparentSwatch = document.getElementById('transparent-swatch'); + const colorPicker = document.getElementById('id_color_hex_picker'); + const colorText = document.getElementById('id_color_hex_text'); + + /** + * Show checkerboard swatch and disable color inputs when transparent, + * restore normal color picker when not transparent. + * @param {boolean} isTransparent + */ + function applyTransparentState(isTransparent) { + if (!colorPicker) return; + if (isTransparent) { + transparentSwatch.style.display = 'block'; + colorPicker.style.display = 'none'; + colorPicker.disabled = true; + if (colorText) { colorText.disabled = true; colorText.value = ''; } + } else { + transparentSwatch.style.display = 'none'; + colorPicker.style.display = ''; + colorPicker.disabled = false; + if (colorText) { colorText.disabled = false; } + } + } + + if (transparentCheckbox) { + applyTransparentState(transparentCheckbox.checked); + transparentCheckbox.addEventListener('change', function () { + applyTransparentState(this.checked); + }); + } + + // ── Color picker ↔ hex text sync ────────────────────────────────────────── + + if (colorPicker && colorText) { + colorPicker.addEventListener('input', function () { + colorText.value = this.value.toUpperCase(); + }); + + colorText.addEventListener('input', function () { + const value = this.value.trim(); + if (/^#[0-9A-Fa-f]{6}$/.test(value)) { + colorPicker.value = value; + this.classList.remove('is-invalid'); + } else if (value.length === 7) { + this.classList.add('is-invalid'); + } + }); + + if (colorText.value && /^#[0-9A-Fa-f]{6}$/.test(colorText.value)) { + colorPicker.value = colorText.value; + } else if (colorPicker.value && !colorText.value) { + colorText.value = colorPicker.value.toUpperCase(); + } + } + + // ── Delete confirmation modal ───────────────────────────────────────────── + + const deleteConfirmText = document.getElementById('deleteConfirmText'); + const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); + const deleteForm = document.getElementById('deleteForm'); + const deleteModal = document.getElementById('deleteModal'); + + if (deleteConfirmText && confirmDeleteBtn) { + deleteConfirmText.addEventListener('input', function () { + const value = this.value.trim(); + if (value === 'DELETE') { + confirmDeleteBtn.disabled = false; + this.classList.remove('is-invalid'); + this.classList.add('is-valid'); + } else { + confirmDeleteBtn.disabled = true; + this.classList.remove('is-valid'); + if (value.length > 0) { + this.classList.add('is-invalid'); + } else { + this.classList.remove('is-invalid'); + } + } + }); + + if (deleteForm) { + deleteForm.addEventListener('submit', function (e) { + if (confirmDeleteBtn.disabled) { + e.preventDefault(); + alert('Please type DELETE to confirm deletion'); + return false; + } + return true; + }); + } + + if (deleteModal) { + deleteModal.addEventListener('hidden.bs.modal', function () { + deleteConfirmText.value = ''; + confirmDeleteBtn.disabled = true; + deleteConfirmText.classList.remove('is-valid', 'is-invalid'); + }); + + deleteModal.addEventListener('shown.bs.modal', function () { + deleteConfirmText.focus(); + }); + } + } + + // ── Delete button modal opener (backup) ─────────────────────────────────── + + const deleteBtn = document.getElementById('deleteBtn'); + if (deleteBtn && deleteModal) { + deleteBtn.addEventListener('click', function () { + if (!deleteModal.classList.contains('show')) { + if (typeof bootstrap !== 'undefined') { + bootstrap.Modal.getOrCreateInstance(deleteModal).show(); + } else if (typeof coreui !== 'undefined' && coreui.Modal) { + coreui.Modal.getOrCreateInstance(deleteModal).show(); + } + } + }); + } + +}); diff --git a/bambu_run/static/bambu_run/js/printer_charts.js b/bambu_run/static/bambu_run/js/printer_charts.js index 7acb95d..857dfb1 100644 --- a/bambu_run/static/bambu_run/js/printer_charts.js +++ b/bambu_run/static/bambu_run/js/printer_charts.js @@ -646,8 +646,20 @@ function hexToRgba(hex, alpha) { function applyFilamentColors() { // Apply colors to filament cards document.querySelectorAll('.filament-card').forEach(card => { + const isTransparent = card.getAttribute('data-filament-transparent') === 'true'; const colorHex = card.getAttribute('data-filament-color'); - if (colorHex) { + + if (isTransparent) { + // Checkerboard left border and subtle background for clear filaments + card.style.borderLeft = '4px solid #aaa'; + card.style.background = 'repeating-conic-gradient(rgba(180,180,180,0.15) 0% 25%, transparent 0% 50%) 0 0/10px 10px'; + + const badge = card.querySelector('.filament-badge'); + if (badge) { + badge.style.backgroundColor = '#aaa'; + badge.style.color = '#fff'; + } + } else if (colorHex) { const color = '#' + colorHex; // Set card background with gradient diff --git a/bambu_run/templates/bambu_run/filament_color_list.html b/bambu_run/templates/bambu_run/filament_color_list.html index c777595..a3189ce 100644 --- a/bambu_run/templates/bambu_run/filament_color_list.html +++ b/bambu_run/templates/bambu_run/filament_color_list.html @@ -54,11 +54,19 @@ {% for color in colors %} + {% if color.is_transparent %} +
+ {% else %}
+ {% endif %} {{ color.color_name }} + {% if color.is_transparent %} + Clear / Transparent + {% else %} {{ color.get_hex_color }} + {% endif %} {{ color.filament_type }} diff --git a/bambu_run/templates/bambu_run/filament_detail.html b/bambu_run/templates/bambu_run/filament_detail.html index f6f0e78..aba3c62 100644 --- a/bambu_run/templates/bambu_run/filament_detail.html +++ b/bambu_run/templates/bambu_run/filament_detail.html @@ -27,10 +27,14 @@
Color
+ {% if filament.is_transparent %} +
+ {% else %}
+ {% endif %}
{{ filament.color }}
- {{ filament.color_hex }} + {% if filament.is_transparent %}Clear / Transparent{% else %}{{ filament.color_hex }}{% endif %}
diff --git a/bambu_run/templates/bambu_run/filament_form.html b/bambu_run/templates/bambu_run/filament_form.html index 6a03453..d7c0da6 100644 --- a/bambu_run/templates/bambu_run/filament_form.html +++ b/bambu_run/templates/bambu_run/filament_form.html @@ -43,6 +43,14 @@
Specifications
+
+
+ + {{ form.filament_type }} + Selecting a preset auto-fills Type, Sub Type, and Brand below. +
+
+
@@ -62,12 +70,19 @@
-
-
+
+
+ {{ form.color_hex }}
-
+
+
+ {{ form.is_transparent }} + +
+
+
{{ form.color_hex_text }} e.g. #0A2CA5 @@ -209,95 +224,7 @@ {% endblock %} {% block extra_js %} - +{# Server-side data consumed by filament_form.js #} + + {% endblock %} diff --git a/bambu_run/templates/bambu_run/filament_list.html b/bambu_run/templates/bambu_run/filament_list.html index e83052a..8526386 100644 --- a/bambu_run/templates/bambu_run/filament_list.html +++ b/bambu_run/templates/bambu_run/filament_list.html @@ -122,7 +122,11 @@
+ {% if filament.is_transparent %} +
+ {% else %}
+ {% endif %} {{ filament.color }}
diff --git a/bambu_run/templates/bambu_run/printer_dashboard.html b/bambu_run/templates/bambu_run/printer_dashboard.html index df5496c..49ae1cc 100644 --- a/bambu_run/templates/bambu_run/printer_dashboard.html +++ b/bambu_run/templates/bambu_run/printer_dashboard.html @@ -152,7 +152,7 @@
{% for filament in stats.filaments %}
-
+
Tray {{ filament.tray_id }}
diff --git a/bambu_run/utils.py b/bambu_run/utils.py index c28e786..1cc6f98 100644 --- a/bambu_run/utils.py +++ b/bambu_run/utils.py @@ -3,14 +3,23 @@ Utility functions for filament color matching """ # BambuLab AMS reports colors as 8-char hex with an alpha channel suffix (e.g. '489FDFFF'). -# The last two chars are always 'FF' (fully opaque). Only the first 6 chars are the RGB value. +# Opaque filaments use alpha 'FF'. Clear/transparent filaments use alpha '00' (e.g. '00000000'). MQTT_COLOR_HEX_LENGTH = 6 +def is_mqtt_color_transparent(mqtt_color): + """ + Return True if the AMS color represents a clear/transparent filament. + Bambu Lab uses alpha=00 for transparent (e.g. '00000000'), not 'FF' like opaque filaments. + """ + return bool(mqtt_color) and len(mqtt_color) == 8 and mqtt_color[6:8].upper() == '00' + + def strip_color_padding(mqtt_color): """ - Strip FF padding from MQTT color - MQTT: '000000FF' -> '000000' + Strip alpha padding from MQTT color, returning the 6-char RGB hex. + MQTT: '000000FF' -> '000000' (opaque black) + MQTT: '00000000' -> '000000' (transparent — use is_mqtt_color_transparent() to distinguish) MQTT: 'FF6A13FF' -> 'FF6A13' """ if not mqtt_color: diff --git a/bambu_run/views.py b/bambu_run/views.py index e4e1c6e..73d42a2 100644 --- a/bambu_run/views.py +++ b/bambu_run/views.py @@ -125,6 +125,7 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView): if snapshot.filament: filament_dict['color_name'] = snapshot.filament.color filament_dict['filament_pk'] = snapshot.filament.pk + filament_dict['is_transparent'] = snapshot.filament.is_transparent filaments_list.append(filament_dict) except Exception: filaments_list = [] @@ -544,6 +545,14 @@ class FilamentListView(LoginRequiredMixin, ListView): return context +def _filament_type_map(): + """Return a JSON-serialisable dict mapping FilamentType pk → {type, sub_type, brand}.""" + return { + str(ft.pk): {'type': ft.type, 'sub_type': ft.sub_type or '', 'brand': ft.brand} + for ft in FilamentType.objects.all() + } + + class FilamentCreateView(LoginRequiredMixin, CreateView): model = Filament form_class = FilamentForm @@ -553,6 +562,7 @@ class FilamentCreateView(LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE + context['filament_type_map'] = json.dumps(_filament_type_map()) return context def form_valid(self, form): @@ -569,6 +579,7 @@ class FilamentUpdateView(LoginRequiredMixin, UpdateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE + context['filament_type_map'] = json.dumps(_filament_type_map()) return context def form_valid(self, form): diff --git a/docs/Bambu_Color_Catalog/PETG Translucent.txt b/docs/Bambu_Color_Catalog/PETG Translucent.txt index efa2c5e..2b76ac2 100644 --- a/docs/Bambu_Color_Catalog/PETG Translucent.txt +++ b/docs/Bambu_Color_Catalog/PETG Translucent.txt @@ -1,3 +1,4 @@ +Translucent #000000 Translucent Gray #8E8E8E Translucent Light Blue #61B0FF Translucent Olive #748C45 diff --git a/pyproject.toml b/pyproject.toml index 9c9934d..763f49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bambu-run" -version = "0.1.1" +version = "0.1.2" description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management" readme = "README.md" license = {text = "MIT"}