Skip to content

Instantly share code, notes, and snippets.

@oisincar
Created November 17, 2022 12:04
Show Gist options
  • Save oisincar/4584ead55a52c242b62d16c8e67d6900 to your computer and use it in GitHub Desktop.
Save oisincar/4584ead55a52c242b62d16c8e67d6900 to your computer and use it in GitHub Desktop.
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