From 5324dfe95c326b96b1349a6030598aea6cd5d1c7 Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Tue, 25 Nov 2025 14:45:23 -0700 Subject: [PATCH 1/8] add csv import buttons --- src/kiri/core/init.js | 2 ++ web/kiri/index.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index a8968d1a9..f91ebecdb 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -922,6 +922,8 @@ function init_one() { toolsClose: $('tools-close'), toolsImport: $('tools-import'), toolsExport: $('tools-export'), + toolsImportCSV: $('tools-import-csv'), + toolsExportCSV: $('tools-export-csv'), toolSelect: $('tool-select'), toolAdd: $('tool-add'), toolCopy: $('tool-dup'), diff --git a/web/kiri/index.html b/web/kiri/index.html index 7ba7dfc28..e92fd3d8f 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -614,6 +614,8 @@
+ +
From 2143fae071a0c3cf0c03f51e2a8131e78a468efe Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Tue, 25 Nov 2025 14:51:12 -0700 Subject: [PATCH 2/8] add CSV import --- src/kiri/core/platform.js | 8 ++- src/kiri/mode/cam/tools.js | 117 ++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/kiri/core/platform.js b/src/kiri/core/platform.js index 48c902826..426f3f96e 100644 --- a/src/kiri/core/platform.js +++ b/src/kiri/core/platform.js @@ -11,6 +11,7 @@ import { Packer } from './pack.js'; import { COLOR, MODES } from './consts.js'; import { THREE } from '../../ext/three.js'; +import { decodeToolCSV } from '../mode/cam/tools.js'; const V0 = new THREE.Vector3(0,0,0); @@ -754,6 +755,7 @@ function platformLoadFiles(files, group) { iskmz = lower.endsWith(".kmz"), isini = lower.endsWith(".ini"), isgbr = lower.endsWith(".gbr"), + iscsv = lower.endsWith(".csv"), israw = lower.endsWith(".raw") || lower.indexOf('.') < 0, isset = lower.endsWith(".b64") || lower.endsWith(".km"), isgcode = lower.endsWith(".gcode") || lower.endsWith(".nc"); @@ -810,7 +812,11 @@ function platformLoadFiles(files, group) { } }); } - } else if (is3mf) { + }else if(iscsv){ + let [success, result] = decodeToolCSV(data.textDecode('utf-8')); + if(!success) api.show.alert("Error: "+result) + else api.settings.import(result, true); + }else if (is3mf) { let odon = function(models) { let msg = api.show.alert('Adding Objects'); for (let model of models) { diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 8c6b43d2f..2b6cd52db 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -233,9 +233,113 @@ function setToolChanged(changed) { api.ui.toolsSave.disabled = !changed; } +function generateToolCSV(){ + + let {tools} = api.conf.get(); + + let header = [ + 'api_version', + 'id', + 'number', + 'type', + 'name', + 'metric', + 'shaft_diam', + 'shaft_len', + 'flute_diam', + 'flute_len', + 'taper_tip', + 'order', + ].join(','); + + let acc = header + '\n'; + + for(let [i,t] of tools.entries()) { + acc += + [ + i == 0 ? api.version : '', + t.id, + t.number, + t.type, + t.name, + t.metric ? 'true' : 'false', + t.shaft_diam, + t.shaft_len, + t.flute_diam, + t.flute_len, + t.taper_tip, + t.order, + ].join(',') + (i == tools.length - 1 ? '' : '\n'); + } + + return acc; +} + +export function decodeToolCSV(data){ + + let apiVer; + try{ + apiVer = data.split('\n')[1].split(',')[0]; + + }catch( err ){ + console.log(err) + return [false, "malformed csv: cannot determine api version"]; + } + + // will need to implement logic in the future if the tool API changes + // console.log("got api version", apiVer) + + try{ + //get and parse tools line by line + let tools = data.split( '\n' ).slice( 1 ).map( line => { + let [ver,id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip] = line.split(','); + // console.log({ver,id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip}) + return { + id: parseInt( id ), + type: type.toString(), + number: parseInt(number), + name: name.toString(), + metric: metric == 'true'? true : ( metric == 'false' ? false : null ), + shaft_diam: parseFloat(shaft_diam), + shaft_len: parseFloat(shaft_len), + flute_diam: parseFloat(flute_diam), + flute_len: parseFloat(flute_len), + taper_tip: parseFloat(taper_tip), + } + }) + + let IDs = new Set(); + + for(let tool of tools){ + //check tool IDs + if( tool.id == NaN ) throw "id must be a number"; + if( IDs.has(tool.id) ) throw "tool ids must be unique"; + IDs.add(tool.id); + // check remaining fields + if( toolNames.indexOf(tool.type) == -1 ) throw "tool type must be one of " + toolNames.join(', '); + if( tool.number == NaN ) throw "number must be a number"; + if( tool.metric == null ) throw "metric must be a boolean"; + if( tool.shaft_diam == NaN ) throw "shaft_diam must be a number"; + if( tool.shaft_len == NaN ) throw "shaft_len must be a number"; + if( tool.flute_diam == NaN ) throw "flute_diam must be a number"; + if( tool.flute_len == NaN ) throw "flute_len must be a number"; + if( tool.taper_tip == NaN ) throw "taper_tip must be a number"; + } + + return [true, { + version: apiVer, + tools, + time: Date.now() + }]; + + }catch(err){ + return [ false, "malformed csv: " + err ]; + } +} + export function showTools() { - if (api.mode.get_id() !== MODES.CAM) return; - setconf.sync.get().then(_showTools); + if ( api.mode.get_id() !== MODES.CAM ) return; + setconf.sync.get().then( _showTools ); } function _showTools() { @@ -325,6 +429,15 @@ function _showTools() { api.util.download(api.util.b64enc(record), `${name}.km`); }); }; + ui.toolsImportCSV.onclick = (ev) => api.event.import(ev); + ui.toolsExportCSV.onclick = () => { + api.uc.prompt("Export Tools Filename", "tools").then(name => { + if (!name) { + return; + } + api.util.download(generateToolCSV(), `${name}.csv`); + }); + }; renderTools(); if (editTools.length > 0) { From 763215e59631f172397544c90a41078d4218d760 Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Tue, 25 Nov 2025 15:33:11 -0700 Subject: [PATCH 3/8] fix bugs, add CSV escaping --- src/kiri/mode/cam/tools.js | 79 ++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 2b6cd52db..0463df059 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -238,7 +238,6 @@ function generateToolCSV(){ let {tools} = api.conf.get(); let header = [ - 'api_version', 'id', 'number', 'type', @@ -250,6 +249,7 @@ function generateToolCSV(){ 'flute_len', 'taper_tip', 'order', + 'api_version='+ api.version, ].join(','); let acc = header + '\n'; @@ -257,11 +257,11 @@ function generateToolCSV(){ for(let [i,t] of tools.entries()) { acc += [ - i == 0 ? api.version : '', + t.id, t.number, t.type, - t.name, + escapeCSV(t.name), t.metric ? 'true' : 'false', t.shaft_diam, t.shaft_len, @@ -275,25 +275,70 @@ function generateToolCSV(){ return acc; } +function escapeCSV(x) { + if (x == null) return ""; + x = x.toString(); + return /[",\n]/.test(x) ? `"${x.replace(/"/g, '""')}"` : x; + } + + + function splitCSVLine(text) { + const line = []; + let current = ""; + let inQuotes = false; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + + if (char === '"') { + // doubled quotes inside quoted sections + if (inQuotes && i + 1 < text.length && text[i + 1] === '"') { + current += '"'; + i++; + } + else { + inQuotes = !inQuotes; + } + } + else if(char === ',') { + if(!inQuotes) { + line.push(current); + current = ""; + }else{ + current += ','; + } + }else{ + current += char; + } + } + + line.push(current); + + return line; +} + export function decodeToolCSV(data){ let apiVer; try{ - apiVer = data.split('\n')[1].split(',')[0]; - + apiVer = data.split('\n')[0].split(',')[11].split('=')[1]; }catch( err ){ console.log(err) return [false, "malformed csv: cannot determine api version"]; } // will need to implement logic in the future if the tool API changes - // console.log("got api version", apiVer) + console.log("got api version", apiVer) try{ //get and parse tools line by line - let tools = data.split( '\n' ).slice( 1 ).map( line => { - let [ver,id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip] = line.split(','); - // console.log({ver,id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip}) + let tools = data.split( '\n' ) + .slice( 1 ) + .filter( line => line.length > 0 ) + .map( line => { + console.log(splitCSVLine( line )) + let [id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip, order,] = splitCSVLine( line ); + console.log({id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip}) return { id: parseInt( id ), type: type.toString(), @@ -305,6 +350,7 @@ export function decodeToolCSV(data){ flute_diam: parseFloat(flute_diam), flute_len: parseFloat(flute_len), taper_tip: parseFloat(taper_tip), + order: parseInt(order), } }) @@ -317,13 +363,14 @@ export function decodeToolCSV(data){ IDs.add(tool.id); // check remaining fields if( toolNames.indexOf(tool.type) == -1 ) throw "tool type must be one of " + toolNames.join(', '); - if( tool.number == NaN ) throw "number must be a number"; - if( tool.metric == null ) throw "metric must be a boolean"; - if( tool.shaft_diam == NaN ) throw "shaft_diam must be a number"; - if( tool.shaft_len == NaN ) throw "shaft_len must be a number"; - if( tool.flute_diam == NaN ) throw "flute_diam must be a number"; - if( tool.flute_len == NaN ) throw "flute_len must be a number"; - if( tool.taper_tip == NaN ) throw "taper_tip must be a number"; + if( Number.isNaN(tool.number) ) throw "number must be a number"; + if( Number.isNaN(tool.metric) ) throw "metric must be a boolean"; + if( Number.isNaN(tool.shaft_diam) ) throw "shaft_diam must be a number"; + if( Number.isNaN(tool.shaft_len) ) throw "shaft_len must be a number"; + if( Number.isNaN(tool.flute_diam) ) throw "flute_diam must be a number"; + if( Number.isNaN(tool.flute_len) ) throw "flute_len must be a number"; + if( Number.isNaN(tool.taper_tip) ) throw "taper_tip must be a number"; + if( Number.isNaN(tool.order) ) throw "order must be a number"; } return [true, { From 4cfac3942c66390a97e4ff318be2acd0444f4423 Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Tue, 25 Nov 2025 15:43:05 -0700 Subject: [PATCH 4/8] beautify and add jsdoc --- src/kiri/mode/cam/tools.js | 79 ++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 0463df059..13c32b47b 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -233,9 +233,20 @@ function setToolChanged(changed) { api.ui.toolsSave.disabled = !changed; } +/** + * generateToolCSV generates a CSV string containing all the tools in the + * settings object. The CSV string will have the following format: + * + * id,number,type,name,metric,shaft_diam,shaft_len,flute_diam,flute_len,taper_tip,order,api_version + * + * Each line of the CSV string will represent a single tool, with the above + * fields listed in order. The 'api_version' field will be set to the current + * version of the Kiri API. + * + * @return {string} the generated CSV string + */ function generateToolCSV(){ - - let {tools} = api.conf.get(); + let { tools } = api.conf.get(); let header = [ 'id', @@ -253,11 +264,9 @@ function generateToolCSV(){ ].join(','); let acc = header + '\n'; - - for(let [i,t] of tools.entries()) { + for( let [i,t] of tools.entries() ) { acc += [ - t.id, t.number, t.type, @@ -269,19 +278,38 @@ function generateToolCSV(){ t.flute_len, t.taper_tip, t.order, - ].join(',') + (i == tools.length - 1 ? '' : '\n'); + ].join(',') + ( i == tools.length - 1 ? '' : '\n' ); } return acc; } + +/** + * Escapes a string for use in a CSV file. + * + * If the string contains a comma or newline, it will be wrapped in + * double quotes and any double quotes inside the string will be + * replaced with two double quotes. + * + * @param {string} x - The string to escape + * @returns {string} - The escaped string + */ function escapeCSV(x) { if (x == null) return ""; x = x.toString(); return /[",\n]/.test(x) ? `"${x.replace(/"/g, '""')}"` : x; } - +/** + * Splits a line of CSV into an array of strings. + * + * This function will handle quoted sections with double quotes inside, + * and will split on commas outside of quoted sections. + * + * @param {string} text - The line of CSV to split + * @returns {string[]} - The array of strings split from the text + */ function splitCSVLine(text) { const line = []; let current = ""; @@ -289,7 +317,6 @@ function escapeCSV(x) { for (let i = 0; i < text.length; i++) { const char = text[i]; - if (char === '"') { // doubled quotes inside quoted sections if (inQuotes && i + 1 < text.length && text[i + 1] === '"') { @@ -311,12 +338,18 @@ function escapeCSV(x) { current += char; } } - line.push(current); - return line; } - +/** + * + * @param {string} data + * decodeToolCSV takes a CSV string containing tool data and returns an object + * containing an array of tool objects and the version of the API used to generate + * the CSV. The object returned will be in the format of [true, {tools, time, version}] + * + * If the CSV is malformed in any way, decodeToolCSV will return a tuple of [false, errMessage] + */ export function decodeToolCSV(data){ let apiVer; @@ -328,7 +361,7 @@ export function decodeToolCSV(data){ } // will need to implement logic in the future if the tool API changes - console.log("got api version", apiVer) + // console.log("got api version", apiVer) try{ //get and parse tools line by line @@ -336,26 +369,24 @@ export function decodeToolCSV(data){ .slice( 1 ) .filter( line => line.length > 0 ) .map( line => { - console.log(splitCSVLine( line )) let [id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip, order,] = splitCSVLine( line ); - console.log({id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip}) + // console.log({id, number, type, name, metric, shaft_diam, shaft_len, flute_diam, flute_len, taper_tip, order}) return { id: parseInt( id ), - type: type.toString(), - number: parseInt(number), - name: name.toString(), + type, + number: parseInt( number ), + name, metric: metric == 'true'? true : ( metric == 'false' ? false : null ), - shaft_diam: parseFloat(shaft_diam), - shaft_len: parseFloat(shaft_len), - flute_diam: parseFloat(flute_diam), - flute_len: parseFloat(flute_len), - taper_tip: parseFloat(taper_tip), - order: parseInt(order), + shaft_diam: parseFloat( shaft_diam ), + shaft_len: parseFloat( shaft_len ), + flute_diam: parseFloat( flute_diam ), + flute_len: parseFloat( flute_len ), + taper_tip: parseFloat( taper_tip ), + order: parseInt( order ), } }) let IDs = new Set(); - for(let tool of tools){ //check tool IDs if( tool.id == NaN ) throw "id must be a number"; From b708da922c2a0c1f2f6b6f85b12a659d9f11dc6e Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Thu, 27 Nov 2025 08:20:32 -0700 Subject: [PATCH 5/8] fix nan comparison --- src/kiri/mode/cam/tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 13c32b47b..e46475aea 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -389,7 +389,7 @@ export function decodeToolCSV(data){ let IDs = new Set(); for(let tool of tools){ //check tool IDs - if( tool.id == NaN ) throw "id must be a number"; + if( Number.isNAN(tool.id) ) throw "id must be a number"; if( IDs.has(tool.id) ) throw "tool ids must be unique"; IDs.add(tool.id); // check remaining fields From 450b43b971dacecd2f601806d45b248bb2aa4755 Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Thu, 27 Nov 2025 09:01:03 -0700 Subject: [PATCH 6/8] condense export menu --- src/kiri/mode/cam/tools.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index e46475aea..bcf4f13f5 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -389,7 +389,7 @@ export function decodeToolCSV(data){ let IDs = new Set(); for(let tool of tools){ //check tool IDs - if( Number.isNAN(tool.id) ) throw "id must be a number"; + if( Number.isNaN(tool.id) ) throw "id must be a number"; if( IDs.has(tool.id) ) throw "tool ids must be unique"; IDs.add(tool.id); // check remaining fields @@ -495,25 +495,21 @@ function _showTools() { }; ui.toolsImport.onclick = (ev) => api.event.import(ev); ui.toolsExport.onclick = () => { + let csv = ui.toolsExportCSV.checked; api.uc.prompt("Export Tools Filename", "tools").then(name => { if (!name) { return; } - const record = { - version: api.version, - tools: api.conf.get().tools, - time: Date.now() - }; - api.util.download(api.util.b64enc(record), `${name}.km`); - }); - }; - ui.toolsImportCSV.onclick = (ev) => api.event.import(ev); - ui.toolsExportCSV.onclick = () => { - api.uc.prompt("Export Tools Filename", "tools").then(name => { - if (!name) { - return; + if (csv) { + api.util.download(generateToolCSV(), `${name}.csv`); + }else{ + const record = { + version: api.version, + tools: api.conf.get().tools, + time: Date.now() + }; + api.util.download(api.util.b64enc(record), `${name}.km`); } - api.util.download(generateToolCSV(), `${name}.csv`); }); }; From 1588c1625ab418cf98430f65a41360757daed3cb Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Thu, 27 Nov 2025 09:01:21 -0700 Subject: [PATCH 7/8] switch to tool export checkbox --- src/kiri/core/init.js | 1 - web/kiri/index.html | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index f91ebecdb..0f9c354bc 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -922,7 +922,6 @@ function init_one() { toolsClose: $('tools-close'), toolsImport: $('tools-import'), toolsExport: $('tools-export'), - toolsImportCSV: $('tools-import-csv'), toolsExportCSV: $('tools-export-csv'), toolSelect: $('tool-select'), toolAdd: $('tool-add'), diff --git a/web/kiri/index.html b/web/kiri/index.html index e92fd3d8f..f85ea689a 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -614,8 +614,12 @@
- - +
+ +
+ + +
From 1ac27f86ba91497014c4fa58e7dfbee2f8edfa8d Mon Sep 17 00:00:00 2001 From: bakedpotatolord Date: Sun, 30 Nov 2025 15:43:04 -0700 Subject: [PATCH 8/8] change option name to 'CSV', remove comment HTML --- web/kiri/index.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/kiri/index.html b/web/kiri/index.html index f85ea689a..fcc7dcb98 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -615,11 +615,8 @@
- +
- - -