Files
wenil d8cb0dc06d Initial commit: Busbar Designer
Web tool for designing nickel/copper busbars over cylindrical-cell battery
packs (21700, 18650) in hex holders. Flask + build123d backend exports
STEP/DXF/SVG; vanilla JS frontend with live preview, multi-project SQLite
persistence, snapshot history.

Deploy scripts in deploy/ (proxmox-lxc.sh, install.sh, update.sh).
2026-05-24 18:59:50 +03:00

112 lines
3.7 KiB
JavaScript

/* importer.js — parse OpenSCAD ECHO / CSV / JSON / parametric generator.
*
* All parsers return an array of {id, x, y} in millimetres.
* Generator formulas are 1:1 mirrors of `get_hex_center_points_*` from
* Addy's hex_cell.scad (see CLAUDE.md for the cheat sheet).
*/
const Importer = (() => {
const COS30 = Math.cos(Math.PI / 6);
/** Parse OpenSCAD ECHO lines or loose `Cell N: x = …, y = …`. */
function parsePaste(text) {
const out = [];
const re = /Cell\s*(\d+)\s*:\s*x\s*=\s*(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*,\s*y\s*=\s*(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/g;
let m;
while ((m = re.exec(text)) !== null) {
out.push({ id: parseInt(m[1], 10), x: parseFloat(m[2]), y: parseFloat(m[3]) });
}
if (out.length) return out;
// Fallback: lines of form `index x y` or `index, x, y` (any separator).
const lines = text.split(/\r?\n/);
let auto = 1;
for (const line of lines) {
const nums = line.match(/-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?/g);
if (!nums) continue;
if (nums.length >= 3) {
out.push({ id: parseInt(nums[0], 10), x: parseFloat(nums[1]), y: parseFloat(nums[2]) });
} else if (nums.length === 2) {
out.push({ id: auto++, x: parseFloat(nums[0]), y: parseFloat(nums[1]) });
}
}
return out;
}
/** CSV with optional header. Columns: index,x,y OR x,y. */
function parseCSV(text) {
const out = [];
const lines = text.trim().split(/\r?\n/).filter(Boolean);
let auto = 1;
for (const raw of lines) {
const cols = raw.split(/[,;\t]/).map((s) => s.trim());
// Skip header rows that don't parse as numbers.
if (cols.length >= 2 && isNaN(parseFloat(cols[0])) && isNaN(parseFloat(cols[1]))) continue;
if (cols.length >= 3) {
const id = parseInt(cols[0], 10);
const x = parseFloat(cols[1]);
const y = parseFloat(cols[2]);
if (!isNaN(x) && !isNaN(y)) out.push({ id: isNaN(id) ? auto++ : id, x, y });
} else if (cols.length === 2) {
const x = parseFloat(cols[0]);
const y = parseFloat(cols[1]);
if (!isNaN(x) && !isNaN(y)) out.push({ id: auto++, x, y });
}
}
return out;
}
/** JSON: array of objects {id,x,y} OR array of [x,y]. */
function parseJSON(text) {
const j = JSON.parse(text);
if (!Array.isArray(j)) throw new Error("JSON must be a top-level array");
return j.map((item, i) => {
if (Array.isArray(item)) {
return { id: i + 1, x: +item[0], y: +item[1] };
}
return {
id: item.id != null ? +item.id : i + 1,
x: +item.x,
y: +item.y,
};
});
}
/** Generator — mirrors get_hex_center_points_{rect,para,tria} from hex_cell.scad. */
function generate({ cellDia, wall, rows, cols, style }) {
const hex_w = cellDia + 2 * wall;
const hex_pt = (hex_w / 2) / COS30;
const rowY = (r) => r * 1.5 * hex_pt;
const out = [];
let id = 1;
if (style === "rect") {
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = (r % 2 === 0) ? hex_w * c : 0.5 * hex_w + hex_w * c;
out.push({ id: id++, x, y: rowY(r) });
}
}
} else if (style === "para") {
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = r * 0.5 * hex_w + hex_w * c;
out.push({ id: id++, x, y: rowY(r) });
}
}
} else if (style === "tria") {
for (let r = 0; r < rows; r++) {
for (let c = 0; c <= r; c++) {
const x = r * 0.5 * hex_w - hex_w * c;
out.push({ id: id++, x, y: rowY(r) });
}
}
} else {
throw new Error(`unknown style: ${style}`);
}
return out;
}
return { parsePaste, parseCSV, parseJSON, generate };
})();