Created
November 17, 2022 12:04
-
-
Save oisincar/4584ead55a52c242b62d16c8e67d6900 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { promises as fs } from 'fs'; | |
interface FieldData { | |
x: number; | |
w: number; | |
} | |
type KeyboardLayoutField = FieldData | 'key'; | |
const u = 19.05; | |
const hole_width = 14; | |
const hole_gap = u - hole_width; | |
// How much space around the outside of the board - must be > 0 | |
const board_padding = hole_gap; | |
// The diameter (mm) that this particular laser cuts out | |
const kerf = 0.105; | |
const kerf2 = 0.5*kerf; | |
const board_width = 7.75*u - hole_gap + 2*board_padding; | |
const board_top_padding = board_padding; | |
const board_height = 5*u - hole_gap + 2*board_padding + board_top_padding; // Second is for holes at top | |
const corner_radius = 5.05; | |
const board_offset_padding = 0.75*board_padding + kerf; | |
const board_offset_x = 1*(board_width + board_offset_padding); | |
const board_offset_y = 1*(board_height + board_offset_padding); | |
// M2 >= 2.0 | |
const screw_size_small = 2.1; | |
// spacer >= 3.3 | |
// const screw_size_big = 3.4; | |
const screw_size_big = 2.1; | |
const screw_padding = hole_gap; | |
// The full screw_padding square width | |
const screw_square = screw_size_big+2*screw_padding; | |
const screw_square2 = 0.5*screw_square; | |
// The gap between the screw padding and a key hole 0.5u from the edge of the board | |
const screw_padding_gap = (board_padding + 0.5*u) - screw_square; | |
// // How far down screw padding in top right is | |
const screw_top_right = 0; board_padding; | |
// ponoko needs style on each g element | |
const styleText = 'style="fill:none;stroke:#000000;stroke-width:0.2"'; | |
// Offsets between rows along right edge of left piece... | |
let left_edge_dumbo = [-0.5, 0.25, -0.5, 0.5]; | |
// Vice versa | |
let right_edge_dumbo = [0.5, -0.25, 0.5, -0.5].reverse(); | |
function parseKeyboardLayoutField(text: string): [KeyboardLayoutField, string] | 'error' { | |
if (text.length === 0) { | |
return 'error'; | |
} | |
if (text.charAt(0) === '{') { | |
let res = {x: -1, w: -1}; | |
let endPos = text.indexOf('}'); | |
if (endPos === -1) { | |
return 'error'; | |
} | |
let pairs = text.slice(1, endPos).split(','); | |
for (let pairText of pairs) { | |
let pair = pairText.split(':'); | |
if (pair[0] === 'x') { | |
res.x = parseFloat(pair[1]); | |
} | |
if (pair[0] === 'w') { | |
res.w = parseFloat(pair[1]); | |
} | |
} | |
return [res, text.slice(endPos+1)]; | |
} else if (text.charAt(0) === '"') { | |
let pos = 1; | |
while (pos < text.length && text.charAt(pos) !== '"') { | |
if (text.charAt(pos) === '\\') { | |
pos++; | |
} | |
pos++; | |
} | |
if (pos >= text.length) { | |
return 'error'; | |
} | |
return ['key', text.slice(pos+1)]; | |
} else { | |
return 'error'; | |
} | |
} | |
function parseKeyboardLayoutLine(text: string): [FieldData[], string] | 'error' { | |
let x = 0; | |
let lastFieldData: FieldData | undefined; | |
let res: FieldData[] = []; | |
while (text.length > 0 && text.charAt(0) !== ']') { | |
if (text.charAt(0) === ',') { | |
text = text.slice(1); | |
} | |
let field: KeyboardLayoutField; | |
let pair = parseKeyboardLayoutField(text); | |
if (pair === 'error') { | |
return 'error'; | |
} | |
[field, text] = pair; | |
if (field === 'key') { | |
let fieldData = {x: x, w: 1}; | |
if (lastFieldData !== undefined && lastFieldData.x !== -1) { | |
x = lastFieldData.x; | |
fieldData.x = x; | |
} | |
if (lastFieldData !== undefined && lastFieldData.w !== -1) { | |
fieldData.w = lastFieldData.w; | |
} | |
res.push(fieldData); | |
lastFieldData = undefined; | |
x += fieldData.w; | |
} else { | |
lastFieldData = field; | |
} | |
} | |
return [res, text]; | |
} | |
function parseKeyboardLayout(text: string): FieldData[][] | 'error' { | |
let res: FieldData[][] = []; | |
while (text.length > 0) { | |
if (text.charAt(0) !== '[') { | |
return 'error'; | |
} | |
let pair = parseKeyboardLayoutLine(text.slice(1)); | |
if (pair === 'error') { | |
return 'error'; | |
} | |
let values: FieldData[]; | |
[values, text] = pair; | |
res.push(values); | |
while (text.length > 0 && (text.charAt(0) !== '[')) { | |
text = text.slice(1); | |
} | |
} | |
return res; | |
} | |
function showKeyPositions(keyboardName: string, layoutText: string) { | |
console.log(keyboardName); | |
let layout = parseKeyboardLayout(layoutText); | |
if (layout === 'error') { | |
console.log('error'); | |
return; | |
} | |
for (let row = 0; row < layout.length; row++) { | |
let rowText = ' '; | |
for (let col = 0; col < layout[row].length; col++) { | |
let x = layout[row][col].x + 0.5*(layout[row][col].w - 1); | |
rowText += `[${x},${row}],`; | |
} | |
console.log(rowText); | |
} | |
console.log(''); | |
} | |
// Get part of the svg text for a sandwich layer, assuming the board outline came beforehand. | |
// This includes gaps at the top for micro usb and trrs (unless connected === true), and screw holes. | |
// Starting in the top left, after the curved corner. | |
function get_layer(top_length_left: number, top_length_right: number, is_left: boolean, connected = false): string { | |
let text = ''; | |
let corner_locs; | |
if (is_left) { | |
corner_locs = [[0, 0], | |
[0, board_height], | |
[board_width-corner_radius-hole_gap/2 -board_padding, 0], | |
[board_width-corner_radius-hole_gap/2 -board_padding - u*0.25, board_height]]; | |
} else { | |
corner_locs = [[(0.625+0.25)*u + kerf2 + 0, 0], | |
[0.625*u+kerf2, board_height - 0], | |
[board_width, 0], | |
[board_width, board_height]]; | |
} | |
if (connected) { | |
// Complete the outer perimeter, and start the inner | |
text += ` Z" />\n`; | |
text += ` <path d="M ${0.5*board_width} ${board_padding+kerf2}\n`; | |
} else { | |
text += ` H ${top_length_left+kerf2} V ${board_padding+kerf2}\n`; | |
} | |
if (is_left) { | |
// top left screw padding | |
text += ` H ${screw_square+kerf2}\n`; | |
text += ` V ${screw_square2} a ${screw_square2+kerf2} ${screw_square2+kerf2} 0 0 1 -${screw_square2+kerf2} ${screw_square2+kerf2} H ${board_padding+kerf2}\n`; | |
// bottom left screw padding | |
text += ` V ${board_height-screw_square-kerf2}\n`; | |
text += ` H ${screw_square2} a ${screw_square2+kerf2} ${screw_square2+kerf2} 0 0 1 ${screw_square2+kerf2} ${screw_square2+kerf2} V ${board_height-(board_padding+kerf2)}\n`; | |
} | |
else { | |
// do squiggles here... | |
text += ` V ${board_padding}\n`; // Unneeded? | |
text += ` H ${corner_locs[0][0] + screw_square}\n`; | |
text += ` V ${board_padding+board_top_padding}\n`; | |
text += ` H ${corner_locs[0][0] + hole_gap/2}\n`; | |
// Go up to midway up the key | |
text += ` V ${board_padding + board_top_padding - hole_gap/2 + 0.5*u}\n`; | |
for (let i = 0; i < 4; i++) { | |
let x = right_edge_dumbo[3-i]; | |
// Align y with start of nub bit... | |
text += ` v ${0.5*u - hole_gap/2}\n`; | |
if (x < 0) | |
text += ` h ${-x*u}\n`; | |
let nub_sz = 0.3*u; | |
text += ` h ${nub_sz}\n`; | |
text += ` v ${hole_gap}\n`; | |
text += ` h ${-nub_sz}\n`; | |
if (x >= 0) | |
text += ` h ${-x*u}\n`; | |
text += ` v ${0.5*u - hole_gap/2}\n`; | |
} | |
// Draw bottom left screwhole | |
text += ` V ${board_height - screw_square}\n`; | |
text += ` H ${corner_locs[1][0] + 0.5*u}\n`; | |
text += ` V ${board_height - board_padding}\n`; | |
} | |
if (!is_left) { | |
// bottom right screw padding | |
text += ` H ${board_width-(screw_square+kerf2)}\n`; | |
text += ` V ${board_height-(screw_square2)} a ${screw_square2+kerf2} ${screw_square2+kerf2} 0 0 1 ${screw_square2+kerf2} -${screw_square2+kerf2} H ${board_width-(board_padding+kerf2)}\n`; | |
// top right screw | |
text += ` V ${board_padding + board_top_padding}\n`; | |
text += ` H ${board_width - screw_square}\n`; | |
text += ` V ${board_padding}\n`; | |
// top right screw padding (1u lower, so more tricky) | |
// text += ` V ${screw_top_right+screw_square+kerf2}\n`; | |
// text += ` H ${board_width-(screw_square2)} a ${screw_square2+kerf2} ${screw_square2+kerf2} 0 0 1 0 -${screw_square+kerf} H ${board_width-(board_padding+kerf2)}\n`; | |
// text += ` V ${board_padding+kerf2}\n`; | |
} | |
else { | |
text += ` H ${corner_locs[3][0] - 0.5*u}\n`; | |
// ALigned with bottom of keys... | |
text += ` v ${-screw_square2}\n`; | |
text += ` h ${0.5*u-hole_gap/2}\n`; | |
// Go up to midway up the key | |
text += ` V ${board_height - board_padding/2 - 0.5*u}\n`; | |
for (let i = 0; i < 4; i++) { | |
let x = left_edge_dumbo[3-i]; | |
// Align y with start of nub bit... | |
text += ` v ${-0.5*u + hole_gap/2}\n`; | |
if (x > 0) | |
text += ` h ${-x*u}\n`; | |
let nub_sz = 0.3*u; | |
text += ` h ${-nub_sz}\n`; | |
text += ` v ${-hole_gap}\n`; | |
text += ` h ${nub_sz}\n`; | |
if (x <= 0) | |
text += ` h ${-x*u}\n`; | |
text += ` v ${-0.5*u + hole_gap/2}\n`; | |
} | |
// Go up to top of top key, and draw square around top left screw | |
text += ` V ${board_padding+board_top_padding}\n`; | |
text += ` H ${corner_locs[2][0]-screw_square}`; | |
text += ` V ${board_padding}\n`; | |
} | |
if (!connected) { | |
text += ` H ${board_width-top_length_right-kerf2} V ${-kerf2}\n`; | |
} | |
text += ` Z" />\n`; | |
let radius = 0.5*(screw_size_big)-kerf2; | |
text += screwHoles(is_left, radius, true); | |
// text += ` <circle cx="${screw_square2}" cy="${screw_square2}" r="${radius}" />\n`; | |
// text += ` <circle cx="${screw_square2}" cy="${board_height-screw_square2}" r="${radius}" />\n`; | |
// text += ` <circle cx="${board_width-screw_square2}" cy="${board_height-screw_square2}" r="${radius}" />\n`; | |
// text += ` <circle cx="${board_width-screw_square2}" cy="${screw_top_right+screw_square2}" r="${radius}" />\n`; | |
return text; | |
} | |
function drawHex(x: number, y: number): string { | |
let r = 3.3*0.5; | |
let a = 0; | |
let text = ''; | |
text += `<path d="M ${x+r*Math.cos(a)} ${y+r*Math.sin(a)}\n`; | |
for (let i = 0; i < 5; i++) { | |
a += Math.PI/3; | |
text += `L ${x+r*Math.cos(a)} ${y+r*Math.sin(a)}\n`; | |
} | |
text += `Z" />\n`; | |
return text; | |
} | |
function screwHoles(is_left, radius, is_hex=false): string { | |
// Hole cols | |
let hole_locs; | |
if (is_left) { | |
hole_locs = [[screw_square2, screw_square2], | |
[screw_square2, board_height - screw_square2], | |
[board_width-corner_radius-hole_gap/2 -board_padding - screw_square2, screw_square2], | |
[board_width-corner_radius-hole_gap/2 -board_padding - u*0.25 - 0.25*u, board_height-screw_square2]]; | |
} else { | |
hole_locs = [[(0.625+0.25)*u + kerf2 + screw_square2, screw_square2], | |
[0.625*u+kerf2 + 0.25*u, board_height - screw_square2], | |
[board_width-screw_square2, screw_square2], | |
[board_width-screw_square2, board_height-screw_square2]]; | |
} | |
let text = ''; | |
for (let i = 0; i < 4; i++) { | |
if (!is_hex) { | |
text += ` <circle cx="${hole_locs[i][0]}" cy="${hole_locs[i][1]}" r="${radius}" />\n`; | |
} | |
else { | |
let cx = hole_locs[i][0]; | |
let cy = hole_locs[i][1]; | |
text += drawHex(cx, cy); | |
} | |
} | |
return text; | |
} | |
function getLineFromOffsets(offsets, is_up): string { | |
let y_sign = 1; | |
if (is_up) y_sign = -1; | |
let board_outline = `v ${y_sign*(u+hole_gap/2)}\n`; | |
// Going down, gotta add in hole spacing at top | |
if (!is_up) | |
board_outline += `v ${board_top_padding}\n`; | |
offsets.forEach(function (o) { | |
board_outline += `h ${o*u}\n`; | |
board_outline += `v ${y_sign*u}\n`; | |
}); | |
board_outline += `v ${y_sign*hole_gap/2}\n`; | |
if (is_up) | |
board_outline += `v ${-board_top_padding}\n`; | |
return board_outline; | |
} | |
function getSvg(keyboardName: string, layoutText: string, svgPos: number, ): string { | |
let layout = parseKeyboardLayout(layoutText); | |
if (layout === 'error') { | |
console.log(keyboardName, 'error'); | |
return; | |
} | |
let is_left = keyboardName == 'left'; | |
// The outline of the board, aprt from the top, starting with the top right curve | |
// Add kerf on all sides, so that key positions don't need to change | |
let board_outline = ''; | |
board_outline += ` <path d="\n`; | |
// Top corner/ right side of board | |
if (is_left) { | |
board_outline += ` M ${board_width-corner_radius-hole_gap/2 -board_padding} ${-kerf2}\n`; | |
board_outline += getLineFromOffsets(left_edge_dumbo, false); | |
} | |
else { | |
board_outline += ` M ${board_width-corner_radius} ${-kerf2}\n`; | |
board_outline += ` a ${corner_radius+kerf2} ${corner_radius+kerf2} 0 0 1 ${corner_radius+kerf2} ${corner_radius+kerf2}\n`; | |
board_outline += ` V ${board_height-corner_radius}`; | |
board_outline += ` a ${corner_radius+kerf2} ${corner_radius+kerf2} 0 0 1 -${corner_radius+kerf2} ${corner_radius+kerf2}\n`; | |
} | |
// Bottom / Left side of board | |
if (!is_left) { | |
board_outline += `H ${corner_radius + 0.375*u}\n`; | |
board_outline += getLineFromOffsets(right_edge_dumbo, true); | |
} | |
else { | |
board_outline += ` H ${corner_radius}`; | |
board_outline += ` a ${corner_radius+kerf2} ${corner_radius+kerf2} 0 0 1 -${corner_radius+kerf2} -${corner_radius+kerf2}\n`; | |
board_outline += ` V ${corner_radius}`; | |
board_outline += ` a ${corner_radius+kerf2} ${corner_radius+kerf2} 0 0 1 ${corner_radius+kerf2} -${corner_radius+kerf2}\n`; | |
} | |
// 1st layer. | |
// ponoko needs style on each g element | |
let text = ''; | |
text += ` <g transform="translate(${board_offset_padding + 0*board_offset_x} ${board_offset_padding + svgPos*board_offset_y})" ${styleText}>\n`; | |
text += board_outline; | |
text += ` Z" />\n`; | |
for (let row = 0; row < layout.length; row++) { | |
for (let col = 0; col < layout[row].length; col++) { | |
let x = layout[row][col].x + 0.5*(layout[row][col].w - 1); | |
let k_x = board_padding + x*u + kerf2; | |
let k_y = board_padding + board_top_padding + row*u + kerf2; | |
if (keyboardName == 'left') | |
k_x -= 0.25*u; | |
text += ` <rect width="${hole_width - kerf}" height="${hole_width - kerf}" x="${k_x}" y="${k_y}" />\n`; | |
} | |
} | |
let radius = 0.5*(screw_size_small)-kerf2; | |
text += screwHoles(is_left, radius); | |
text += ' </g>\n'; | |
// Layer 2 - micro usb + trrs | |
let usb_width2 = 5; | |
// >= 2.5 | |
let trrs_width2 = 2.7; | |
// the trrs socket is laid on its side, with the legs pointing towards the micro usb socket | |
// half width of trrs + leg length >= 5.3 | |
let trrs_legs_width2 = 7.0; | |
text += ` <g transform="translate(${board_offset_padding + 1*board_offset_x} ${board_offset_padding + svgPos*board_offset_y})" ${styleText}>\n`; | |
let layer2_left: number; | |
let layer2_right: number; | |
if (keyboardName === 'left') { | |
layer2_left = board_padding - 0.5*hole_gap + 5.5*u - usb_width2 - 0.5*u; | |
layer2_right = board_padding - 0.5*hole_gap + 1.25*u - trrs_width2 + 0.5*u; | |
} else { | |
layer2_left = board_padding - 0.5*hole_gap + 0.75*u - trrs_width2 + u; | |
layer2_right = board_padding - 0.5*hole_gap + 6.0*u - usb_width2 - u; | |
} | |
text += board_outline; | |
text += get_layer(layer2_left, layer2_right, is_left); | |
text += ' </g>\n'; | |
// Layer 3 | |
text += ` <g transform="translate(${board_offset_padding + 2*board_offset_x} ${board_offset_padding + svgPos*board_offset_y})" ${styleText}>\n`; | |
let layer3_left: number; | |
let layer3_right: number; | |
if (keyboardName === 'left') { | |
layer3_left = board_padding - 0.5*hole_gap + 6.5*u - trrs_legs_width2 - 0.5*u; | |
layer3_right = board_padding - 0.5*hole_gap + 1.25*u - trrs_width2 + 0.5*u; | |
} else { | |
layer3_left = board_padding - 0.5*hole_gap + 0.75*u - trrs_width2 + u; | |
layer3_right = board_padding - 0.5*hole_gap + 7.0*u - trrs_legs_width2 - u; | |
} | |
text += board_outline; | |
text += get_layer(layer3_left, layer3_right, is_left); | |
text += ' </g>\n'; | |
// Layer 4 | |
text += ` <g transform="translate(${board_offset_padding + 3*board_offset_x} ${board_offset_padding + svgPos*board_offset_y})" ${styleText}>\n`; | |
text += board_outline; | |
text += ` Z" />\n`; | |
text += screwHoles(is_left, radius); | |
text += ' </g>\n'; | |
// Layer 5 - extra sandwich layer for cherry MX switches, rather than kailh choc v2 | |
text += ` <g transform="translate(${board_offset_padding + 4*board_offset_x} ${board_offset_padding + svgPos*board_offset_y})" ${styleText}>\n`; | |
text += board_outline; | |
text += get_layer(0, 0, is_left, true); | |
text += ' </g>\n'; | |
return text; | |
} | |
async function main() { | |
let leftText = await fs.readFile('left.txt', 'utf8'); | |
let rightText = await fs.readFile('right.txt', 'utf8'); | |
showKeyPositions('left', leftText); | |
showKeyPositions('right', rightText); | |
let svgAll = ''; | |
svgAll += '<?xml version="1.0" encoding="UTF-8"?>\n'; | |
svgAll += '<svg xmlns="http://www.w3.org/2000/svg" width="790mm" height="384mm" viewBox="0 0 790 384"\n'; | |
svgAll += ` ${styleText}>\n`; | |
svgAll += getSvg('left', leftText, 0); | |
svgAll += getSvg('right', rightText, 1); | |
svgAll += '</svg>\n'; | |
await fs.writeFile('out.svg', svgAll, 'utf8'); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment