From 6aa58647216ea60f156ec83e2686a0bb1850c935 Mon Sep 17 00:00:00 2001 From: Rrrrobert Date: Fri, 16 Oct 2026 09:47:07 -0400 Subject: [PATCH 1/3] Support up to 5 colors on OpenSpool tags OpenSpool Extended tags can now carry up to 4 extra colors alongside the primary one, written as `additional_color_hexes` (the array OpenRFID reads for multi-color spools). Reading a tag loads them back into colors 2-5. Each extra color now has its own checkbox and must be enabled before it can be set; only enabled colors are written. The next color's checkbox appears once the previous one is enabled, and disabling a color also disables the ones after it, so colors stay in order. The extra colors moved from the Advanced section to sit under Color 1. OpenPrintTag keeps colors 2-4 with the same per-color checkboxes. Made with help from Claude --- public/app.js | 96 ++++++++++++++++++++----------- public/index.html | 137 ++++++++++++++++++++++++++------------------ public/openspool.js | 29 +++++++++- 3 files changed, 171 insertions(+), 91 deletions(-) diff --git a/public/app.js b/public/app.js index d91fa50..5f10101 100644 --- a/public/app.js +++ b/public/app.js @@ -102,6 +102,9 @@ const app = { 'PA-CF': { minTemp: 250, maxTemp: 280, bedTempMin: 70, bedTempMax: 90 } }, + // Primary color plus up to 4 extra colors + MAX_COLORS: 5, + palettes: { material: { paletteId: 'materialPalette', @@ -129,15 +132,16 @@ const app = { this.checkNFC(); // Removed preset swatch palette; using canvas pickers only if (typeof ColorPicker !== 'undefined' && ColorPicker && typeof ColorPicker.init === 'function') { - ColorPicker.init(this); + ColorPicker.init(this, this.MAX_COLORS); } for (const name in this.palettes) this.initPalette(name); this.populateFormats(); this.initEventListeners(); + this.updateColorSlots(); this.updateFormat(); this.updateVisibility(); - // Initialize all four colors - for (let i = 1; i <= 4; i++) { + // Initialize all colors + for (let i = 1; i <= this.MAX_COLORS; i++) { this.updateColor('#FFFFFF', i); } this.applyTemperaturePreset(); @@ -310,8 +314,8 @@ const app = { this.handleFileUpload(e.target.files[0]); }); - // Add event listeners for all four color inputs - for (let i = 1; i <= 4; i++) { + // Add event listeners for all color inputs + for (let i = 1; i <= this.MAX_COLORS; i++) { document.getElementById(`colorHex${i}`).addEventListener('input', (e) => { this.updateColor('#' + e.target.value, i); this.updateRecordSize(); @@ -323,9 +327,12 @@ const app = { this.updateVisibility(); }); - document.getElementById('showAdditionalColors').addEventListener('change', (e) => { - this.toggleAdditionalColors(e.target.checked); - }); + // Extra colors (2..MAX_COLORS) must be enabled before they can be set + for (let i = 2; i <= this.MAX_COLORS; i++) { + document.getElementById(`colorEnable${i}`).addEventListener('change', (e) => { + this.setColorEnabled(i, e.target.checked); + }); + } document.getElementById('brandInput').addEventListener('input', () => { document.getElementById('brandValue').value = document.getElementById('brandInput').value || ''; @@ -390,25 +397,23 @@ const app = { document.getElementById('formatSelect').value = data.format || format; this.setPaletteValue('material', data.materialType || 'PLA'); - // Populate all four color fields - document.getElementById('colorHex1').value = data.colorHex || 'FFFFFF'; - this.updateColor('#' + (data.colorHex || 'FFFFFF'), 1); - - document.getElementById('colorHex2').value = data.colorHex2 || 'FFFFFF'; - this.updateColor('#' + (data.colorHex2 || 'FFFFFF'), 2); - - document.getElementById('colorHex3').value = data.colorHex3 || 'FFFFFF'; - this.updateColor('#' + (data.colorHex3 || 'FFFFFF'), 3); - - document.getElementById('colorHex4').value = data.colorHex4 || 'FFFFFF'; - this.updateColor('#' + (data.colorHex4 || 'FFFFFF'), 4); + // Populate all color fields + for (let i = 1; i <= this.MAX_COLORS; i++) { + const hex = data[i === 1 ? 'colorHex' : `colorHex${i}`] || 'FFFFFF'; + document.getElementById(`colorHex${i}`).value = hex; + this.updateColor('#' + hex, i); + } - // Show additional colors if any are present - const hasAdditionalColors = (data.colorHex2 && data.colorHex2 !== 'FFFFFF') || - (data.colorHex3 && data.colorHex3 !== 'FFFFFF') || - (data.colorHex4 && data.colorHex4 !== 'FFFFFF'); - document.getElementById('showAdditionalColors').checked = hasAdditionalColors; - this.toggleAdditionalColors(hasAdditionalColors); + // Enable each extra color present in the data. Colors are enabled in + // order, so a later color also enables any earlier gaps. + let enableUpTo = 1; + for (let i = 2; i <= this.MAX_COLORS; i++) { + if (data[`colorHex${i}`]) enableUpTo = i; + } + for (let i = 2; i <= this.MAX_COLORS; i++) { + document.getElementById(`colorEnable${i}`).checked = i <= enableUpTo; + } + this.updateColorSlots(); if (data.brand && this.palettes.brand.items.includes(data.brand)) { this.setPaletteValue('brand', data.brand); @@ -464,9 +469,10 @@ const app = { materialType: document.getElementById('materialType').value, brand: brand || 'Generic', colorHex: document.getElementById('colorHex1').value.replace('#', ''), - colorHex2: document.getElementById('colorHex2').value.replace('#', ''), - colorHex3: document.getElementById('colorHex3').value.replace('#', ''), - colorHex4: document.getElementById('colorHex4').value.replace('#', ''), + colorHex2: this.getExtraColor(2), + colorHex3: this.getExtraColor(3), + colorHex4: this.getExtraColor(4), + colorHex5: this.getExtraColor(5), // Temps/IDs minTemp: document.getElementById('minTemp').value, @@ -839,11 +845,33 @@ const app = { return '#' + result.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join(''); }, - toggleAdditionalColors(show) { - const additionalColorFields = document.querySelectorAll('.additional-colors'); - additionalColorFields.forEach(field => { - field.style.display = show ? 'block' : 'none'; - }); + // Extra color i's hex, or '' when it isn't enabled (so it isn't written) + getExtraColor(i) { + if (!document.getElementById(`colorEnable${i}`).checked) return ''; + return document.getElementById(`colorHex${i}`).value.replace('#', ''); + }, + + setColorEnabled(i, enabled) { + if (enabled) { + // Enabling a color also enables any earlier ones, keeping them in order + for (let j = 2; j <= i; j++) document.getElementById(`colorEnable${j}`).checked = true; + } else { + // Disabling a color also disables every later one + for (let j = i; j <= this.MAX_COLORS; j++) document.getElementById(`colorEnable${j}`).checked = false; + } + this.updateColorSlots(); + this.updateRecordSize(); + }, + + // Show each enabled color's fields, and offer the next color's checkbox + // only once the one before it is enabled. + updateColorSlots() { + for (let i = 2; i <= this.MAX_COLORS; i++) { + const enabled = document.getElementById(`colorEnable${i}`).checked; + const prevEnabled = i === 2 || document.getElementById(`colorEnable${i - 1}`).checked; + document.querySelector(`.extra-color[data-color-index="${i}"]`).style.display = prevEnabled ? '' : 'none'; + document.getElementById(`colorFields${i}`).style.display = enabled ? '' : 'none'; + } }, showStatus(id, type, message) { diff --git a/public/index.html b/public/index.html index d2c871b..95c2993 100644 --- a/public/index.html +++ b/public/index.html @@ -573,6 +573,88 @@

✏️ Create New Tag

+ + + Multi-color filament: enable each extra color to set it + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -667,61 +749,6 @@

✏️ Create New Tag

-
- - - Enable fields for colors 2, 3, and 4 - -
- - - - - - -
diff --git a/public/openspool.js b/public/openspool.js index 255951b..a85501b 100644 --- a/public/openspool.js +++ b/public/openspool.js @@ -20,10 +20,18 @@ const OpenSpool = { bedTempMax: 'bed_max_temp', // spoolmanId: 'spool_id', // disabled for now // lotNr: 'lot_nr', // disabled for now - extendedSubType: 'subtype' + extendedSubType: 'subtype', + // Extra colors 2-5 for multi-color filament, stored in order + colorHex2: 'additional_color_hexes', + colorHex3: 'additional_color_hexes', + colorHex4: 'additional_color_hexes', + colorHex5: 'additional_color_hexes' } }, + // Most extra colors a tag can carry (OpenRFID reads up to 4) + MAX_ADDITIONAL_COLORS: 4, + // Return list of fields available based on formData (e.g., format) availableFields: function(formData) { const conditionalObj = this.COMPATIBILITY_FIELDS[formData.format] || {}; @@ -47,6 +55,15 @@ const OpenSpool = { color_hex: formData.colorHex.replace('#', '').toUpperCase(), }; + const additionalColors = []; + for (let i = 2; i <= this.MAX_ADDITIONAL_COLORS + 1; i++) { + const hex = formData[`colorHex${i}`]; + if (hex) additionalColors.push(hex.replace('#', '').toUpperCase()); + } + if (additionalColors.length) { + data.additional_color_hexes = additionalColors; + } + if (formData.brand) { data.brand = formData.brand; } @@ -94,7 +111,7 @@ const OpenSpool = { } } - return { + const result = { format, materialType: jsonData.type || 'PLA', colorHex: jsonData.color_hex || 'FFFFFF', @@ -107,6 +124,14 @@ const OpenSpool = { lotNr: jsonData.lot_nr || '', extendedSubType: jsonData.subtype || '' }; + + // Extra colors map to colorHex2..colorHex5 in order + const additional = Array.isArray(jsonData.additional_color_hexes) ? jsonData.additional_color_hexes : []; + additional.slice(0, this.MAX_ADDITIONAL_COLORS).forEach((hex, idx) => { + result[`colorHex${idx + 2}`] = String(hex).replace('#', '').toUpperCase(); + }); + + return result; }, // Create NDEF record for NFC writing From 3bd5d82ad4857214b2ebef2189736b5db41f5026 Mon Sep 17 00:00:00 2001 From: Rrrrobert Date: Fri, 23 Oct 2026 18:34:34 -0400 Subject: [PATCH 2/3] OpenSpool: choose Gradient or Segments for multi-color spools Once a second color is added, a "Multi-color Style" choice appears with Gradient and Segments (Segments by default). It's written as "color_type" next to additional_color_hexes, using OrcaSlicer's filament_colour_type values: 0 = gradient, 1 = solid/segments. Single color tags don't get the field, and tags without it read as Segments. OpenSpool Compat doesn't show or write it. Printers and OpenRFID ignore the field for now; it's there so they (and OrcaSlicer's filament color sync) can tell the two apart later. Made with help from Claude --- public/app.js | 12 ++++++++++++ public/index.html | 12 ++++++++++++ public/openspool.js | 11 ++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index 5f10101..acae557 100644 --- a/public/app.js +++ b/public/app.js @@ -126,6 +126,13 @@ const app = { items: [{label: 'None', value: ''}, 'Basic', 'Matte', 'SnapSpeed', 'Silk', 'Support', 'HF', '95A', '95A HF'], defaultValue: '', }, + // Multi-color style, using OrcaSlicer's filament_colour_type values + colorType: { + paletteId: 'colorTypePalette', + inputId: 'colorType', + items: [{label: 'Gradient', value: '0'}, {label: 'Segments', value: '1'}], + defaultValue: '1', + }, }, init() { @@ -431,6 +438,7 @@ const app = { document.getElementById('spoolmanId').value = data.spoolmanId || ''; document.getElementById('lotNr').value = data.lotNr || ''; this.setPaletteValue('variant', data.extendedSubType ?? ''); + this.setPaletteValue('colorType', data.colorType === '0' ? '0' : '1'); // Advanced fields document.getElementById('materialName').value = data.materialName || ''; @@ -482,6 +490,7 @@ const app = { spoolmanId: document.getElementById('spoolmanId').value, lotNr: document.getElementById('lotNr').value, extendedSubType: document.getElementById('extendedSubType').value, + colorType: document.getElementById('colorType').value, // Advanced materialName: document.getElementById('materialName').value, @@ -872,6 +881,9 @@ const app = { document.querySelector(`.extra-color[data-color-index="${i}"]`).style.display = prevEnabled ? '' : 'none'; document.getElementById(`colorFields${i}`).style.display = enabled ? '' : 'none'; } + // Gradient/Segments only matters once there's more than one color + const typeGroup = document.getElementById('colorTypeGroup'); + if (typeGroup) typeGroup.style.display = document.getElementById('colorEnable2').checked ? '' : 'none'; }, showStatus(id, type, message) { diff --git a/public/index.html b/public/index.html index 95c2993..ca58010 100644 --- a/public/index.html +++ b/public/index.html @@ -655,6 +655,18 @@

✏️ Create New Tag

+ + +
diff --git a/public/openspool.js b/public/openspool.js index a85501b..bc0ba1c 100644 --- a/public/openspool.js +++ b/public/openspool.js @@ -25,7 +25,10 @@ const OpenSpool = { colorHex2: 'additional_color_hexes', colorHex3: 'additional_color_hexes', colorHex4: 'additional_color_hexes', - colorHex5: 'additional_color_hexes' + colorHex5: 'additional_color_hexes', + // How the colors run: 0 = gradient, 1 = solid/segments (OrcaSlicer's + // filament_colour_type values). Only written with extra colors. + colorType: 'color_type' } }, @@ -62,6 +65,9 @@ const OpenSpool = { } if (additionalColors.length) { data.additional_color_hexes = additionalColors; + if (formData.colorType === '0' || formData.colorType === '1') { + data.color_type = Number(formData.colorType); + } } if (formData.brand) { @@ -125,6 +131,9 @@ const OpenSpool = { extendedSubType: jsonData.subtype || '' }; + // 0 = gradient; anything else (or missing) = solid/segments + result.colorType = String(jsonData.color_type) === '0' ? '0' : '1'; + // Extra colors map to colorHex2..colorHex5 in order const additional = Array.isArray(jsonData.additional_color_hexes) ? jsonData.additional_color_hexes : []; additional.slice(0, this.MAX_ADDITIONAL_COLORS).forEach((hex, idx) => { From 8f9a0e17c60a53bc878a6284a3cd67b5ae1f0838 Mon Sep 17 00:00:00 2001 From: Rrrrobert Date: Fri, 23 Oct 2026 20:47:58 -0400 Subject: [PATCH 3/3] OpenSpool: only write color_type for gradient spools To match the documented field (color_type: 0 = gradient, 1 = default, which is assumed when it's missing), "color_type": 0 is written for gradient multi-color spools and the field is left out otherwise. Reading is unchanged: 0 is Gradient, anything else or missing is Segments. Made with help from Claude --- public/index.html | 5 +++-- public/openspool.js | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index ca58010..f3ab2fd 100644 --- a/public/index.html +++ b/public/index.html @@ -656,8 +656,9 @@

✏️ Create New Tag

+ there are 2+ colors. Gradient is written as "color_type": 0 + (OrcaSlicer's filament_colour_type values); Segments is the + default, so the field is left out. -->