From 642097a283a6d3b94e4b39caae55c81f4ff2764d Mon Sep 17 00:00:00 2001 From: Water-Zi <928069358@qq.com> Date: Fri, 19 May 2023 02:54:15 +0800 Subject: [PATCH] Added support for importing Altium Designer CSV. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This code provides an interface for directly importing CSV files from Altium Designer, reorganizing and rearranging the CSV, and then invoking the original interface for the import operation. However, there is a question regarding the documentation: it mentions the need to take the inverse of some coordinates, specifically the coordinates of the fiducial marks. However, in actual testing, it was found that the original interface automatically takes the inverse of the y-axis coordinates during the CSV import. Modifying the original CSV document or taking the inverse when copying the coordinates of the fiducial marks can lead to incorrect positioning of the components during operation. On the contrary, if no modifications are made to the output CSV data and the coordinates from the component list are directly copied into the Fiducial PCB column, the resulting operational position aligns with the expectations. Further testing may be required for this aspect, but at least on my computer with AD22.0.2 (Build 36), this is the observed behavior. 这些代码提供了一个直接导入Altium Designer的CSV文件的接口,对CSV进行重新组织和排列,然后调用原接口进行导入操作。但对于文档有一点疑问:文档中提到需要将一些坐标取反,对标记点的坐标取反。但是在实际测试中发现,原接口在导入CSV时会对y轴坐标取反,在此基础上,如果修改原CSV文档或在抄写标记点坐标时取反,都将导致设备运行的位置出错。相反,如果不对输出的CSV数据做任何修改,直接将component list中的坐标抄写到Fiducial pcb一栏中,得到的运行结果是符合预期的。对于这部分内容,可能还需要进行更多的测试,但至少在我的电脑上是这样的。我使用的是AD22.0.2(Build36) --- html/index.html | 13 ++ html/index.js | 1 + html/libs/load_altium_csv_modal.css | 39 ++++++ html/libs/papaparse.min.js | 7 + html/load_altium_csv.js | 190 ++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 html/libs/load_altium_csv_modal.css create mode 100644 html/libs/papaparse.min.js create mode 100644 html/load_altium_csv.js diff --git a/html/index.html b/html/index.html index 79f95f6..e24c76d 100644 --- a/html/index.html +++ b/html/index.html @@ -16,6 +16,8 @@ border-bottom-width: 0; } + + @@ -24,6 +26,7 @@

CDPnP

+  |  @@ -141,6 +144,7 @@

CDPnP

Fiducial pcb: +
@@ -230,6 +234,15 @@

Offsets

+ + + diff --git a/html/index.js b/html/index.js index 292edfa..0600a3e 100644 --- a/html/index.js +++ b/html/index.js @@ -13,6 +13,7 @@ import { search_comp_parents, search_next_comp, select_comp, move_to_comp, get_c import { input_init, csa_to_page_input } from './input_ctrl.js'; import { get_camera_cfg, get_motor_pos, set_motor_pos, set_pump, pcb2xyz, z_keep_high, enable_force, cam_comp_snap, set_camera_cfg } from './dev_cmd.js'; +import './load_altium_csv.js'; let csa_dft = { cur_pos: [0, 0, 0, 0], diff --git a/html/libs/load_altium_csv_modal.css b/html/libs/load_altium_csv_modal.css new file mode 100644 index 0000000..d4503c2 --- /dev/null +++ b/html/libs/load_altium_csv_modal.css @@ -0,0 +1,39 @@ +.load_altium_csv_modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.load_altium_csv_modal_content { + background-color: #fefefe; + margin: 20% auto; + padding: 20px; + border: 1px solid #888; + width: 400px; +} + +.load_altium_csv_modal_buttons { + text-align: center; + margin-top: 20px; +} + +.load_altium_csv_modal_button { + margin: 0 10px; + padding: 8px 20px; + border: 1px solid gray; + background-color: #D5F5E3; + color: black; + cursor: pointer; + font-weight: bold; + transition: background-color 0.1s; +} + +.load_altium_csv_modal_button.hovered { + background-color: #97e6ba; +} \ No newline at end of file diff --git a/html/libs/papaparse.min.js b/html/libs/papaparse.min.js new file mode 100644 index 0000000..eeaf983 --- /dev/null +++ b/html/libs/papaparse.min.js @@ -0,0 +1,7 @@ +/* @license +Papa Parse +v5.4.1 +https://github.com/mholt/PapaParse +License: MIT +*/ +!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=f.IS_PAPA_WORKER||!1,a={},u=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;J(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!J(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ","(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=u++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=J(t.step),t.chunk=J(t.chunk),t.complete=J(t.complete),t.error=J(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?(e=function(e){if(65279===e.charCodeAt(0))return e.slice(1);return e}(e),n=t.download?new l(t):new p(t)):!0===e.readable&&J(e.read)&&J(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,r=!1,i=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");i=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);("boolean"==typeof t.escapeFormulae||t.escapeFormulae instanceof RegExp)&&(o=t.escapeFormulae instanceof RegExp?t.escapeFormulae:/^[=+\-@\t\r].*$/)}();var u=new RegExp(Q(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return h(null,e,r);if("object"==typeof e[0])return h(i||Object.keys(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||i),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),h(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(J(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!J(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){J(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),h.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)),i.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===i.status&&this._chunkError()}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:i.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),h.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;h.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=r.substring(0,t),r=r.substring(t)):(e=r,r=""),this._finished=!r,this.parseChunk(e)}}}function g(e){h.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){h.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){h.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(m){var a,o,u,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,h=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(J(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;r+=e.data.length,m.preview&&r>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){return c&&u&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),u=!1),m.skipEmptyLines&&(c.data=c.data.filter(function(e){return!y(e)})),_()&&function(){if(!c)return;function e(e,t){J(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t=l.length?"__parsed_extra":l[r]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return m.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),u=!1,m.delimiter)J(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,u;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var h=0;h=N)return L(!0)}else for(S=W,W++;;){if(-1===(S=i.indexOf(z,S+1)))return r||h.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:u.length,index:W}),T();if(S===n-1)return T(i.substring(W,S).replace(C,z));if(z!==K||i[S+1]!==K){if(z===K||0===S||i[S-1]!==K){-1!==w&&w=N)return L(!0);break}h.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:u.length,index:W}),S++}}else S++}return T();function I(e){u.push(e),d=W}function A(e){var t=0;if(-1!==e){var r=i.substring(S+1,e);r&&""===r.trim()&&(t=r.length)}return t}function T(e){return r||(void 0===e&&(e=i.substring(W)),f.push(e),W=n,I(f),o&&F()),L()}function D(e){W=e,I(f),f=[],R=i.indexOf(P,W)}function L(e){return{data:u,errors:h,meta:{delimiter:M,linebreak:P,aborted:H,truncated:!!e,cursor:d+(t||0)}}}function F(){q(L()),u=[],h=[]}},this.abort=function(){H=!0},this.getCharIndex=function(){return W}}function _(e){var t=e.data,r=a[t.workerId],i=!1;if(t.error)r.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){i=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(J(r.userStep)){for(var s=0;s + */ + +import { read_file, download, readable_float, cpy, sleep, wildcard_test } from './utils/helper.js'; +import { csv_to_pos, pos_to_page } from './pos_list.js' + +let layer_selection_modal = document.getElementById("modal_load_altium_csv"); +let modalLayersButtonsContainer = document.getElementById("load_altium_csv_modal_buttons_container"); + +var csvObject; + +document.getElementById('btn_load_altium_csv').onclick = async function() { + console.log('load_altium_csv'); + + //let input = document.createElement('input'); + //cpy(input, {type: 'file', accept: '*.cdg'}, ['type', 'accept']); + let input = document.getElementById('input_file'); + input.accept = '.csv'; + input.onchange = async function () { + var files = this.files; + if (files && files.length) { + let file = files[0]; + let data_str = await readANSIToUTF8(file); + + //以下删除Altium的文件头,要求文件头中必须有以下列,否则无法进行转换 + var csvData = data_str.split('\r\n'); + var filteredRows = csvData.filter(function(row) { + var keywords = ['Designator', 'Comment', 'Footprint', 'Center-X(mm)', 'Center-Y(mm)', 'Rotation', 'Layer']; + return keywords.every(function(keyword) { + return row.includes(keyword); + }); + }); + var rowIndex = csvData.findIndex(function(row) { + return row === filteredRows[0]; + }); + if (rowIndex == -1) { + alert("The Pick and Place file must contains the following information: \r\nDesignator\r\nComment\r\nFootprint\r\nCenter-X(mm)\r\nCenter-Y(mm)\r\nRotation\r\nLayer\r\nThe selected file has missing information."); + return; + } + csvData.splice(0, rowIndex); + + //以下删除多余的空行,因为Description中可能存在空行,输出是在csv文件中也会被保留,需要删除 + for (var i = 0; i < csvData.length - 1; i++) { + var currentRow = csvData[i]; + var nextRow = csvData[i + 1]; + + if (currentRow[currentRow.length - 1] !== '"') { + csvData[i] = currentRow.trim() + nextRow.trim(); + csvData.splice(i + 1, 1); + i--; // 调整索引,以便处理合并后的行 + } + } + + //以下解析Altium的CSV,便于处理数据 + let config = buildConfig(); + csvObject = Papa.parse(csvData.join('\r\n'), config); + + //以下处理数据,并生成一个新的CSV String,符合KiCad格式,随后交给常规的导入函数进行导入 + var layers = new Set(); + for (let elm of csvObject.data) { + layers.add(elm.Layer); + //底层的话还要修改座标文件,把所有 X 轴数据取反 + /* + if (elm.Layer == "BottomLayer") { + elm['Center-X(mm)'] = elm['Center-X(mm)'] * -1 + } + */ + //实际测得不需要取反 + } + + //以下处理当CSV中存在多个Layer时,让用户选择需要导入的Layer + if (layers.size == 1) { + createKicadCSVString(Array.from(layers)[0]); + } else { + modifyAndShowLayerOptions(layers); + } + console.log(csvObject); + console.log(layers); + + } + this.value = ''; + }; + input.click(); +}; + +function buildConfig() { + return { + delimiter: "", + header: true, + dynamicTyping: true, + skipEmptyLines: true, + worker: false + }; +} + +function modifyAndShowLayerOptions(layers) { + // 清空旧的选项按钮 + modalLayersButtonsContainer.innerHTML = ""; + + for (let layer of layers) { + var layerButton = document.createElement("button"); + layerButton.className = "load_altium_csv_modal_button"; + layerButton.textContent = layer; + layerButton.addEventListener("click", handleLayerOptionButtonOnClick.bind(null, layer)); + layerButton.addEventListener("mouseenter", function() { + this.classList.add('hovered'); + }); + layerButton.addEventListener("mouseleave", function() { + this.classList.remove('hovered'); + }); + modalLayersButtonsContainer.appendChild(layerButton); + } + + layer_selection_modal.style.display = "block"; +} + +function handleLayerOptionButtonOnClick(layer) { + layer_selection_modal.style.display = "none"; + createKicadCSVString(layer); +} + +function createKicadCSVString(layer = "") { + var inuseCSVData; + if (layer != "") { + inuseCSVData = csvObject.data.filter(obj => obj.Layer == layer); + } else { + inuseCSVData = csvObject.data + } + + var kicadString = ""; + kicadString += "Ref,Val,Package,PosX,PosY,Rot,Side" + for (let elm of inuseCSVData) { + kicadString += "\r\n"; + kicadString += "\"" + elm.Designator + "\","; + kicadString += "\"" + elm.Comment + "\","; + kicadString += "\"" + elm.Footprint + "\","; + kicadString += elm['Center-X(mm)'] + ","; + kicadString += elm['Center-Y(mm)'] + ","; + kicadString += elm.Rotation + ","; + kicadString += elm.Layer; + } + + //显示关于Fiducial的提示文字 + let waring_text = document.getElementById("fiducial_warning_message"); + waring_text.innerHTML = `Simply copy the coordinates from the table below. Documentation is here.`; + /* + if (layer == "BottomLayer") { + //底层的话,要在 CDPnP 软件界面,把参考点 PCB 文件座标 X 和 Y 轴都取反 + waring_text.innerHTML = "CSV from Altium, using BottomLayer, you should inverse both X-axis and Y-axis value of markers."; + } else if (layer == "TopLayer") { + //顶层的话,要在 CDPnP 软件界面,把参考点 PCB 文件座标 Y 轴取反 + waring_text.innerHTML = "CSV from Altium, using TopLayer, you should inverse Y-axis value of markers."; + } + */ + waring_text.style.display = "inline-block"; + + importAltiumCSVFinalStep(kicadString); +} + +function importAltiumCSVFinalStep(kicadString) { + let pos = csv_to_pos(kicadString); + console.log('load pos:', pos); + pos_to_page(pos); + document.getElementById('btn_save_cfg').style.background = 'yellow'; +} + +//由于Altium输出的CSV编码为GBK,所以需要转换为utf-8进行读取,中文才不会乱码 +async function readANSIToUTF8(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = function(event) { + const content = event.target.result; + const ansiDecoder = new TextDecoder('GBK'); // 将解码字符集更改为 GBK + + const ansiText = ansiDecoder.decode(content); + + resolve(ansiText); + }; + + reader.onerror = function() { + reject(new Error("Failed to read the file.")); + }; + + reader.readAsArrayBuffer(file); + }); +} \ No newline at end of file