Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 70 additions & 28 deletions src/OpenPrintTagWriterHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,34 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
<h2 class="section-title">Basic</h2>
<div class="grid-2">
<div class="field">
<label for="material_type">Material</label>
<select id="material_type" name="material_type" required>
<option value="0">PLA</option>
<option value="1">PETG</option>
<option value="2">TPU</option>
<option value="3">ABS</option>
<option value="4">ASA</option>
<option value="5">PC</option>
<option value="6">PCTG</option>
<option value="7">PP</option>
<option value="8">PA6 (Nylon 6)</option>
<option value="9">PA11 (Nylon 11)</option>
<option value="10">PA12 (Nylon 12)</option>
<option value="11">PA66 (Nylon 66)</option>
<option value="12">CPE</option>
<option value="13">TPE</option>
<option value="14">HIPS</option>
<option value="15">PHA</option>
<option value="16">PET</option>
<option value="17">PEI</option>
<option value="18">PBT</option>
<option value="19">PVB</option>
<option value="20">PVA</option>
<option value="21">PEKK</option>
<option value="22">PEEK</option>
</select>
<label for="material_search">Material</label>
<input id="material_search" name="material_search" list="material-list" placeholder="Type to search materials" required />
<datalist id="material-list">
<option data-id="0" value="PLA"></option>
<option data-id="1" value="PETG"></option>
<option data-id="2" value="TPU"></option>
<option data-id="3" value="ABS"></option>
<option data-id="4" value="ASA"></option>
<option data-id="5" value="PC"></option>
<option data-id="6" value="PCTG"></option>
<option data-id="7" value="PP"></option>
<option data-id="8" value="PA6 (Nylon 6)"></option>
<option data-id="9" value="PA11 (Nylon 11)"></option>
<option data-id="10" value="PA12 (Nylon 12)"></option>
<option data-id="11" value="PA66 (Nylon 66)"></option>
<option data-id="12" value="CPE"></option>
<option data-id="13" value="TPE"></option>
<option data-id="14" value="HIPS"></option>
<option data-id="15" value="PHA"></option>
<option data-id="16" value="PET"></option>
<option data-id="17" value="PEI"></option>
<option data-id="18" value="PBT"></option>
<option data-id="19" value="PVB"></option>
<option data-id="20" value="PVA"></option>
<option data-id="21" value="PEKK"></option>
<option data-id="22" value="PEEK"></option>
</datalist>
<input type="hidden" id="material_type" name="material_type" value="0" />
</div>

<div class="field">
Expand Down Expand Up @@ -268,6 +270,8 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
var writerForm = document.getElementById('writerForm');

var materialTypeEl = document.getElementById('material_type');
var materialSearchEl = document.getElementById('material_search');
var materialListEl = document.getElementById('material-list');
var materialNameEl = document.getElementById('material_name');

var backBtn = document.getElementById('backBtn');
Expand All @@ -278,6 +282,44 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
syncColorPicker('colorPicker', 'colorHex');
setupAdvancedToggle('advancedToggle', 'advancedBox');

// Sync material search → hidden material_type ID
function syncMaterialId() {
var opts = materialListEl.querySelectorAll('option');
materialTypeEl.value = '0'; // default PLA
for (var i = 0; i < opts.length; i++) {
if (opts[i].value === materialSearchEl.value) {
materialTypeEl.value = opts[i].dataset.id;
break;
}
}
}

// Auto-fill temps and density from material selection
var optFieldMap = {
minPrintTemp: 'min_print_temp', maxPrintTemp: 'max_print_temp',
minBedTemp: 'min_bed_temp', maxBedTemp: 'max_bed_temp',
density: 'density'
};
trackAutoFill(['min_print_temp','max_print_temp','min_bed_temp','max_bed_temp','density']);
materialSearchEl.addEventListener('input', function() {
syncMaterialId();
autoFillMaterialData(materialSearchEl.value, optFieldMap);
});
loadMaterialDb().then(function(db) {
// Expand datalist with API materials that aren't in the hardcoded list
var existing = {};
var opts = materialListEl.querySelectorAll('option');
for (var i = 0; i < opts.length; i++) existing[opts[i].value.toUpperCase()] = true;
Object.keys(db).sort().forEach(function(key) {
if (!existing[key]) {
var opt = document.createElement('option');
opt.value = db[key].material || key;
opt.dataset.id = db[key].id || '0';
materialListEl.appendChild(opt);
}
});
});

function showStatusView() {
createView.classList.add('hidden');
statusView.classList.remove('hidden');
Expand All @@ -291,7 +333,7 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
}

function syncMaterialNameFromSelection() {
var selectedText = materialTypeEl.options[materialTypeEl.selectedIndex].text;
var selectedText = materialSearchEl.value || 'PLA';
if (!materialNameEl.value.trim() || materialNameEl.dataset.autoFilled === 'true') {
materialNameEl.value = selectedText.toUpperCase();
materialNameEl.dataset.autoFilled = 'true';
Expand Down Expand Up @@ -481,7 +523,7 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
}
}

materialTypeEl.addEventListener('change', syncMaterialNameFromSelection);
materialSearchEl.addEventListener('input', syncMaterialNameFromSelection);

materialNameEl.addEventListener('input', function() {
materialNameEl.value = materialNameEl.value.toUpperCase();
Expand Down
84 changes: 62 additions & 22 deletions src/OpenTag3DWriterHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,29 @@ const char OPENTAG3D_WRITER_HTML[] PROGMEM = R"rawliteral(
<div class="grid-2">
<div class="field">
<label for="base_material">Material</label>
<select id="base_material" required>
<option value="PLA" selected>PLA</option>
<option value="PETG">PETG</option>
<option value="TPU">TPU</option>
<option value="ABS">ABS</option>
<option value="ASA">ASA</option>
<option value="PC">PC</option>
<option value="PCTG">PCTG</option>
<option value="PP">PP</option>
<option value="PA6">PA6</option>
<option value="PA12">PA12</option>
<option value="PA66">PA66</option>
<option value="CPE">CPE</option>
<option value="TPE">TPE</option>
<option value="HIPS">HIPS</option>
<option value="PET">PET</option>
<option value="PEI">PEI</option>
<option value="PVA">PVA</option>
<option value="PVB">PVB</option>
<option value="PEEK">PEEK</option>
<option value="PEKK">PEKK</option>
</select>
<input id="base_material" list="material-list" placeholder="Type to search materials" required value="PLA" />
<datalist id="material-list">
<option value="PLA"></option>
<option value="PETG"></option>
<option value="TPU"></option>
<option value="ABS"></option>
<option value="ASA"></option>
<option value="PC"></option>
<option value="PCTG"></option>
<option value="PP"></option>
<option value="PA6"></option>
<option value="PA12"></option>
<option value="PA66"></option>
<option value="CPE"></option>
<option value="TPE"></option>
<option value="HIPS"></option>
<option value="PET"></option>
<option value="PEI"></option>
<option value="PVA"></option>
<option value="PVB"></option>
<option value="PEEK"></option>
<option value="PEKK"></option>
</datalist>
</div>
<div class="field">
<label for="material_modifiers">Modifiers</label>
Expand Down Expand Up @@ -308,6 +309,45 @@ const char OPENTAG3D_WRITER_HTML[] PROGMEM = R"rawliteral(
syncColorPicker('colorPicker', 'colorHex');
setupAdvancedToggle('advancedToggle', 'advancedBox');

// Auto-fill temps and density from material selection
var baseMaterialEl = document.getElementById('base_material');
var modifiersEl = document.getElementById('material_modifiers');
var ot3dFieldMap = {
minPrintTemp: 'min_print_temp_c', maxPrintTemp: 'max_print_temp_c',
minBedTemp: 'min_bed_temp_c', maxBedTemp: 'max_bed_temp_c',
density: 'density'
};
trackAutoFill(['print_temp_c','bed_temp_c','min_print_temp_c','max_print_temp_c','min_bed_temp_c','max_bed_temp_c','density']);
function ot3dAutoFill() {
var name = baseMaterialEl.value;
var mod = modifiersEl ? modifiersEl.value : '';
if (mod && mod !== 'None') name = name + '-' + mod;
autoFillMaterialData(name, ot3dFieldMap);
// Also fill the basic print/bed temp fields
var m = lookupMaterial(name);
if (m) {
var pt = document.getElementById('print_temp_c');
if (pt && pt.dataset.autoFilled !== 'false') { pt.value = m.extruder_temp; pt.dataset.autoFilled = 'true'; }
var bt = document.getElementById('bed_temp_c');
if (bt && bt.dataset.autoFilled !== 'false') { bt.value = m.bed_temp; bt.dataset.autoFilled = 'true'; }
}
}
baseMaterialEl.addEventListener('input', ot3dAutoFill);
if (modifiersEl) modifiersEl.addEventListener('change', ot3dAutoFill);
loadMaterialDb().then(function(db) {
var dl = document.getElementById('material-list');
var existing = {};
var opts = dl.querySelectorAll('option');
for (var i = 0; i < opts.length; i++) existing[opts[i].value.toUpperCase()] = true;
Object.keys(db).sort().forEach(function(key) {
if (!existing[key]) {
var opt = document.createElement('option');
opt.value = db[key].material || key;
dl.appendChild(opt);
}
});
});

var EXTENDED_IDS = [
'serial_number', 'online_url', 'measured_filament_weight_g',
'empty_spool_weight_g', 'min_print_temp_c', 'max_print_temp_c',
Expand Down
119 changes: 119 additions & 0 deletions src/SharedJS.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,125 @@ function setupAdvancedToggle(toggleId, boxId) {
return { open: function(){ set(true); }, close: function(){ set(false); } };
}

/* ---- Material data auto-fill ---- */

// Hardcoded fallback — works offline when API is unreachable
var _materialFallback = {
'PLA': {extruder_temp:240, bed_temp:55, density:1.24},
'PETG': {extruder_temp:270, bed_temp:75, density:1.27},
'ABS': {extruder_temp:280, bed_temp:90, density:1.04},
'ASA': {extruder_temp:280, bed_temp:100, density:1.05},
'TPU': {extruder_temp:250, bed_temp:40, density:1.21},
'PC': {extruder_temp:290, bed_temp:115, density:1.3},
'PCTG': {extruder_temp:270, bed_temp:75, density:1.21},
'PP': {extruder_temp:250, bed_temp:60, density:0.9},
'HIPS': {extruder_temp:270, bed_temp:95, density:1.03},
'PVA': {extruder_temp:240, bed_temp:60, density:1.23},
'PEEK': {extruder_temp:410, bed_temp:145, density:1.32},
'PA6': {extruder_temp:300, bed_temp:45, density:1.52},
'PA12': {extruder_temp:300, bed_temp:45, density:1.52},
'PEI': {extruder_temp:380, bed_temp:130, density:1.27},
'CPE': {extruder_temp:270, bed_temp:80, density:1.25}
};
var _materialDb = {};
var _materialDbLoaded = false;

function loadMaterialDb() {
if (_materialDbLoaded) return Promise.resolve(_materialDb);
// Start with fallback data
Object.keys(_materialFallback).forEach(function(k) {
_materialDb[k] = _materialFallback[k];
});
return fetch('https://api.tigertag.io/api:tigertag/SpoolmanDB/materials')
.then(function(r) { return r.ok ? r.json() : []; })
.then(function(data) {
if (Array.isArray(data)) {
data.forEach(function(m) {
if (m.material) _materialDb[m.material.toUpperCase()] = m;
});
}
Comment on lines +163 to +168
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential null reference if API returns malformed material data.

The code assumes m.material exists when iterating API results. If the API returns an object without a material property, m.material.toUpperCase() will throw a TypeError.

🛡️ Proposed defensive fix
       if (Array.isArray(data)) {
         data.forEach(function(m) {
-          if (m.material) _materialDb[m.material.toUpperCase()] = m;
+          if (m && m.material) _materialDb[m.material.toUpperCase()] = m;
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SharedJS.h` around lines 163 - 168, The then-callback that populates
_materialDb assumes each item has a string material and directly calls
m.material.toUpperCase(), which can throw if m.material is missing or not a
string; update the loop in the .then(function(data) { ... }) handler to guard
each item (e.g., verify m is truthy and typeof m.material === 'string' or use a
safe access) before calling toUpperCase(), and only add entries to _materialDb
when the validated uppercase key is non-empty to avoid null/undefined key
errors.

_materialDbLoaded = true;
return _materialDb;
})
.catch(function() {
_materialDbLoaded = true;
return _materialDb;
});
}

function lookupMaterial(name) {
if (!name) return null;
var key = name.toUpperCase().replace(/\s+/g, '');
// Exact match first
if (_materialDb[key]) return _materialDb[key];
// Try with common separators
var withDash = key.replace(/([A-Z]+)(\d)/, '$1-$2');
if (_materialDb[withDash]) return _materialDb[withDash];
// Prefix match (e.g. "PA6 (Nylon 6)" → "PA6")
var prefix = key.split(/[^A-Z0-9-]/)[0];
if (prefix && _materialDb[prefix]) return _materialDb[prefix];
return null;
}

// Track which fields the user has manually edited
function trackAutoFill(fieldIds) {
fieldIds.forEach(function(id) {
var el = document.getElementById(id);
if (!el) return;
el.dataset.autoFilled = 'true';
el.addEventListener('input', function() {
el.dataset.autoFilled = 'false';
});
el.addEventListener('change', function() {
if (el.value === '') el.dataset.autoFilled = 'true';
});
});
}

function autoFillMaterialData(materialName, fieldMap) {
var m = lookupMaterial(materialName);
if (!m) return;
if (m.extruder_temp) {
if (fieldMap.minPrintTemp) {
var el = document.getElementById(fieldMap.minPrintTemp);
if (el && el.dataset.autoFilled !== 'false') {
el.value = Math.max(0, m.extruder_temp - 10);
el.dataset.autoFilled = 'true';
}
}
if (fieldMap.maxPrintTemp) {
var el = document.getElementById(fieldMap.maxPrintTemp);
if (el && el.dataset.autoFilled !== 'false') {
el.value = m.extruder_temp + 10;
el.dataset.autoFilled = 'true';
}
}
}
if (m.bed_temp) {
if (fieldMap.minBedTemp) {
var el = document.getElementById(fieldMap.minBedTemp);
if (el && el.dataset.autoFilled !== 'false') {
el.value = Math.max(0, m.bed_temp - 5);
el.dataset.autoFilled = 'true';
}
}
if (fieldMap.maxBedTemp) {
var el = document.getElementById(fieldMap.maxBedTemp);
if (el && el.dataset.autoFilled !== 'false') {
el.value = m.bed_temp + 5;
el.dataset.autoFilled = 'true';
}
}
}
if (m.density && fieldMap.density) {
var el = document.getElementById(fieldMap.density);
if (el && el.dataset.autoFilled !== 'false') {
el.value = m.density;
el.dataset.autoFilled = 'true';
}
}
}

/* ---- Tag kind labels ---- */
var TAG_KIND_LABELS = {
'OpenPrintTag': 'OpenPrintTag',
Expand Down
Loading