mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 14:09:04 +01:00
Compare commits
3 Commits
feature/mu
...
color_base
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a3bf969bf | ||
|
|
5e7333fc83 | ||
|
|
0dd4758860 |
@@ -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'}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})"
|
||||
|
||||
27
bambu_run/migrations/0002_filament_is_transparent.py
Normal file
27
bambu_run/migrations/0002_filament_is_transparent.py
Normal file
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
156
bambu_run/static/bambu_run/js/filament_form.js
Normal file
156
bambu_run/static/bambu_run/js/filament_form.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -54,11 +54,19 @@
|
||||
{% for color in colors %}
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
{% if color.is_transparent %}
|
||||
<div style="width: 50px; height: 50px; border-radius: 4px; border: 2px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||
{% else %}
|
||||
<div style="width: 50px; height: 50px; background-color: {{ color.get_hex_color }}; border-radius: 4px; border: 2px solid #ddd;"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle"><strong>{{ color.color_name }}</strong></td>
|
||||
<td class="align-middle">
|
||||
{% if color.is_transparent %}
|
||||
<span class="text-muted fst-italic">Clear / Transparent</span>
|
||||
{% else %}
|
||||
<span class="font-monospace">{{ color.get_hex_color }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<span class="badge bg-secondary">{{ color.filament_type }}</span>
|
||||
|
||||
@@ -27,10 +27,14 @@
|
||||
<div class="card-body">
|
||||
<h6>Color</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if filament.is_transparent %}
|
||||
<div style="width: 50px; height: 50px; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||
{% else %}
|
||||
<div style="width: 50px; height: 50px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd;"></div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<strong>{{ filament.color }}</strong><br>
|
||||
<small class="text-muted">{{ filament.color_hex }}</small>
|
||||
<small class="text-muted">{% if filament.is_transparent %}Clear / Transparent{% else %}{{ filament.color_hex }}{% endif %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
<hr>
|
||||
<h5>Specifications</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Filament Type Preset</label>
|
||||
{{ form.filament_type }}
|
||||
<small class="form-text text-muted">Selecting a preset auto-fills Type, Sub Type, and Brand below.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type *</label>
|
||||
@@ -62,12 +70,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="row mb-3 align-items-end">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Color Picker</label>
|
||||
<div id="transparent-swatch" style="display:none; width:100%; height:38px; border-radius:4px; border:1px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||
{{ form.color_hex }}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-2">
|
||||
<div class="form-check mt-4">
|
||||
{{ form.is_transparent }}
|
||||
<label class="form-check-label" for="id_is_transparent">Transparent / Clear</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">{{ form.color_hex_text.label }}</label>
|
||||
{{ form.color_hex_text }}
|
||||
<small class="form-text text-muted">e.g. #0A2CA5</small>
|
||||
@@ -209,95 +224,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Sync color picker and text input
|
||||
const colorPicker = document.getElementById('id_color_hex_picker');
|
||||
const colorText = document.getElementById('id_color_hex_text');
|
||||
|
||||
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 logic
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Backup modal opener
|
||||
const deleteBtn = document.getElementById('deleteBtn');
|
||||
if (deleteBtn && deleteModal) {
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
if (!deleteModal.classList.contains('show')) {
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const modalInstance = bootstrap.Modal.getOrCreateInstance(deleteModal);
|
||||
modalInstance.show();
|
||||
} else if (typeof coreui !== 'undefined' && coreui.Modal) {
|
||||
const modalInstance = coreui.Modal.getOrCreateInstance(deleteModal);
|
||||
modalInstance.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{# Server-side data consumed by filament_form.js #}
|
||||
<script type="application/json" id="filament-type-data">{{ filament_type_map|safe }}</script>
|
||||
<script src="{% static 'bambu_run/js/filament_form.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -122,7 +122,11 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center">
|
||||
{% if filament.is_transparent %}
|
||||
<div style="width: 30px; height: 30px; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd; background: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0/10px 10px;" title="Clear / Transparent"></div>
|
||||
{% else %}
|
||||
<div style="width: 30px; height: 30px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd;"></div>
|
||||
{% endif %}
|
||||
{{ filament.color }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<div class="row g-3">
|
||||
{% for filament in stats.filaments %}
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}">
|
||||
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}"{% if filament.is_transparent %} data-filament-transparent="true"{% endif %}>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Translucent #000000
|
||||
Translucent Gray #8E8E8E
|
||||
Translucent Light Blue #61B0FF
|
||||
Translucent Olive #748C45
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user