# Analyze .3MF File and Select Filament Colors

> Source: <https://gist.github.com/fireymerlin/380153723c9a62808eb4bd4e7de6d7b9>
> Published: 2025-08-26 14:00:08+00:00

Created
August 26, 2025 14:00

-
-
Save fireymerlin/380153723c9a62808eb4bd4e7de6d7b9 to your computer and use it in GitHub Desktop.

Analyze .3MF File and Select Filament Colors

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.

[Learn more about bidirectional Unicode characters](https://github.co/hiddenchars)| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Bambu Lab .3MF Color Analyzer - Enhanced Matching</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <style> | |
| body { | |
| font-family: sans-serif; | |
| background: #f4f4f4; | |
| padding: 2em; | |
| color: #333; | |
| } | |
| h1 { | |
| text-align: center; | |
| } | |
| .file-section, .results { | |
| background: white; | |
| border: 1px solid #ccc; | |
| border-radius: 8px; | |
| padding: 1em; | |
| margin-bottom: 2em; | |
| } | |
| .match-item { | |
| border-left: 4px solid #888; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| background: #f9f9f9; | |
| border-radius: 4px; | |
| position: relative; | |
| } | |
| .match-item input[type="radio"] { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| transform: scale(1.5); | |
| } | |
| .match-item.selected { | |
| border-left: 4px solid #0066cc; | |
| background: #e6f3ff; | |
| } | |
| .color-box { | |
| width: 30px; | |
| height: 30px; | |
| display: inline-block; | |
| border: 1px solid #000; | |
| vertical-align: middle; | |
| margin-right: 10px; | |
| } | |
| .match-group { | |
| margin-bottom: 2em; | |
| } | |
| .match-color-box { | |
| width: 20px; | |
| height: 20px; | |
| display: inline-block; | |
| border: 1px solid #000; | |
| vertical-align: middle; | |
| margin-right: 5px; | |
| } | |
| #generateSummary { | |
| background: #0066cc; | |
| color: white; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| margin: 20px 0; | |
| } | |
| #generateSummary:hover { | |
| background: #0052a3; | |
| } | |
| #generateSummary:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| #summary { | |
| background: white; | |
| border: 1px solid #ccc; | |
| border-radius: 8px; | |
| padding: 1em; | |
| margin-top: 2em; | |
| display: none; | |
| } | |
| .summary-item { | |
| margin-bottom: 15px; | |
| padding: 10px; | |
| border-left: 4px solid #0066cc; | |
| background: #f9f9f9; | |
| } | |
| #printSummary { | |
| background: #28a745; | |
| color: white; | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin-right: 10px; | |
| } | |
| #copySummary { | |
| background: #6c757d; | |
| color: white; | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| @media print { | |
| body * { | |
| visibility: hidden; | |
| } | |
| #summary, #summary * { | |
| visibility: visible; | |
| } | |
| #summary { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| } | |
| #printSummary, #copySummary { | |
| display: none; | |
| } | |
| .print-color-box { | |
| -webkit-print-color-adjust: exact !important; | |
| print-color-adjust: exact !important; | |
| color-adjust: exact !important; | |
| } | |
| } | |
| #dropZone { | |
| border: 2px dashed #aaa; | |
| border-radius: 8px; | |
| padding: 20px; | |
| text-align: center; | |
| color: #666; | |
| margin: 1em 0; | |
| background: #f9f9f9; | |
| } | |
| .algorithm-selector { | |
| margin: 10px 0; | |
| padding: 10px; | |
| background: #f0f8ff; | |
| border-radius: 5px; | |
| } | |
| .algorithm-info { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| .distance-info { | |
| font-size: 11px; | |
| color: #888; | |
| } | |
| .test-section { | |
| background: #fff3cd; | |
| border: 1px solid #ffeaa7; | |
| border-radius: 8px; | |
| padding: 1em; | |
| margin-bottom: 2em; | |
| } | |
| .matches-selector { | |
| margin: 10px 0; | |
| padding: 10px; | |
| background: #fff8e1; | |
| border-radius: 5px; | |
| border: 1px solid #ffcc02; | |
| } | |
| .matches-selector label { | |
| font-weight: bold; | |
| margin-right: 15px; | |
| } | |
| .matches-selector input[type="radio"] { | |
| margin-right: 5px; | |
| margin-left: 15px; | |
| } | |
| .matches-info { | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Bambu Lab .3MF Color Analyzer - Enhanced Matching</h1> | |
| <div class="file-section"> | |
| <div id="dropZone"> | |
| <strong>Drag and drop your .3mf file here</strong><br>or use the browse button below. | |
| </div> | |
| <input type="file" id="fileInput" accept=".3mf" /> | |
| <br><br> | |
| <div class="algorithm-selector"> | |
| <label for="algorithmSelect"><strong>Color Matching Algorithm:</strong></label> | |
| <select id="algorithmSelect"> | |
| <option value="euclidean">Euclidean RGB (Original)</option> | |
| <option value="deltaE76">Delta E 76 (CIE Lab)</option> | |
| <option value="deltaE94">Delta E 94 (Improved)</option> | |
| <option value="deltaE00" selected>Delta E 2000 (Best)</option> | |
| <option value="weighted">Weighted RGB</option> | |
| </select> | |
| <div class="algorithm-info" id="algorithmInfo"> | |
| Delta E 2000 - most perceptually accurate but complex | |
| </div> | |
| </div> | |
| <div class="matches-selector"> | |
| <label><strong>Number of Matches to Show:</strong></label> | |
| <input type="radio" id="matches3" name="matchCount" value="3" checked> | |
| <label for="matches3">Top 3 Matches</label> | |
| <input type="radio" id="matches5" name="matchCount" value="5"> | |
| <label for="matches5">Top 5 Matches</label> | |
| <div class="matches-info"> | |
| Choose how many color matches to display for each input color | |
| </div> | |
| </div> | |
| <label for="typeFilter"><strong>Filter by Filament Type:</strong></label> | |
| <select id="typeFilter" multiple size="6"> | |
| <option value="">All Types (No Filter)</option> | |
| </select> | |
| <br><br> | |
| <button onclick="analyze3MF()">Analyze .3MF File</button> | |
| </div> | |
| <div id="colorStatus" style="margin-bottom: 1em; font-weight: bold; color: green;"></div> | |
| <div id="results" class="results"></div> | |
| <div id="selectionSection" style="display: none;"> | |
| <button id="generateSummary" onclick="generateSummary()">Generate Summary for Selected Colors</button> | |
| <p style="color: #666; font-size: 14px;">Select one match for each color above, then click to generate a printable summary.</p> | |
| </div> | |
| <div id="summary"> | |
| <h2>Selected Filament Colors Summary</h2> | |
| <div style="margin-bottom: 15px;"> | |
| <button id="printSummary" onclick="window.print()">Print Summary</button> | |
| <button id="copySummary" onclick="copyToClipboard()">Copy to Clipboard</button> | |
| </div> | |
| <div id="summaryContent"></div> | |
| </div> | |
| <script> | |
| const colors = [ | |
| {"type":"PLA Basic", "name":"Jade White", "hex":"#FFFFFF","code":10001,"location":"PLA 04"}, | |
| {"type":"PLA Basic", "name":"Voxel Army Green","hex":"#667B65","code":0,"location":"PLA 01"}, | |
| {"type":"PLA Silk+", "name":"Gold", "hex":"#F4A925","code":13405,"location":"PLA Silk 01"}, | |
| {"type":"PLA Silk+", "name":"White", "hex":"#FFFFFF","code":13110,"location":"PLA Silk 02"}, | |
| {"type":"PLA Matte", "name":"Ivory White", "hex":"#FFFFFF","code":11100,"location":"PLA Matte 04"}, | |
| {"type":"PLA Matte", "name":"Inland Wood Brown","hex":"#D2B48C","code":0,"location":"PLA Matte 02"}, | |
| {"type":"PETG Translucent", "name":"Inland Clear", "hex":"#F0F2F5","code":0,"location":"PETG Translucent 01"} | |
| ]; | |
| populateTypeFilter(); | |
| var currentMatches = []; | |
| var currentHexColors = []; | |
| // Algorithm descriptions | |
| const algorithmDescriptions = { | |
| euclidean: "Simple RGB distance - fast but not perceptually accurate", | |
| deltaE76: "CIE Lab Delta E 76 - perceptually uniform, good for most uses", | |
| deltaE94: "CIE Lab Delta E 94 - improved weighting for lightness/chroma", | |
| deltaE00: "CIE Lab Delta E 2000 - most perceptually accurate but complex", | |
| weighted: "Weighted RGB - considers human eye sensitivity to R/G/B differently" | |
| }; | |
| // Update algorithm info when selection changes | |
| document.getElementById('algorithmSelect').addEventListener('change', function() { | |
| const info = document.getElementById('algorithmInfo'); | |
| info.textContent = algorithmDescriptions[this.value]; | |
| // Re-analyze if we have colors loaded | |
| if (currentHexColors.length > 0) { | |
| displayMatches(currentHexColors); | |
| } | |
| }); | |
| // Update matches when count selection changes | |
| document.querySelectorAll('input[name="matchCount"]').forEach(radio => { | |
| radio.addEventListener('change', function() { | |
| // Re-analyze if we have colors loaded | |
| if (currentHexColors.length > 0) { | |
| displayMatches(currentHexColors); | |
| } | |
| }); | |
| }); | |
| // Update matches when type filter changes | |
| document.getElementById('typeFilter').addEventListener('change', function() { | |
| // Re-analyze if we have colors loaded | |
| if (currentHexColors.length > 0) { | |
| displayMatches(currentHexColors); | |
| } | |
| }); | |
| <!-- fetch("bambu-colors.json") --> | |
| <!-- .then(res => res.json()) --> | |
| <!-- .then(data => { --> | |
| <!-- colors = data; --> | |
| <!-- document.getElementById("colorStatus").innerHTML = `✅ Loaded ${data.length} colors.`; --> | |
| <!-- populateTypeFilter(); --> | |
| <!-- }) --> | |
| <!-- .catch(error => { --> | |
| <!-- document.getElementById("colorStatus").innerHTML = `❌ Error loading colors: ${error.message}`; --> | |
| <!-- document.getElementById("colorStatus").style.color = "red"; --> | |
| <!-- }); --> | |
| function populateTypeFilter() { | |
| const typeFilter = document.getElementById("typeFilter"); | |
| const uniqueTypes = [...new Set(colors.map(c => c.type))].sort(); | |
| typeFilter.innerHTML = '<option value="">All Types (No Filter)</option>'; | |
| uniqueTypes.forEach(type => { | |
| const option = document.createElement("option"); | |
| option.value = type; | |
| option.textContent = type; | |
| typeFilter.appendChild(option); | |
| }); | |
| } | |
| function getMatchCount() { | |
| const checkedRadio = document.querySelector('input[name="matchCount"]:checked'); | |
| return parseInt(checkedRadio.value); | |
| } | |
| function hexToRgb(hex) { | |
| const result = hex.replace("#", "").match(/.{1,2}/g); | |
| return result ? result.map((v) => parseInt(v, 16)) : [0, 0, 0]; | |
| } | |
| // RGB to XYZ conversion (D65 illuminant) | |
| function rgbToXyz(r, g, b) { | |
| // Normalize RGB values | |
| r /= 255; g /= 255; b /= 255; | |
| // Apply gamma correction | |
| r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; | |
| g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; | |
| b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; | |
| // Observer = 2°, Illuminant = D65 | |
| const x = r * 0.4124 + g * 0.3576 + b * 0.1805; | |
| const y = r * 0.2126 + g * 0.7152 + b * 0.0722; | |
| const z = r * 0.0193 + g * 0.1192 + b * 0.9505; | |
| return [x * 100, y * 100, z * 100]; | |
| } | |
| // XYZ to LAB conversion | |
| function xyzToLab(x, y, z) { | |
| // Reference white D65 | |
| const xn = 95.047, yn = 100.000, zn = 108.883; | |
| x /= xn; y /= yn; z /= zn; | |
| const fx = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x + 16/116); | |
| const fy = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y + 16/116); | |
| const fz = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z + 16/116); | |
| const l = 116 * fy - 16; | |
| const a = 500 * (fx - fy); | |
| const b = 200 * (fy - fz); | |
| return [l, a, b]; | |
| } | |
| // RGB to LAB direct conversion | |
| function rgbToLab(r, g, b) { | |
| const [x, y, z] = rgbToXyz(r, g, b); | |
| return xyzToLab(x, y, z); | |
| } | |
| // Color difference algorithms | |
| function getEuclideanDistance(c1, c2) { | |
| return Math.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])** 2 + (c1[2]-c2[2])**2); | |
| } | |
| function getWeightedRGBDistance(c1, c2) { | |
| // Human eye is more sensitive to green, less to blue | |
| const rWeight = 0.3, gWeight = 0.59, bWeight = 0.11; | |
| return Math.sqrt( | |
| rWeight * (c1[0]-c2[0])**2 + | |
| gWeight * (c1[1]-c2[1])**2 + | |
| bWeight * (c1[2]-c2[2])**2 | |
| ); | |
| } | |
| function getDeltaE76(lab1, lab2) { | |
| const dL = lab1[0] - lab2[0]; | |
| const da = lab1[1] - lab2[1]; | |
| const db = lab1[2] - lab2[2]; | |
| return Math.sqrt(dL*dL + da*da + db*db); | |
| } | |
| function getDeltaE94(lab1, lab2) { | |
| const dL = lab1[0] - lab2[0]; | |
| const da = lab1[1] - lab2[1]; | |
| const db = lab1[2] - lab2[2]; | |
| const c1 = Math.sqrt(lab1[1]*lab1[1] + lab1[2]*lab1[2]); | |
| const c2 = Math.sqrt(lab2[1]*lab2[1] + lab2[2]*lab2[2]); | |
| const dC = c1 - c2; | |
| const dH = Math.sqrt(da*da + db*db - dC*dC); | |
| const sL = 1; | |
| const sC = 1 + 0.045 * c1; | |
| const sH = 1 + 0.015 * c1; | |
| return Math.sqrt((dL/sL)**2 + (dC/sC)** 2 + (dH/sH)**2); | |
| } | |
| function getDeltaE00(lab1, lab2) { | |
| // Simplified Delta E 2000 - full implementation is very complex | |
| const dL = lab1[0] - lab2[0]; | |
| const da = lab1[1] - lab2[1]; | |
| const db = lab1[2] - lab2[2]; | |
| const c1 = Math.sqrt(lab1[1]*lab1[1] + lab1[2]*lab1[2]); | |
| const c2 = Math.sqrt(lab2[1]*lab2[1] + lab2[2]*lab2[2]); | |
| const cBar = (c1 + c2) / 2; | |
| const g = 0.5 * (1 - Math.sqrt(Math.pow(cBar, 7) / (Math.pow(cBar, 7) + Math.pow(25, 7)))); | |
| const a1p = lab1[1] * (1 + g); | |
| const a2p = lab2[1] * (1 + g); | |
| const c1p = Math.sqrt(a1p*a1p + lab1[2]*lab1[2]); | |
| const c2p = Math.sqrt(a2p*a2p + lab2[2]*lab2[2]); | |
| const cBarP = (c1p + c2p) / 2; | |
| const dCp = c2p - c1p; | |
| const dap = a2p - a1p; | |
| const dbp = lab2[2] - lab1[2]; | |
| const dHp = Math.sqrt(dap*dap + dbp*dbp - dCp*dCp); | |
| const sL = 1 + (0.015 * Math.pow(lab1[0] - 50, 2)) / Math.sqrt(20 + Math.pow(lab1[0] - 50, 2)); | |
| const sC = 1 + 0.045 * cBarP; | |
| const sH = 1 + 0.015 * cBarP; | |
| return Math.sqrt((dL/sL)**2 + (dCp/sC)** 2 + (dHp/sH)**2); | |
| } | |
| function getColorDistance(c1, c2, algorithm) { | |
| switch(algorithm) { | |
| case 'euclidean': | |
| return getEuclideanDistance(c1, c2); | |
| case 'weighted': | |
| return getWeightedRGBDistance(c1, c2); | |
| case 'deltaE76': | |
| const lab1_76 = rgbToLab(c1[0], c1[1], c1[2]); | |
| const lab2_76 = rgbToLab(c2[0], c2[1], c2[2]); | |
| return getDeltaE76(lab1_76, lab2_76); | |
| case 'deltaE94': | |
| const lab1_94 = rgbToLab(c1[0], c1[1], c1[2]); | |
| const lab2_94 = rgbToLab(c2[0], c2[1], c2[2]); | |
| return getDeltaE94(lab1_94, lab2_94); | |
| case 'deltaE00': | |
| const lab1_00 = rgbToLab(c1[0], c1[1], c1[2]); | |
| const lab2_00 = rgbToLab(c2[0], c2[1], c2[2]); | |
| return getDeltaE00(lab1_00, lab2_00); | |
| default: | |
| return getEuclideanDistance(c1, c2); | |
| } | |
| } | |
| async function analyze3MF() { | |
| if (colors.length === 0) { | |
| alert("Color data is not loaded yet. Please try again in a moment."); | |
| return; | |
| } | |
| window.uploadedFileName = undefined; | |
| const fileInput = document.getElementById("fileInput"); | |
| const file = fileInput.files[0]; | |
| window.uploadedFileName = file.name; | |
| if (!file) return alert("Please select a .3mf file."); | |
| try { | |
| const zip = await JSZip.loadAsync(file); | |
| const configFile = Object.keys(zip.files).find(name => name.toLowerCase().endsWith("project_settings.config")); | |
| if (!configFile) return alert("No project_settings.config found in .3mf"); | |
| const configText = await zip.files[configFile].async("string"); | |
| const filamentColorMatch = configText.match(/"filament_colour":\s*\[([\s\S]*?)\]/); | |
| let colorHexes = []; | |
| if (filamentColorMatch) { | |
| colorHexes = [...new Set((filamentColorMatch[1].match(/#[A-Fa-f0-9]{6}/g) || []).map(c => c.toUpperCase()))]; | |
| } else { | |
| colorHexes = [...new Set((configText.match(/#[A-Fa-f0-9]{6}/g) || []).map(c => c.toUpperCase()))].slice(0, 16); | |
| } | |
| if (colorHexes.length === 0) { | |
| alert("No hex color codes found."); | |
| return; | |
| } | |
| currentHexColors = colorHexes; | |
| displayMatches(colorHexes); | |
| document.getElementById("selectionSection").style.display = "block"; | |
| document.getElementById("summary").style.display = "none"; | |
| } catch (error) { | |
| alert("Error processing .3MF file: " + error.message); | |
| } | |
| } | |
| function displayMatches(hexColors) { | |
| const resultsDiv = document.getElementById("results"); | |
| const typeSelect = document.getElementById("typeFilter"); | |
| const selectedTypes = Array.from(typeSelect.selectedOptions).map(opt => opt.value).filter(v => v !== ""); | |
| const algorithm = document.getElementById('algorithmSelect').value; | |
| const matchCount = getMatchCount(); | |
| resultsDiv.innerHTML = ""; | |
| currentMatches = []; | |
| const filteredColors = selectedTypes.length === 0 ? colors : colors.filter(c => selectedTypes.includes(c.type)); | |
| const filterStatus = selectedTypes.length > 0 | |
| ? ` (filtered to ${selectedTypes.join(", ")} - ${filteredColors.length} colors)` | |
| : ` (all types - ${filteredColors.length} colors)`; | |
| hexColors.forEach((hex, i) => { | |
| const rgb = hexToRgb(hex); | |
| const colorDistances = filteredColors.filter(c => c.hex).map(c => { | |
| const targetRgb = hexToRgb(c.hex); | |
| return { | |
| ...c, | |
| distance: getColorDistance(rgb, targetRgb, algorithm) | |
| }; | |
| }).sort((a, b) => a.distance - b.distance).slice(0, matchCount); | |
| const group = document.createElement("div"); | |
| group.className = "match-group"; | |
| group.innerHTML = `<h3>Input Color ${i+1}: <span class='color-box' style='background:${hex}'></span> ${hex}${filterStatus} [${algorithm}] - Top ${matchCount}</h3>`; | |
| if (colorDistances.length === 0) { | |
| group.innerHTML += `<p style="color: #666; font-style: italic;">No matches found for selected filament type.</p>`; | |
| } else { | |
| colorDistances.forEach((match, index) => { | |
| const item = document.createElement("div"); | |
| item.className = "match-item"; | |
| // Different thresholds for different algorithms | |
| let isExactMatch = false; | |
| if (algorithm === 'euclidean' || algorithm === 'weighted') { | |
| isExactMatch = match.distance < 5; | |
| } else { | |
| isExactMatch = match.distance < 3; // Delta E < 3 is generally considered a good match | |
| } | |
| const exactMatchLabel = isExactMatch ? " 🎯 <strong style='color: #00AA00;'>EXCELLENT MATCH!</strong>" : ""; | |
| if (isExactMatch) { | |
| item.style.borderLeft = "4px solid #00AA00"; | |
| item.style.backgroundColor = "#f0fff0"; | |
| } | |
| const radioId = `color${i}_match${index}`; | |
| item.innerHTML = ` | |
| <input type="radio" name="color${i}" id="${radioId}" value="${index}" onchange="updateSelection(${i}, ${index})" ${index === 0 ? 'checked' : ''}> | |
| <strong>${index + 1}.</strong> | |
| <span class='match-color-box' style='background:${match.hex}'></span> | |
| ${match.name} (${match.hex})${exactMatchLabel}<br> | |
| <strong>Type:</strong> ${match.type}<br> | |
| <strong>Code:</strong> ${match.code}<br> | |
| <strong>Location:</strong> ${match.location}<br> | |
| <span class="distance-info"><strong>Distance:</strong> ${match.distance.toFixed(2)} (${algorithm})</span> | |
| `; | |
| if (index === 0) { | |
| item.classList.add('selected'); | |
| } | |
| group.appendChild(item); | |
| }); | |
| } | |
| resultsDiv.appendChild(group); | |
| currentMatches[i] = { | |
| inputHex: hex, | |
| inputIndex: i, | |
| matches: colorDistances, | |
| selectedIndex: 0 | |
| }; | |
| }); | |
| } | |
| function updateSelection(colorIndex, matchIndex) { | |
| currentMatches[colorIndex].selectedIndex = matchIndex; | |
| const colorGroup = document.querySelectorAll('.match-group')[colorIndex]; | |
| const matchItems = colorGroup.querySelectorAll('.match-item'); | |
| matchItems.forEach((item, idx) => { | |
| if (idx === matchIndex) { | |
| item.classList.add('selected'); | |
| } else { | |
| item.classList.remove('selected'); | |
| } | |
| }); | |
| } | |
| function generateSummary() { | |
| const summaryContent = document.getElementById("summaryContent"); | |
| const algorithm = document.getElementById('algorithmSelect').value; | |
| const matchCount = getMatchCount(); | |
| //let summaryHTML = ""; | |
| //let textSummary = "Selected Filament Colors Summary\n" + "=".repeat(35) + "\n\n"; | |
| let fileName = window.uploadedFileName || "Unknown File"; | |
| let summaryHTML = `<p><strong>File:</strong> ${fileName}</p>`; | |
| let textSummary = `Selected Filament Colors Summary\n` + | |
| `File: ${fileName}\n` + | |
| "=".repeat(35) + "\n\n"; | |
| currentMatches.forEach((colorData, i) => { | |
| const selectedMatch = colorData.matches[colorData.selectedIndex]; | |
| let isExactMatch = false; | |
| if (algorithm === 'euclidean' || algorithm === 'weighted') { | |
| isExactMatch = selectedMatch.distance < 5; | |
| } else { | |
| isExactMatch = selectedMatch.distance < 3; | |
| } | |
| summaryHTML += ` | |
| <div class="summary-item"> | |
| <strong>Color ${i+1}:</strong> | |
| <span class='match-color-box print-color-box' style='background:${selectedMatch.hex} !important; border: 3px solid ${selectedMatch.hex} !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; color-adjust: exact;'></span> | |
| ${selectedMatch.name} (${selectedMatch.hex})${isExactMatch ? " 🎯 EXCELLENT MATCH" : ""}<br> | |
| <strong>Type:</strong> ${selectedMatch.type} | <strong>Code:</strong> ${selectedMatch.code}<br> | |
| <strong>Location:</strong> ${selectedMatch.location}<br> | |
| <strong>Distance:</strong> ${selectedMatch.distance.toFixed(2)} (${algorithm}, Top ${matchCount})<br> | |
| <strong>Color Sample:</strong> <span style='padding: 2px 8px; background: ${selectedMatch.hex}; color: white; border: 2px solid ${selectedMatch.hex}; -webkit-print-color-adjust: exact; print-color-adjust: exact;'>${selectedMatch.hex}</span> | |
| </div> | |
| `; | |
| textSummary += `Color ${i+1}: ${selectedMatch.name} (${selectedMatch.hex})${isExactMatch ? " 🎯 EXCELLENT MATCH" : ""}\n`; | |
| textSummary += `Type: ${selectedMatch.type} | Code: ${selectedMatch.code}\n`; | |
| textSummary += `Location: ${selectedMatch.location}\n`; | |
| textSummary += `Distance: ${selectedMatch.distance.toFixed(2)} (${algorithm}, Top ${matchCount})\n\n`; | |
| }); | |
| summaryContent.innerHTML = summaryHTML; | |
| document.getElementById("summary").style.display = "block"; | |
| window.currentTextSummary = textSummary; | |
| document.getElementById("summary").scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function copyToClipboard() { | |
| if (window.currentTextSummary) { | |
| navigator.clipboard.writeText(window.currentTextSummary).then(() => { | |
| const button = document.getElementById("copySummary"); | |
| const originalText = button.textContent; | |
| button.textContent = "Copied!"; | |
| button.style.background = "#28a745"; | |
| setTimeout(() => { | |
| button.textContent = originalText; | |
| button.style.background = "#6c757d"; | |
| }, 2000); | |
| }).catch(err => { | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = window.currentTextSummary; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| alert("Summary copied to clipboard!"); | |
| }); | |
| } | |
| } | |
| // Drag and drop functionality | |
| const dropZone = document.getElementById("dropZone"); | |
| const fileInput = document.getElementById("fileInput"); | |
| dropZone.addEventListener("dragover", e => { | |
| e.preventDefault(); | |
| dropZone.style.background = "#eef"; | |
| }); | |
| dropZone.addEventListener("dragleave", () => { | |
| dropZone.style.background = "#f9f9f9"; | |
| }); | |
| dropZone.addEventListener("drop", e => { | |
| e.preventDefault(); | |
| dropZone.style.background = "#f9f9f9"; | |
| const file = e.dataTransfer.files[0]; | |
| if (file && file.name.endsWith(".3mf")) { | |
| fileInput.files = e.dataTransfer.files; | |
| } else { | |
| alert("Only .3mf files are supported."); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
