Skip to content
Open
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
1 change: 1 addition & 0 deletions src/kiri/core/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ function init_one() {
toolsClose: $('tools-close'),
toolsImport: $('tools-import'),
toolsExport: $('tools-export'),
toolsExportCSV: $('tools-export-csv'),
toolSelect: $('tool-select'),
toolAdd: $('tool-add'),
toolCopy: $('tool-dup'),
Expand Down
8 changes: 7 additions & 1 deletion src/kiri/core/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand Down
203 changes: 195 additions & 8 deletions src/kiri/mode/cam/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,191 @@ 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 header = [
'id',
'number',
'type',
'name',
'metric',
'shaft_diam',
'shaft_len',
'flute_diam',
'flute_len',
'taper_tip',
'order',
'api_version='+ api.version,
].join(',');

let acc = header + '\n';
for( let [i,t] of tools.entries() ) {
acc +=
[
t.id,
t.number,
t.type,
escapeCSV(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;
}


/**
* 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 = "";
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;
}
/**
*
* @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;
try{
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)

try{
//get and parse tools line by line
let tools = data.split( '\n' )
.slice( 1 )
.filter( line => line.length > 0 )
.map( 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, order})
return {
id: parseInt( id ),
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 ),
}
})

let IDs = new Set();
for(let tool of tools){
//check tool IDs
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
if( toolNames.indexOf(tool.type) == -1 ) throw "tool type must be one of " + toolNames.join(', ');
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, {
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() {
Expand Down Expand Up @@ -313,16 +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`);
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`);
}
});
};

Expand Down
3 changes: 3 additions & 0 deletions web/kiri/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@
<div class="f-row">
<button id="tools-import" lk="import">import</button>
<button id="tools-export" lk="export">export</button>
<div class="f-row a-center">
<label lk="csv">CSV</label><input id="tools-export-csv" type="checkbox"></input>
</div>
</div>
</div>
</div>
Expand Down